2020-02-08 11:55:27 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
|
|
|
const assert = require('assert');
|
2020-04-12 08:04:47 -07:00
|
|
|
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/ota_update/.+$`);
|
2020-04-11 09:10:56 -07:00
|
|
|
const Extension = require('./extension');
|
2020-02-16 08:00:15 -07:00
|
|
|
const MINUTES_10 = 1000 * 60 * 10;
|
2020-02-08 11:55:27 -07:00
|
|
|
|
2020-04-11 09:10:56 -07:00
|
|
|
class OTAUpdate extends Extension {
|
2020-02-09 12:44:37 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
|
|
|
this.inProgress = new Set();
|
2020-02-16 08:00:15 -07:00
|
|
|
this.lastChecked = {};
|
2020-04-12 08:04:47 -07:00
|
|
|
this.legacyApi = settings.get().advanced.legacy_api;
|
2020-02-09 12:44:37 -07:00
|
|
|
}
|
|
|
|
|
2020-02-08 11:55:27 -07:00
|
|
|
onMQTTConnected() {
|
2020-04-12 08:04:47 -07:00
|
|
|
/* istanbul ignore else */
|
|
|
|
if (this.legacyApi) {
|
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/check`);
|
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/ota_update/update`);
|
|
|
|
}
|
2020-02-08 11:55:27 -07:00
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
async onZigbeeEvent(type, data, resolvedEntity) {
|
|
|
|
if (data.type !== 'commandQueryNextImageRequest' || !resolvedEntity || !resolvedEntity.definition) return;
|
2020-02-28 15:30:33 -07:00
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
const supportsOTA = resolvedEntity.definition.hasOwnProperty('ota');
|
2020-02-28 15:42:59 -07:00
|
|
|
if (supportsOTA) {
|
2020-02-28 15:30:33 -07:00
|
|
|
// 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();
|
2020-04-05 06:41:24 -07:00
|
|
|
const available = await resolvedEntity.definition.ota.isUpdateAvailable(data.device, logger, data.data);
|
2020-02-28 15:30:33 -07:00
|
|
|
this.publishEntityState(data.device.ieeeAddr, {update_available: available});
|
|
|
|
|
|
|
|
if (available) {
|
2020-04-05 06:41:24 -07:00
|
|
|
const message = `Update available for '${resolvedEntity.settings.friendly_name}'`;
|
2020-02-28 15:30:33 -07:00
|
|
|
logger.info(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
|
|
|
const meta = {status: 'available', device: resolvedEntity.settings.friendly_name};
|
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
2020-02-28 15:30:33 -07:00
|
|
|
}
|
2020-02-20 12:01:26 -07:00
|
|
|
}
|
2020-02-28 15:42:59 -07:00
|
|
|
|
|
|
|
// Respond to the OTA request:
|
|
|
|
// - In case we don't support OTA: respond with NO_IMAGE_AVAILABLE (0x98) (so the client stops requesting OTAs)
|
|
|
|
// - In case we do support OTA: respond with ABORT (0x95) as we don't want to update now.
|
2020-02-29 04:43:53 -07:00
|
|
|
const endpoint = data.device.endpoints.find((e) => e.supportsOutputCluster('genOta'));
|
2020-04-28 12:23:32 -07:00
|
|
|
if (endpoint) {
|
|
|
|
// Some devices send OTA requests without defining OTA cluster as input cluster.
|
|
|
|
await endpoint.commandResponse('genOta', 'queryNextImageResponse', {status: supportsOTA ? 0x95 : 0x98});
|
|
|
|
}
|
2020-02-16 08:00:15 -07:00
|
|
|
}
|
|
|
|
|
2020-02-09 12:44:37 -07:00
|
|
|
async readSoftwareBuildIDAndDateCode(device, update) {
|
2020-02-13 13:10:44 -07:00
|
|
|
try {
|
|
|
|
const endpoint = device.endpoints.find((e) => e.supportsInputCluster('genBasic'));
|
|
|
|
const result = await endpoint.read('genBasic', ['dateCode', 'swBuildId']);
|
2020-02-09 12:44:37 -07:00
|
|
|
|
2020-02-13 13:10:44 -07:00
|
|
|
if (update) {
|
|
|
|
device.softwareBuildID = result.swBuildId;
|
|
|
|
device.dateCode = result.dateCode;
|
|
|
|
device.save();
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
|
2020-02-13 13:10:44 -07:00
|
|
|
return {softwareBuildID: result.swBuildId, dateCode: result.dateCode};
|
|
|
|
} catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
}
|
|
|
|
|
2020-02-08 11:55:27 -07:00
|
|
|
async onMQTTMessage(topic, message) {
|
2020-04-12 08:04:47 -07:00
|
|
|
let resolvedEntity = null;
|
2020-04-12 11:32:14 -07:00
|
|
|
/* istanbul ignore else */
|
2020-04-12 08:04:47 -07:00
|
|
|
if (this.legacyApi) {
|
|
|
|
if (!topic.match(legacyTopicRegex)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
resolvedEntity = this.zigbee.resolveEntity(message);
|
|
|
|
} else {
|
2020-02-08 11:55:27 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-04-12 08:04:47 -07:00
|
|
|
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`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.error(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `not_supported`, device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-02-08 11:55:27 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-12 08:04:47 -07:00
|
|
|
if (this.inProgress.has(resolvedEntity.device.ieeeAddr)) {
|
|
|
|
logger.error(`Update or check already in progress for '${resolvedEntity.name}', skipping...`);
|
2020-02-09 12:44:37 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-04-12 08:04:47 -07:00
|
|
|
this.inProgress.add(resolvedEntity.device.ieeeAddr);
|
2020-02-08 11:55:27 -07:00
|
|
|
|
2020-02-27 12:33:04 -07:00
|
|
|
const type = topic.substring(settings.get().mqtt.base_topic.length).split('/')[3];
|
2020-02-09 12:44:37 -07:00
|
|
|
if (type === 'check') {
|
2020-04-12 08:04:47 -07:00
|
|
|
const message = `Checking if update available for '${resolvedEntity.name}'`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.info(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `checking_if_available`, device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:44:37 -07:00
|
|
|
try {
|
2020-04-12 08:04:47 -07:00
|
|
|
const available = await resolvedEntity.definition.ota.isUpdateAvailable(resolvedEntity.device, logger);
|
|
|
|
const message = `${available ? 'Update' : 'No update'} available for '${resolvedEntity.name}'`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.info(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: available ? 'available' : 'not_available', device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-12 08:04:47 -07:00
|
|
|
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: available});
|
|
|
|
this.lastChecked[resolvedEntity.device.ieeeAddr] = Date.now();
|
2020-02-09 12:44:37 -07:00
|
|
|
} catch (error) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const message = `Failed to check if update available for '${resolvedEntity.name}' (${error.message})`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.error(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `check_failed`, device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
}
|
|
|
|
} else { // type === 'update'
|
2020-04-12 08:04:47 -07:00
|
|
|
const message = `Updating '${resolvedEntity.name}' to latest firmware`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.info(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `update_in_progress`, device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `ota_update`, message, meta}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:44:37 -07:00
|
|
|
try {
|
|
|
|
const onProgress = (progress, remaining) => {
|
2020-04-24 13:38:51 -07:00
|
|
|
let message = `Update of '${resolvedEntity.name}' at ${progress.toFixed(2)}%`;
|
2020-02-09 12:44:37 -07:00
|
|
|
if (remaining) {
|
|
|
|
message += `, +- ${Math.round(remaining / 60)} minutes remaining`;
|
|
|
|
}
|
2020-02-08 11:55:27 -07:00
|
|
|
|
2020-02-09 12:44:37 -07:00
|
|
|
logger.info(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `update_progress`, device: resolvedEntity.name, progress};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
};
|
2020-02-08 11:55:27 -07:00
|
|
|
|
2020-04-12 08:04:47 -07:00
|
|
|
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);
|
2020-02-09 12:44:37 -07:00
|
|
|
const [fromS, toS] = [JSON.stringify(from_), JSON.stringify(to)];
|
2020-04-12 08:04:47 -07:00
|
|
|
const message = `Finished update of '${resolvedEntity.name}'` +
|
|
|
|
(to ? `, from '${fromS}' to '${toS}'` : ``);
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.info(message);
|
2020-04-12 08:04:47 -07:00
|
|
|
this.publishEntityState(resolvedEntity.device.ieeeAddr, {update_available: false});
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `update_succeeded`, device: resolvedEntity.name, from: from_, to};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
} catch (error) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const message = `Update of '${resolvedEntity.name}' failed (${error.message})`;
|
2020-02-14 15:41:34 -07:00
|
|
|
logger.error(message);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 08:04:47 -07:00
|
|
|
const meta = {status: `update_failed`, device: resolvedEntity.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish('bridge/log', JSON.stringify({type: `ota_update`, message, meta}));
|
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
}
|
2020-02-08 11:55:27 -07:00
|
|
|
}
|
2020-02-09 12:44:37 -07:00
|
|
|
|
2020-04-12 08:04:47 -07:00
|
|
|
this.inProgress.delete(resolvedEntity.device.ieeeAddr);
|
2020-02-08 11:55:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = OTAUpdate;
|