Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix!: Rework OTA #24634

Open
wants to merge 9 commits into
base: feat/2.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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