Skip to content

Commit

Permalink
fix!: Rework OTA (#24634)
Browse files Browse the repository at this point in the history
* fix(ignore): Update zh and zhc

* update

* Update

* fix

* fix!: Rework OTA

* Import only required from zhc.

* Remove uri-js

* Update settings.schema.json

* fix save

---------

Co-authored-by: Koen Kanters <[email protected]>
  • Loading branch information
Nerivec and Koenkk authored Nov 19, 2024
1 parent 629036c commit 8c925a3
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 357 deletions.
14 changes: 5 additions & 9 deletions lib/extension/homeassistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
humidity_max: {entity_category: 'config', icon: 'mdi:water-percent'},
humidity_min: {entity_category: 'config', icon: 'mdi:water-percent'},
illuminance_calibration: {entity_category: 'config', icon: 'mdi:wrench-clock'},
illuminance_lux: {device_class: 'illuminance', state_class: 'measurement'},
illuminance: {device_class: 'illuminance', enabled_by_default: false, state_class: 'measurement'},
illuminance: {device_class: 'illuminance', state_class: 'measurement'},
linkquality: {
enabled_by_default: false,
entity_category: 'diagnostic',
Expand Down Expand Up @@ -1292,13 +1291,10 @@ export default class HomeAssistant extends Extension {
* Whenever a device publish an {action: *} we discover an MQTT device trigger sensor
* and republish it to zigbee2mqtt/my_device/action
*/
if (entity.isDevice() && entity.definition) {
const keys = ['action', 'click'].filter((k) => data.message[k]);
for (const key of keys) {
const value = data.message[key].toString();
await this.publishDeviceTriggerDiscover(entity, key, value);
await this.mqtt.publish(`${data.entity.name}/${key}`, value, {});
}
if (entity.isDevice() && entity.definition && 'action' in data.message) {
const value = data.message['action'].toString();
await this.publishDeviceTriggerDiscover(entity, 'action', value);
await this.mqtt.publish(`${data.entity.name}/action`, value, {});
}
}

Expand Down
69 changes: 31 additions & 38 deletions lib/extension/otaUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type {Ota} from 'zigbee-herdsman-converters';

import assert from 'assert';
import path from 'path';

import bind from 'bind-decorator';
import stringify from 'json-stable-stringify-without-jsonify';
import * as URI from 'uri-js';

import {Zcl} from 'zigbee-herdsman';
import * as zhc from 'zigbee-herdsman-converters';
import {ota} from 'zigbee-herdsman-converters';

import Device from '../model/device';
import dataDir from '../util/data';
Expand All @@ -15,17 +16,6 @@ import * as settings from '../util/settings';
import utils from '../util/utils';
import Extension from './extension';

function isValidUrl(url: string): boolean {
let parsed;
try {
parsed = URI.parse(url);
} catch {
// istanbul ignore next
return false;
}
return parsed.scheme === 'http' || parsed.scheme === 'https';
}

type UpdateState = 'updating' | 'idle' | 'available';
interface UpdatePayload {
update: {
Expand All @@ -37,7 +27,7 @@ interface UpdatePayload {
};
}

const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)`, 'i');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check)/?(downgrade)?`, 'i');

export default class OTAUpdate extends Extension {
private inProgress = new Set();
Expand All @@ -46,23 +36,24 @@ export default class OTAUpdate extends Extension {
override async start(): Promise<void> {
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
this.eventBus.onDeviceMessage(this, this.onZigbeeEvent);
if (settings.get().ota.ikea_ota_use_test_url) {
zhc.ota.tradfri.useTestURL();
}

// Let zigbeeOTA module know if the override index file is provided
let overrideOTAIndex = settings.get().ota.zigbee_ota_override_index_location;
if (overrideOTAIndex) {
// If the file name is not a full path, then treat it as a relative to the data directory
if (!isValidUrl(overrideOTAIndex) && !path.isAbsolute(overrideOTAIndex)) {
overrideOTAIndex = dataDir.joinPath(overrideOTAIndex);
}
const otaSettings = settings.get().ota;
// Let OTA module know if the override index file is provided
let overrideIndexLocation = otaSettings.zigbee_ota_override_index_location;

zhc.ota.zigbeeOTA.useIndexOverride(overrideOTAIndex);
// If the file name is not a full path, then treat it as a relative to the data directory
if (overrideIndexLocation && !ota.isValidUrl(overrideIndexLocation) && !path.isAbsolute(overrideIndexLocation)) {
overrideIndexLocation = dataDir.joinPath(overrideIndexLocation);
}

// In order to support local firmware files we need to let zigbeeOTA know where the data directory is
zhc.ota.setDataDir(dataDir.getPath());
ota.setConfiguration({
dataDir: dataDir.getPath(),
overrideIndexLocation,
// TODO: implement me
imageBlockResponseDelay: otaSettings.image_block_response_delay,
defaultMaximumDataSize: otaSettings.default_maximum_data_size,
});

// In case Zigbee2MQTT is restared during an update, progress and remaining values are still in state, remove them.
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
Expand Down Expand Up @@ -102,10 +93,11 @@ export default class OTAUpdate extends Extension {
if (!check) return;

this.lastChecked[data.device.ieeeAddr] = Date.now();
let availableResult: zhc.OtaUpdateAvailableResult | undefined;
let availableResult: Ota.UpdateAvailableResult | undefined;

try {
availableResult = await data.device.definition.ota.isUpdateAvailable(data.device.zh, data.data as zhc.ota.ImageInfo);
// never use 'previous' when responding to device request
availableResult = await ota.isUpdateAvailable(data.device.zh, data.device.otaExtraMetas, data.data as Ota.ImageInfo, false);
} catch (error) {
logger.debug(`Failed to check if update available for '${data.device.name}' (${error})`);
}
Expand Down Expand Up @@ -146,7 +138,7 @@ export default class OTAUpdate extends Extension {

private getEntityPublishPayload(
device: Device,
state: zhc.OtaUpdateAvailableResult | UpdateState,
state: Ota.UpdateAvailableResult | UpdateState,
progress?: number,
remaining?: number,
): UpdatePayload {
Expand All @@ -171,14 +163,17 @@ export default class OTAUpdate extends Extension {
}

@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
if (!data.topic.match(topicRegex)) {
const topicMatch = data.topic.match(topicRegex);

if (!topicMatch) {
return;
}

const message = utils.parseJSON(data.message, data.message);
const ID = (typeof message === 'object' && message['id'] !== undefined ? message.id : message) as string;
const device = this.zigbee.resolveEntity(ID);
const type = data.topic.substring(data.topic.lastIndexOf('/') + 1);
const type = topicMatch[1];
const downgrade = Boolean(topicMatch[2]);
const responseData: {id: string; update_available?: boolean; from?: KeyValue | null; to?: KeyValue | null} = {id: ID};
let error: string | undefined;
let errorStack: string | undefined;
Expand All @@ -197,7 +192,7 @@ export default class OTAUpdate extends Extension {
logger.info(msg);

try {
const availableResult = await device.definition.ota.isUpdateAvailable(device.zh, undefined);
const availableResult = await ota.isUpdateAvailable(device.zh, device.otaExtraMetas, undefined, downgrade);
const msg = `${availableResult.available ? 'Update' : 'No update'} available for '${device.name}'`;
logger.info(msg);

Expand All @@ -210,11 +205,12 @@ export default class OTAUpdate extends Extension {
}
} else {
// type === 'update'
const msg = `Updating '${device.name}' to latest firmware`;
const msg = `Updating '${device.name}' to ${downgrade ? 'previous' : 'latest'} firmware`;
logger.info(msg);

try {
const onProgress = async (progress: number, remaining: number | null): Promise<void> => {
const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate');
const fileVersion = await ota.update(device.zh, device.otaExtraMetas, downgrade, async (progress, remaining) => {
let msg = `Update of '${device.name}' at ${progress.toFixed(2)}%`;
if (remaining) {
msg += `, ≈ ${Math.round(remaining / 60)} minutes remaining`;
Expand All @@ -223,10 +219,7 @@ export default class OTAUpdate extends Extension {
logger.info(msg);

await this.publishEntityState(device, this.getEntityPublishPayload(device, 'updating', progress, remaining ?? undefined));
};

const from_ = await this.readSoftwareBuildIDAndDateCode(device, 'immediate');
const fileVersion = await device.definition.ota.updateToLatest(device.zh, onProgress);
});
logger.info(`Finished update of '${device.name}'`);
this.removeProgressAndRemainingFromState(device);
await this.publishEntityState(
Expand Down
3 changes: 3 additions & 0 deletions lib/model/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default class Device {
get customClusters(): CustomClusters {
return this.zh.customClusters;
}
get otaExtraMetas(): zhc.Ota.ExtraMetas {
return typeof this.definition?.ota === 'object' ? this.definition.ota : {};
}

constructor(device: zh.Device) {
this.zh = device;
Expand Down
1 change: 0 additions & 1 deletion lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const dontCacheProperties = [
'button',
'button_left',
'button_right',
'click',
'forgotten',
'keyerror',
'step_size',
Expand Down
3 changes: 2 additions & 1 deletion lib/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ declare global {
update_check_interval: number;
disable_automatic_update_check: boolean;
zigbee_ota_override_index_location?: string;
ikea_ota_use_test_url?: boolean;
image_block_response_delay?: number;
default_maximum_data_size?: number;
};
frontend?: {
auth_token?: string;
Expand Down
31 changes: 17 additions & 14 deletions lib/util/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,19 +325,29 @@
"description": "Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.",
"default": false
},
"ikea_ota_use_test_url": {
"type": "boolean",
"title": "IKEA TRADFRI OTA use test url",
"requiresRestart": true,
"description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation",
"default": false
},
"zigbee_ota_override_index_location": {
"type": ["string", "null"],
"title": "OTA index override file name",
"requiresRestart": true,
"description": "Location of override OTA index file",
"examples": ["index.json"]
},
"image_block_response_delay": {
"type": "number",
"title": "Image block response delay",
"description": "Limits the rate of requests (in milliseconds) during OTA updates to reduce network congestion. You can increase this value if your network appears unstable during OTA.",
"default": 250,
"minimum": 50,
"requiresRestart": true
},
"default_maximum_data_size": {
"type": "number",
"title": "Default maximum data size",
"description": "The size of file chunks sent during an update (in bytes). Note: This value may get ignored for manufacturers that require specific values.",
"default": 50,
"minimum": 10,
"maximum": 100,
"requiresRestart": true
}
}
},
Expand Down Expand Up @@ -733,13 +743,6 @@
"title": "RTS / CTS (deprecated)",
"requiresRestart": true,
"description": "RTS / CTS Hardware Flow Control for serial port"
},
"ikea_ota_use_test_url": {
"type": "boolean",
"title": "IKEA TRADFRI OTA use test url (deprecated)",
"requiresRestart": true,
"description": "Use IKEA TRADFRI OTA test server, see OTA updates documentation",
"default": false
}
}
},
Expand Down
9 changes: 2 additions & 7 deletions lib/util/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ objectAssignDeep(schema, schemaJson);
delete schema.properties.advanced.properties.homeassistant_status_topic;
delete schema.properties.advanced.properties.baudrate;
delete schema.properties.advanced.properties.rtscts;
delete schema.properties.advanced.properties.ikea_ota_use_test_url;
delete schema.properties.experimental;
delete (schemaJson as KeyValue).properties.whitelist;
delete (schemaJson as KeyValue).properties.ban;
Expand Down Expand Up @@ -75,6 +74,8 @@ const defaults: RecursivePartial<Settings> = {
ota: {
update_check_interval: 24 * 60,
disable_automatic_update_check: false,
image_block_response_delay: 250,
default_maximum_data_size: 50,
},
device_options: {},
advanced: {
Expand Down Expand Up @@ -175,12 +176,6 @@ function loadSettingsWithDefaults(): void {
_settingsWithDefaults.serial.rtscts = _settings.advanced.rtscts;
}

// @ts-expect-error ignore typing
if (_settings.advanced?.ikea_ota_use_test_url !== undefined && _settings.ota?.ikea_ota_use_test_url == null) {
// @ts-expect-error ignore typing
_settingsWithDefaults.ota.ikea_ota_use_test_url = _settings.advanced.ikea_ota_use_test_url;
}

// @ts-expect-error ignore typing
if (_settings.experimental?.transmit_power !== undefined && _settings.advanced?.transmit_power == null) {
// @ts-expect-error ignore typing
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@
"semver": "^7.6.3",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"uri-js": "^4.4.1",
"winston": "^3.16.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.8.0",
"ws": "^8.18.0",
"zigbee-herdsman": "3.0.0-pre.0",
"zigbee-herdsman-converters": "21.0.0-pre.0",
"zigbee-herdsman": "3.0.0-pre.1",
"zigbee-herdsman-converters": "21.0.0-pre.1",
"zigbee2mqtt-frontend": "0.7.4"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 8c925a3

Please sign in to comment.