This commit is contained in:
Koen Kanters
2020-07-08 23:23:44 +02:00
parent f1c785eedd
commit 0a84412d75
2 changed files with 296 additions and 130 deletions
+121 -104
View File
@@ -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);
}
}
}
+175 -26
View File
@@ -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'`);
});
});