From 0a84412d7566b24908b0517b5f4f44969cd8cfeb Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 8 Jul 2020 23:23:44 +0200 Subject: [PATCH] Implement OTA commands for new api. https://github.com/Koenkk/zigbee2mqtt/issues/3281 --- lib/extension/otaUpdate.js | 225 ++++++++++++++++++++----------------- test/otaUpdate.test.js | 201 ++++++++++++++++++++++++++++----- 2 files changed, 296 insertions(+), 130 deletions(-) diff --git a/lib/extension/otaUpdate.js b/lib/extension/otaUpdate.js index 0a9a82773..36881ee22 100644 --- a/lib/extension/otaUpdate.js +++ b/lib/extension/otaUpdate.js @@ -1,7 +1,9 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const assert = require('assert'); +const utils = require('../util/utils'); const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/otaUpdate/(update|check)`, 'i'); + const Extension = require('./extension'); const MINUTES_10 = 1000 * 60 * 10; @@ -19,6 +21,14 @@ class OTAUpdate extends Extension { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`); this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`); } + + /* istanbul ignore else */ + if (settings.get().experimental.new_api) { + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/otaUpdate/check`); + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/otaUpdate/update`); + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/otaupdate/check`); + this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/request/device/otaupdate/update`); + } } async onZigbeeEvent(type, data, resolvedEntity) { @@ -80,140 +90,147 @@ class OTAUpdate extends Extension { } async onMQTTMessage(topic, message) { - let resolvedEntity = null; - /* istanbul ignore else */ - if (this.legacyApi) { - if (!topic.match(legacyTopicRegex)) { - return null; - } - - resolvedEntity = this.zigbee.resolveEntity(message); - } else { + if ((!this.legacyApi || !topic.match(legacyTopicRegex)) && !topic.match(topicRegex)) { return null; } - assert(resolvedEntity != null && resolvedEntity.type === 'device', 'Device not found or not a device'); - if (!resolvedEntity.definition || !resolvedEntity.definition.ota) { - const message = `Device '${resolvedEntity.name}' does not support OTA updates`; - logger.error(message); + message = utils.parseJSON(message, message); + const ID = typeof message === 'object' && message.hasOwnProperty('ID') ? message.ID : message; + const resolvedEntity = this.zigbee.resolveEntity(ID); + const type = topic.substring(topic.lastIndexOf('/') + 1); + const responseData = {ID}; + let error = null; + + if (!resolvedEntity || resolvedEntity.type !== 'device') { + error = `Device '${ID}' does not exist`; + } else if (!resolvedEntity.definition || !resolvedEntity.definition.ota) { + error = `Device '${resolvedEntity.name}' does not support OTA updates`; /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { const meta = {status: `not_supported`, device: resolvedEntity.name}; this.mqtt.publish( 'bridge/log', - JSON.stringify({type: `ota_update`, message, meta}), + JSON.stringify({type: `ota_update`, message: error, meta}), ); } + } else if (this.inProgress.has(resolvedEntity.device.ieeeAddr)) { + error = `Update or check for update already in progress for '${resolvedEntity.name}'`; + } else { + this.inProgress.add(resolvedEntity.device.ieeeAddr); - return; - } - - if (this.inProgress.has(resolvedEntity.device.ieeeAddr)) { - logger.error(`Update or check already in progress for '${resolvedEntity.name}', skipping...`); - return; - } - this.inProgress.add(resolvedEntity.device.ieeeAddr); - - const type = topic.substring(settings.get().mqtt.base_topic.length).split('/')[3]; - if (type === 'check') { - const message = `Checking if update available for '${resolvedEntity.name}'`; - logger.info(message); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `checking_if_available`, device: resolvedEntity.name}; - this.mqtt.publish( - 'bridge/log', - JSON.stringify({type: `ota_update`, message, meta}), - ); - } - - try { - const available = await resolvedEntity.definition.ota.isUpdateAvailable(resolvedEntity.device, logger); - const message = `${available ? 'Update' : 'No update'} available for '${resolvedEntity.name}'`; - logger.info(message); + if (type === 'check') { + const msg = `Checking if update available for '${resolvedEntity.name}'`; + logger.info(msg); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const meta = {status: available ? 'available' : 'not_available', device: resolvedEntity.name}; + const meta = {status: `checking_if_available`, device: resolvedEntity.name}; this.mqtt.publish( 'bridge/log', - JSON.stringify({type: `ota_update`, message, meta}), + JSON.stringify({type: `ota_update`, message: msg, meta}), ); } - this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available}); - this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now(); - } catch (error) { - const message = `Failed to check if update available for '${resolvedEntity.name}' (${error.message})`; - logger.error(message); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `check_failed`, device: resolvedEntity.name}; - this.mqtt.publish( - 'bridge/log', - JSON.stringify({type: `ota_update`, message, meta}), + try { + const available = await resolvedEntity.definition.ota.isUpdateAvailable( + resolvedEntity.device, logger, ); - } - } - } else { // type === 'update' - const message = `Updating '${resolvedEntity.name}' to latest firmware`; - logger.info(message); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_in_progress`, device: resolvedEntity.name}; - this.mqtt.publish( - 'bridge/log', - JSON.stringify({type: `ota_update`, message, meta}), - ); - } - - try { - const onProgress = (progress, remaining) => { - let message = `Update of '${resolvedEntity.name}' at ${progress.toFixed(2)}%`; - if (remaining) { - message += `, +- ${Math.round(remaining / 60)} minutes remaining`; - } - - logger.info(message); + const msg = `${available ? 'Update' : 'No update'} available for '${resolvedEntity.name}'`; + logger.info(msg); /* istanbul ignore else */ if (settings.get().advanced.legacy_api) { - const meta = {status: `update_progress`, device: resolvedEntity.name, progress}; + const meta = {status: available ? 'available' : 'not_available', device: resolvedEntity.name}; + this.mqtt.publish( + 'bridge/log', + JSON.stringify({type: `ota_update`, message: msg, meta}), + ); + } + + this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available}); + this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now(); + responseData.updateAvailable = available; + } catch (e) { + error = `Failed to check if update available for '${resolvedEntity.name}' (${e.message})`; + + /* istanbul ignore else */ + if (settings.get().advanced.legacy_api) { + const meta = {status: `check_failed`, device: resolvedEntity.name}; + this.mqtt.publish( + 'bridge/log', + JSON.stringify({type: `ota_update`, message: error, meta}), + ); + } + } + } else { // type === 'update' + const msg = `Updating '${resolvedEntity.name}' to latest firmware`; + logger.info(msg); + + /* istanbul ignore else */ + if (settings.get().advanced.legacy_api) { + const meta = {status: `update_in_progress`, device: resolvedEntity.name}; + this.mqtt.publish( + 'bridge/log', + JSON.stringify({type: `ota_update`, msg, meta}), + ); + } + + try { + const onProgress = (progress, remaining) => { + let msg = `Update of '${resolvedEntity.name}' at ${progress.toFixed(2)}%`; + if (remaining) { + msg += `, +- ${Math.round(remaining / 60)} minutes remaining`; + } + + logger.info(msg); + + /* istanbul ignore else */ + if (settings.get().advanced.legacy_api) { + const meta = {status: `update_progress`, device: resolvedEntity.name, progress}; + this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message: msg, meta})); + } + }; + + const from_ = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, false); + await resolvedEntity.definition.ota.updateToLatest(resolvedEntity.device, logger, onProgress); + const to = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, true); + const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)]; + const msg = `Finished update of '${resolvedEntity.name}'` + + (to ? `, from '${fromS}' to '${toS}'` : ``); + logger.info(msg); + this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false}); + responseData.from = from_; + responseData.to = to; + + /* istanbul ignore else */ + if (settings.get().advanced.legacy_api) { + const meta = {status: `update_succeeded`, device: resolvedEntity.name, from: from_, to}; this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta})); } - }; + } catch (e) { + error = `Update of '${resolvedEntity.name}' failed (${e.message})`; - const from_ = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, false); - await resolvedEntity.definition.ota.updateToLatest(resolvedEntity.device, logger, onProgress); - const to = await this.readSoftwareBuildIDAndDateCode(resolvedEntity.device, true); - const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)]; - const message = `Finished update of '${resolvedEntity.name}'` + - (to ? `, from '${fromS}' to '${toS}'` : ``); - logger.info(message); - this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false}); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_succeeded`, device: resolvedEntity.name, from: from_, to}; - this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta})); - } - } catch (error) { - const message = `Update of '${resolvedEntity.name}' failed (${error.message})`; - logger.error(message); - - /* istanbul ignore else */ - if (settings.get().advanced.legacy_api) { - const meta = {status: `update_failed`, device: resolvedEntity.name}; - this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta})); + /* istanbul ignore else */ + if (settings.get().advanced.legacy_api) { + const meta = {status: `update_failed`, device: resolvedEntity.name}; + this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message: error, meta})); + } } } + + this.inProgress.delete(resolvedEntity.device.ieeeAddr); } - this.inProgress.delete(resolvedEntity.device.ieeeAddr); + const triggeredViaLegacyApi = topic.match(legacyTopicRegex); + if (!triggeredViaLegacyApi) { + const response = utils.getResponse(message, responseData, error); + await this.mqtt.publish(`bridge/response/device/otaUpdate/${type}`, JSON.stringify(response)); + } + + if (error) { + logger.error(error); + } } } diff --git a/test/otaUpdate.test.js b/test/otaUpdate.test.js index f893d1a70..08101b77d 100644 --- a/test/otaUpdate.test.js +++ b/test/otaUpdate.test.js @@ -18,6 +18,7 @@ describe('OTA update', () => { beforeEach(async () => { data.writeDefaultConfiguration(); settings._reRead(); + settings.set(['experimental', 'new_api'], true); data.writeEmptyState(); controller = new Controller(); await controller.start(); @@ -28,6 +29,8 @@ describe('OTA update', () => { it('Should subscribe to topics', async () => { expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/check'); expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/ota_update/update'); + expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/otaUpdate/check'); + expect(MQTT.subscribe).toHaveBeenCalledWith('zigbee2mqtt/bridge/request/device/otaUpdate/update'); }); it('Should OTA update a device', async () => { @@ -41,14 +44,13 @@ describe('OTA update', () => { const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); logger.info.mockClear(); - logger.error.mockClear(); device.save.mockClear(); mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => { onUpdate(0, null); onUpdate(10, 3600); }); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/update', 'bulb'); await flushPromises(); expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); @@ -57,10 +59,14 @@ describe('OTA update', () => { expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, +- 60 minutes remaining`); expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}'`); - expect(logger.error).toHaveBeenCalledTimes(0); expect(device.save).toHaveBeenCalledTimes(1); expect(device.dateCode).toBe('20190102'); expect(device.softwareBuildID).toBe(2); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/update', + JSON.stringify({"data":{"ID": "bulb","from":{"softwareBuildID":1,"dateCode":"20190101"},"to":{"softwareBuildID":2,"dateCode":"20190102"}},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should handle when OTA update fails', async () => { @@ -69,17 +75,18 @@ describe('OTA update', () => { endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}}); const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); - logger.info.mockClear(); - logger.error.mockClear(); device.save.mockClear(); mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => { throw new Error('Update failed'); }); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/update', JSON.stringify({ID: "bulb"})); await flushPromises(); - expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/update', + JSON.stringify({"data":{"ID": "bulb"},"status":"error","error":"Update of 'bulb' failed (Update failed)"}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should be able to check if OTA update is available', async () => { @@ -87,41 +94,65 @@ describe('OTA update', () => { const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); - logger.info.mockClear(); mapped.ota.isUpdateAvailable.mockReturnValueOnce(false); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "bulb"); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "bulb","updateAvailable":false},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); - logger.info.mockClear(); + MQTT.publish.mockClear(); mapped.ota.isUpdateAvailable.mockReturnValueOnce(true); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "bulb"); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "bulb","updateAvailable":true},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should handle if OTA update check fails', async () => { const device = zigbeeHerdsman.devices.bulb; const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); - logger.error.mockClear(); mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')}); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "bulb"); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); - expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "bulb"},"status":"error","error": `Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`}), + {retain: false, qos: 0}, expect.any(Function) + ); + }); + + it('Should fail when device does not exist', async () => { + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "not_existing_deviceooo"); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "not_existing_deviceooo"},"status":"error","error": `Device 'not_existing_deviceooo' does not exist`}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should not check for OTA when device does not support it', async () => { - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'ZNLDP12LM'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "ZNLDP12LM"); await flushPromises(); - expect(logger.error).toHaveBeenCalledWith(`Device 'ZNLDP12LM' does not support OTA updates`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "ZNLDP12LM"},"status":"error","error": `Device 'ZNLDP12LM' does not support OTA updates`}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should refuse to check/update when already in progress', async () => { @@ -130,18 +161,21 @@ describe('OTA update', () => { const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); - logger.info.mockClear(); mapped.ota.isUpdateAvailable.mockImplementationOnce(() => { return new Promise((resolve, reject) => {setTimeout(() => resolve(), 99999)}) }); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "bulb"); await flushPromises(); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/check', "bulb"); await flushPromises(); expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(`Update or check already in progress for 'bulb', skipping...`); jest.runAllTimers(); await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/check', + JSON.stringify({"data":{"ID": "bulb"},"status":"error","error": `Update or check for update already in progress for 'bulb'`}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Shouldnt crash when read modelID after OTA update fails', async () => { @@ -156,10 +190,13 @@ describe('OTA update', () => { const mapped = zigbeeHerdsmanConverters.findByDevice(device) mockClear(mapped); - logger.info.mockClear(); - MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + MQTT.events.message('zigbee2mqtt/bridge/request/device/otaUpdate/update', "bulb"); await flushPromises(); - expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/response/device/otaUpdate/update', + JSON.stringify({"data":{"ID":"bulb","from":{"softwareBuildID":1,"dateCode":"2019010"},"to":null},"status":"ok"}), + {retain: false, qos: 0}, expect.any(Function) + ); }); it('Should check for update when device requests it', async () => { @@ -212,4 +249,116 @@ describe('OTA update', () => { expect(device.endpoints[0].commandResponse).toHaveBeenCalledTimes(0); expect(logger.error).toHaveBeenCalledTimes(0); }); + + it('Legacy api: Should OTA update a device', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.endpoints[0]; + let count = 0; + endpoint.read.mockImplementation(() => { + count++; + return {swBuildId: count, dateCode: '2019010' + count} + }); + const mapped = zigbeeHerdsmanConverters.findByDevice(device) + mockClear(mapped); + logger.info.mockClear(); + logger.error.mockClear(); + device.save.mockClear(); + mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => { + onUpdate(0, null); + onUpdate(10, 3600); + }); + + MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + await flushPromises(); + expect(logger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(0); + expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(1); + expect(mapped.ota.updateToLatest).toHaveBeenCalledWith(device, logger, expect.any(Function)); + expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`); + expect(logger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, +- 60 minutes remaining`); + expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb', from '{"softwareBuildID":1,"dateCode":"20190101"}' to '{"softwareBuildID":2,"dateCode":"20190102"}'`); + expect(logger.error).toHaveBeenCalledTimes(0); + expect(device.save).toHaveBeenCalledTimes(1); + expect(device.dateCode).toBe('20190102'); + expect(device.softwareBuildID).toBe(2); + }); + + it('Legacy api: Should handle when OTA update fails', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.endpoints[0]; + endpoint.read.mockImplementation(() => {return {swBuildId: 1, dateCode: '2019010'}}); + const mapped = zigbeeHerdsmanConverters.findByDevice(device) + mockClear(mapped); + logger.info.mockClear(); + logger.error.mockClear(); + device.save.mockClear(); + mapped.ota.updateToLatest.mockImplementationOnce((a, b, onUpdate) => { + throw new Error('Update failed'); + }); + + MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + await flushPromises(); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith(`Update of 'bulb' failed (Update failed)`); + }); + + it('Legacy api: Should be able to check if OTA update is available', async () => { + const device = zigbeeHerdsman.devices.bulb; + const mapped = zigbeeHerdsmanConverters.findByDevice(device) + mockClear(mapped); + + logger.info.mockClear(); + mapped.ota.isUpdateAvailable.mockReturnValueOnce(false); + MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + await flushPromises(); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); + expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); + expect(logger.info).toHaveBeenCalledWith(`No update available for 'bulb'`); + + logger.info.mockClear(); + mapped.ota.isUpdateAvailable.mockReturnValueOnce(true); + MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + await flushPromises(); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(2); + expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); + expect(logger.info).toHaveBeenCalledWith(`Update available for 'bulb'`); + }); + + it('Legacy api: Should handle if OTA update check fails', async () => { + const device = zigbeeHerdsman.devices.bulb; + const mapped = zigbeeHerdsmanConverters.findByDevice(device) + mockClear(mapped); + logger.error.mockClear(); + mapped.ota.isUpdateAvailable.mockImplementationOnce(() => {throw new Error('RF singals disturbed because of dogs barking')}); + + MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'bulb'); + await flushPromises(); + expect(mapped.ota.isUpdateAvailable).toHaveBeenCalledTimes(1); + expect(mapped.ota.updateToLatest).toHaveBeenCalledTimes(0); + expect(logger.error).toHaveBeenCalledWith(`Failed to check if update available for 'bulb' (RF singals disturbed because of dogs barking)`); + }); + + it('Legacy api: Should not check for OTA when device does not support it', async () => { + MQTT.events.message('zigbee2mqtt/bridge/ota_update/check', 'ZNLDP12LM'); + await flushPromises(); + expect(logger.error).toHaveBeenCalledWith(`Device 'ZNLDP12LM' does not support OTA updates`); + }); + + it('Legacy api: Shouldnt crash when read modelID after OTA update fails', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.endpoints[0]; + let count = 0; + endpoint.read.mockImplementation(() => { + if (count === 1) throw new Error('Failed!') + count++; + return {swBuildId: 1, dateCode: '2019010'} + }); + + const mapped = zigbeeHerdsmanConverters.findByDevice(device) + mockClear(mapped); + logger.info.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/ota_update/update', 'bulb'); + await flushPromises(); + expect(logger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`); + }); });