Implement update_available attribute and discover Home Assistant sensor. #2948

This commit is contained in:
Koen Kanters
2020-02-16 16:00:15 +01:00
parent 9a87abd0f4
commit 58d987b523
4 changed files with 84 additions and 2 deletions
+15 -1
View File
@@ -105,6 +105,15 @@ const cfg = {
device_class: 'battery',
},
},
'binary_sensor_update_available': {
type: 'binary_sensor',
object_id: 'update_available',
discovery_payload: {
payload_on: true,
payload_off: false,
value_template: '{{ value_json.update_available}}',
},
},
'binary_sensor_lock': {
type: 'binary_sensor',
object_id: 'lock',
@@ -1276,7 +1285,12 @@ class HomeAssistant extends BaseExtension {
return;
}
mapping[mappedModel.model].forEach((config) => {
const configs = mapping[mappedModel.model].slice();
if (mappedModel.hasOwnProperty('ota')) {
configs.push(cfg.binary_sensor_update_available);
}
configs.forEach((config) => {
const topic = this.getDiscoveryTopic(config, device);
const payload = {...config.discovery_payload};
const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`;
+20
View File
@@ -3,11 +3,13 @@ const logger = require('../util/logger');
const assert = require('assert');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
const BaseExtension = require('./baseExtension');
const MINUTES_10 = 1000 * 60 * 10;
class OTAUpdate extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
super(zigbee, mqtt, state, publishEntityState, eventBus);
this.inProgress = new Set();
this.lastChecked = {};
}
onMQTTConnected() {
@@ -15,6 +17,21 @@ class OTAUpdate extends BaseExtension {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
}
async onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
if (data.type !== 'commandQueryNextImageRequest' || !mappedDevice.hasOwnProperty('ota')) return;
// When a device does a next image request, it will usually do it a few times after each other
// with only 10 - 60 seconds inbetween. It doesn' make sense to check for a new update
// each time.
const check = this.lastChecked.hasOwnProperty(data.device.ieeeAddr) ?
(Date.now() - this.lastChecked[data.device.ieeeAddr]) > MINUTES_10 : true;
if (!check || this.inProgress.has(data.device.ieeeAddr)) return;
this.lastChecked[data.device.ieeeAddr] = Date.now();
const available = await mappedDevice.ota.isUpdateAvailable(data.device, logger, data.data);
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
}
async readSoftwareBuildIDAndDateCode(device, update) {
try {
const endpoint = device.endpoints.find((e) => e.supportsInputCluster('genBasic'));
@@ -64,6 +81,8 @@ class OTAUpdate extends BaseExtension {
logger.info(message);
const meta = {status: available ? 'available' : 'not_available', device: device.name};
this.mqtt.log('ota_update', message, meta);
this.publishEntityState(device.device.ieeeAddr, {update_available: available});
this.lastChecked[device.device.ieeeAddr] = Date.now();
} catch (error) {
const message = `Failed to check if update available for '${device.name}' (${error.message})`;
logger.error(message);
@@ -92,6 +111,7 @@ class OTAUpdate extends BaseExtension {
logger.info(message);
const meta = {status: `update_succeeded`, device: device.name, from: from_, to};
this.mqtt.log('ota_update', message, meta);
this.publishEntityState(device.device.ieeeAddr, {update_available: false});
} catch (error) {
const message = `Update of '${device.name}' failed (${error.message})`;
logger.error(message);
+32 -1
View File
@@ -5,7 +5,6 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
const flushPromises = () => new Promise(setImmediate);
const MQTT = require('./stub/mqtt');
const Controller = require('../lib/controller');
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
describe('HomeAssistant extension', () => {
beforeEach(async () => {
@@ -688,4 +687,36 @@ describe('HomeAssistant extension', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
});
it('Should discover update_available sensor when device supports it', async () => {
controller = new Controller(false);
await controller.start();
await flushPromises();
const payload = {
"payload_on":true,
"payload_off":false,
"value_template":"{{ value_json.update_available}}",
"state_topic":"zigbee2mqtt/bulb",
"json_attributes_topic":"zigbee2mqtt/bulb",
"name":"bulb_update_available",
"unique_id":"0x000b57fffec6a5b2_update_available_zigbee2mqtt",
"device":{
"identifiers":[
"zigbee2mqtt_0x000b57fffec6a5b2"
],
"name":"bulb",
'sw_version': this.version,
"model":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)",
"manufacturer":"IKEA"
},
"availability_topic":"zigbee2mqtt/bridge/state"
};
expect(MQTT.publish).toHaveBeenCalledWith(
'homeassistant/binary_sensor/0x000b57fffec6a5b2/update_available/config',
JSON.stringify(payload),
{ retain: true, qos: 0 },
expect.any(Function),
);
});
});
+17
View File
@@ -161,4 +161,21 @@ describe('OTA update', () => {
await flushPromises();
expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
});
it('Should check for update when device requests it', async () => {
const device = zigbeeHerdsman.devices.bulb;
const data = {imageType: 12382};
const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID)
mockClear(mapped);
const payload = {data, cluster: 'genOta', device, endpoint: device.getEndpoint(1), type: 'commandQueryNextImageRequest', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledWith(device, logger, {"imageType": 12382});
// Should not request again when device asks again after a short time
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1);
});
});