Koen Kanters
2020-08-02 23:09:43 +02:00
parent 8aa3cf0c29
commit 852bca5a08
4 changed files with 112 additions and 7 deletions
+24 -2
View File
@@ -166,6 +166,14 @@ const cfg = {
},
// Sensor
'sensor_update_state': {
type: 'sensor',
object_id: 'update_available',
discovery_payload: {
icon: 'mdi:update',
value_template: `{{ value_json['update']['state'] }}`,
},
},
'sensor_illuminance': {
type: 'sensor',
object_id: 'illuminance',
@@ -1825,6 +1833,8 @@ class HomeAssistant extends Extension {
// A map of all discoverd devices
this.discovered = {};
this.discoveredTriggers = {};
this.legacyApi = settings.get().advanced.legacy_api;
this.newApi = settings.get().experimental.new_api;
if (!settings.get().advanced.cache_state) {
logger.warn('In order for HomeAssistant integration to work properly set `cache_state: true');
@@ -1941,12 +1951,18 @@ class HomeAssistant extends Extension {
* Publish a value for update_available (if not there yet) to prevent Home Assistant generating warnings of
* this value not being available.
*/
const supportsOTA = data.entity.definition && data.entity.definition.hasOwnProperty('ota');
const mockedValues = [
{
property: 'update_available',
condition: data.entity.device && data.entity.definition && data.entity.definition.hasOwnProperty('ota'),
condition: supportsOTA && this.legacyApi,
value: false,
},
{
property: 'update',
condition: supportsOTA && this.newApi,
value: {state: 'idle'},
},
{
property: 'water_leak',
condition: data.entity.device && data.entity.definition && mapping[data.entity.definition.model] &&
@@ -1985,7 +2001,13 @@ class HomeAssistant extends Extension {
configs.push(cfg.sensor_linkquality);
if (mappedModel.hasOwnProperty('ota')) {
configs.push(cfg.binary_sensor_update_available);
if (this.legacyApi) {
configs.push(cfg.binary_sensor_update_available);
}
if (this.newApi) {
configs.push(cfg.sensor_update_state);
}
}
if (deviceSettings && deviceSettings.hasOwnProperty('legacy') && !deviceSettings.legacy) {
+43 -3
View File
@@ -28,6 +28,17 @@ class OTAUpdate extends Extension {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/check`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`);
}
for (const device of this.zigbee.getClients()) {
// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state.
// remove them.
this.removeProgressAndRemainingFromState(device);
}
}
removeProgressAndRemainingFromState(device) {
this.state.removeKey(device.ieeeAddr, ['update', 'progress']);
this.state.removeKey(device.ieeeAddr, ['update', 'remaining']);
}
async onZigbeeEvent(type, data, resolvedEntity) {
@@ -44,7 +55,8 @@ class OTAUpdate extends Extension {
this.lastChecked[data.device.ieeeAddr] = Date.now();
const available = await resolvedEntity.definition.ota.isUpdateAvailable(data.device, logger, data.data);
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
this.publishEntityState(data.device.ieeeAddr, payload);
if (available) {
const message = `Update available for '${resolvedEntity.settings.friendly_name}'`;
@@ -88,6 +100,24 @@ class OTAUpdate extends Extension {
}
}
getEntityPublishPayload(state, progress=null, remaining=null) {
const payload = {};
/* istanbul ignore else */
if (this.legacyApi) {
payload.update_available = state === 'available';
}
/* istanbul ignore else */
if (settings.get().experimental.new_api) {
payload.update = {state};
if (progress !== null) payload.update.progress = progress;
if (remaining !== null) payload.update.remaining = Math.round(remaining);
}
return payload;
}
async onMQTTMessage(topic, message) {
if ((!this.legacyApi || !topic.match(legacyTopicRegex)) && !topic.match(topicRegex)) {
return null;
@@ -147,7 +177,8 @@ class OTAUpdate extends Extension {
);
}
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available});
const payload = this.getEntityPublishPayload(available ? 'available' : 'idle');
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now();
responseData.updateAvailable = available;
} catch (e) {
@@ -184,6 +215,9 @@ class OTAUpdate extends Extension {
logger.info(msg);
const payload = this.getEntityPublishPayload('updating', progress, remaining);
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_progress`, device: resolvedEntity.name, progress};
@@ -198,7 +232,9 @@ class OTAUpdate extends Extension {
const msg = `Finished update of '${resolvedEntity.name}'` +
(to ? `, from '${fromS}' to '${toS}'` : ``);
logger.info(msg);
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false});
this.removeProgressAndRemainingFromState(resolvedEntity.device);
const payload = this.getEntityPublishPayload('idle');
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
responseData.from = from_ ? utils.toSnakeCase(from_) : null;
responseData.to = to ? utils.toSnakeCase(to) : null;
@@ -210,6 +246,10 @@ class OTAUpdate extends Extension {
} catch (e) {
error = `Update of '${resolvedEntity.name}' failed (${e.message})`;
this.removeProgressAndRemainingFromState(resolvedEntity.device);
const payload = this.getEntityPublishPayload('available');
this.publishEntityState(resolvedEntity.device.ieeeAddr, payload);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const meta = {status: `update_failed`, device: resolvedEntity.name};
+18
View File
@@ -87,6 +87,24 @@ class State {
this.eventBus.emit('stateChange', {ID, from: fromState, to: toState, reason});
}
removeKey(ID, path) {
if (this.exists(ID)) {
let state = this.state[ID];
for (let i = 0; i < path.length; i++) {
const key = path[i];
if (i === path.length - 1) {
delete state[key];
} else {
if (state[key]) {
state = state[key];
} else {
break;
}
}
}
}
}
remove(ID) {
if (this.exists(ID)) {
delete this.state[ID];
+27 -2
View File
@@ -47,7 +47,7 @@ describe('OTA update', () => {
device.save.mockClear();
mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => {
onUpdate(0, null);
onUpdate(10, 3600);
onUpdate(10, 3600.2123);
});
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
@@ -62,6 +62,21 @@ describe('OTA update', () => {
expect(device.save).toHaveBeenCalledTimes(1);
expect(device.dateCode).toBe('20190102');
expect(device.softwareBuildID).toBe(2);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
JSON.stringify({"update_available":false,"update":{"state":"updating","progress":0}}),
{retain: true, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
JSON.stringify({"update_available":false,"update":{"state":"updating","progress":10,"remaining":3600}}),
{retain: true, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
JSON.stringify({"update_available":false,"update":{"state":"idle"}}),
{retain: true, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
JSON.stringify({"data":{"id": "bulb","from":{"software_build_id":1,"date_code":"20190101"},"to":{"software_build_id":2,"date_code":"20190102"}},"status":"ok"}),
@@ -82,6 +97,11 @@ describe('OTA update', () => {
MQTT.events.message('zigbee2mqtt/bridge/request/device/ota_update/update', JSON.stringify({id: "bulb"}));
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
JSON.stringify({"update_available":true,"update":{"state":"available"}}),
{retain: true, qos: 0}, expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
JSON.stringify({"data":{"id": "bulb"},"status":"error","error":"Update of 'bulb' failed (Update failed)"}),
@@ -222,7 +242,12 @@ describe('OTA update', () => {
mapped.ota.isUpdateAvailable.mockReturnValueOnce(false);
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`)
expect(logger.info).not.toHaveBeenCalledWith(`Update available for 'bulb'`);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bulb',
JSON.stringify({"update_available":true,"update":{"state":"available"}}),
{retain: true, qos: 0}, expect.any(Function)
);
});
it('Should respond with NO_IMAGE_AVAILABLE when not supporting OTA', async () => {