fix!: Rework OTA (#24634)

* 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 <koenkanters94@gmail.com>
This commit is contained in:
Nerivec
2024-12-01 20:11:33 +01:00
committed by Koen Kanters
co-authored by Koen Kanters
parent 95279b02af
commit 563514c446
14 changed files with 226 additions and 349 deletions
+31 -38
View File
@@ -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';
@@ -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: {
@@ -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();
@@ -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)) {
@@ -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})`);
}
@@ -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 {
@@ -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;
@@ -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);
@@ -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`;
@@ -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(
+3
View File
@@ -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;
-1
View File
@@ -15,7 +15,6 @@ const dontCacheProperties = [
'button',
'button_left',
'button_right',
'click',
'forgotten',
'keyerror',
'step_size',
+2 -1
View File
@@ -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;
+17 -14
View File
@@ -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
}
}
},
@@ -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
}
}
},
+2 -7
View File
@@ -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;
@@ -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: {
@@ -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
+3 -4
View File
@@ -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.17.0",
"winston": "^3.16.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.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": {
+12 -159
View File
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
zigbee-herdsman: 3.0.0-pre.0
zigbee-herdsman: 3.0.0-pre.1
importers:
@@ -68,9 +68,6 @@ importers:
throttleit:
specifier: ^2.1.0
version: 2.1.0
uri-js:
specifier: ^4.4.1
version: 4.4.1
winston:
specifier: ^3.17.0
version: 3.17.0
@@ -84,11 +81,11 @@ importers:
specifier: ^8.18.0
version: 8.18.0
zigbee-herdsman:
specifier: 3.0.0-pre.0
version: 3.0.0-pre.0
specifier: 3.0.0-pre.1
version: 3.0.0-pre.1
zigbee-herdsman-converters:
specifier: 21.0.0-pre.0
version: 21.0.0-pre.0
specifier: 21.0.0-pre.1
version: 21.0.0-pre.1
zigbee2mqtt-frontend:
specifier: 0.7.4
version: 0.7.4
@@ -1225,10 +1222,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.1:
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
engines: {node: '>= 14'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1276,15 +1269,6 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
b4a@1.6.7:
resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==}
babel-jest@29.7.0:
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -1328,9 +1312,6 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bare-events@2.5.0:
resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -1441,10 +1422,6 @@ packages:
colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commist@3.2.0:
resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==}
@@ -1522,10 +1499,6 @@ packages:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -1681,9 +1654,6 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -1743,23 +1713,10 @@ packages:
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
engines: {node: '>= 6'}
fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@@ -1851,10 +1808,6 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
https-proxy-agent@7.0.5:
resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==}
engines: {node: '>= 14'}
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -2205,14 +2158,6 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
@@ -2420,9 +2365,6 @@ packages:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2433,9 +2375,6 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@@ -2621,9 +2560,6 @@ packages:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
streamx@2.20.1:
resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==}
string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'}
@@ -2678,16 +2614,10 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
text-decoder@1.2.0:
resolution: {integrity: sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==}
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
@@ -2884,11 +2814,11 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zigbee-herdsman-converters@21.0.0-pre.0:
resolution: {integrity: sha512-HtmccVQFI2+XzgZZEBosW7TgxAJPtXRXxwiW8JVnpRMExjkv4LVBgzVPohtj3xjgPHpfEUfXawRcESrylEgE9Q==}
zigbee-herdsman-converters@21.0.0-pre.1:
resolution: {integrity: sha512-ALNeNua3OiQtSGkBn8W2G2XfWrytZjV5VbzwyeIJPYzEpMqMIvBhbXMyF/l6AP88b68dYFVKqjJMuTxXbvCETQ==}
zigbee-herdsman@3.0.0-pre.0:
resolution: {integrity: sha512-3mCSmdwu5eJb0DEJrTDSQPYEQAz1mOGAbqvXyEOjo6UOllo++GyzpmawTxWBH4FLWrbuKc9SefiVqQdC/4uZbQ==}
zigbee-herdsman@3.0.0-pre.1:
resolution: {integrity: sha512-HRonSIS39LwWo0EBA2YRE/E9o6g0tY8RT8TkjlxH6R4uwJwPrE2mIzhLMewqU3xWbmpS6YamsZ7jw/GY9U3byw==}
zigbee2mqtt-frontend@0.7.4:
resolution: {integrity: sha512-skWNYxThSa6Ywn7aRB0ZvRKWifpqbku4+vUM5BbXiNaXYxCCbU0b3pN258Ahxt3NsLtYk2zBdYoQcXuBZxmJxw==}
@@ -4251,12 +4181,6 @@ snapshots:
acorn@8.14.0: {}
agent-base@7.1.1:
dependencies:
debug: 4.3.7
transitivePeerDependencies:
- supports-color
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4304,18 +4228,6 @@ snapshots:
async@3.2.6: {}
asynckit@0.4.0: {}
axios@1.7.7:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
b4a@1.6.7: {}
babel-jest@29.7.0(@babel/core@7.26.0):
dependencies:
'@babel/core': 7.26.0
@@ -4397,9 +4309,6 @@ snapshots:
balanced-match@1.0.2: {}
bare-events@2.5.0:
optional: true
base64-js@1.5.1: {}
bind-decorator@1.0.11: {}
@@ -4516,10 +4425,6 @@ snapshots:
color: 3.2.1
text-hex: 1.0.0
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commist@3.2.0: {}
concat-map@0.0.1: {}
@@ -4586,8 +4491,6 @@ snapshots:
deepmerge@4.3.1: {}
delayed-stream@1.0.0: {}
depd@2.0.0: {}
destroy@1.2.0: {}
@@ -4741,8 +4644,6 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
fast-glob@3.3.2:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -4814,19 +4715,11 @@ snapshots:
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.3
signal-exit: 4.1.0
form-data@4.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
fresh@0.5.2: {}
fs.realpath@1.0.0: {}
@@ -4902,13 +4795,6 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.1
debug: 4.3.7
transitivePeerDependencies:
- supports-color
human-signals@2.1.0: {}
humanize-duration@3.32.1: {}
@@ -5422,12 +5308,6 @@ snapshots:
braces: 3.0.3
picomatch: 2.3.1
mime-db@1.52.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime@1.6.0: {}
mimic-fn@2.1.0: {}
@@ -5626,16 +5506,12 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
proxy-from-env@1.1.0: {}
punycode@2.3.1: {}
pure-rand@6.1.0: {}
queue-microtask@1.2.3: {}
queue-tick@1.0.1: {}
range-parser@1.2.1: {}
react-is@18.3.1: {}
@@ -5818,14 +5694,6 @@ snapshots:
statuses@2.0.1: {}
streamx@2.20.1:
dependencies:
fast-fifo: 1.3.2
queue-tick: 1.0.1
text-decoder: 1.2.0
optionalDependencies:
bare-events: 2.5.0
string-length@4.0.2:
dependencies:
char-regex: 1.0.2
@@ -5879,22 +5747,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
tar-stream@3.1.7:
dependencies:
b4a: 1.6.7
fast-fifo: 1.3.2
streamx: 2.20.1
test-exclude@6.0.0:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 7.2.3
minimatch: 3.1.2
text-decoder@1.2.0:
dependencies:
b4a: 1.6.7
text-hex@1.0.0: {}
throttleit@2.1.0: {}
@@ -6080,21 +5938,16 @@ snapshots:
yocto-queue@0.1.0: {}
zigbee-herdsman-converters@21.0.0-pre.0:
zigbee-herdsman-converters@21.0.0-pre.1:
dependencies:
axios: 1.7.7
buffer-crc32: 1.0.0
https-proxy-agent: 7.0.5
iconv-lite: 0.6.3
semver: 7.6.3
tar-stream: 3.1.7
uri-js: 4.4.1
zigbee-herdsman: 3.0.0-pre.0
zigbee-herdsman: 3.0.0-pre.1
transitivePeerDependencies:
- debug
- supports-color
zigbee-herdsman@3.0.0-pre.0:
zigbee-herdsman@3.0.0-pre.1:
dependencies:
'@serialport/bindings-cpp': 12.0.1
'@serialport/parser-delimiter': 12.0.0
+9 -26
View File
@@ -203,7 +203,12 @@ describe('Extension: Bridge', () => {
},
},
mqtt: {base_topic: 'zigbee2mqtt', force_disable_retain: false, include_device_information: false, server: 'mqtt://localhost'},
ota: {disable_automatic_update_check: false, update_check_interval: 1440},
ota: {
disable_automatic_update_check: false,
update_check_interval: 1440,
image_block_response_delay: 250,
default_maximum_data_size: 50,
},
passlist: [],
serial: {disable_led: false, port: '/dev/dummy'},
},
@@ -757,18 +762,7 @@ describe('Extension: Bridge', () => {
{
access: 2,
description:
'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).',
label: 'Legacy',
name: 'legacy',
property: 'legacy',
type: 'binary',
value_off: false,
value_on: true,
},
{
access: 2,
description:
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. Only works when legacy is false.',
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.',
features: [
{
access: 2,
@@ -869,7 +863,7 @@ describe('Extension: Bridge', () => {
{
access: 2,
description:
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ',
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.',
features: [
{
access: 2,
@@ -1000,17 +994,6 @@ describe('Extension: Bridge', () => {
property: 'device_temperature_calibration',
type: 'numeric',
},
{
access: 2,
description:
'Set to false to disable the legacy integration (highly recommended), will change structure of the published payload (default true).',
label: 'Legacy',
name: 'legacy',
property: 'legacy',
type: 'binary',
value_off: false,
value_on: true,
},
],
supports_ota: false,
vendor: 'Aqara',
@@ -2533,7 +2516,7 @@ describe('Extension: Bridge', () => {
{
access: 2,
description:
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval. ',
'Simulate a brightness value. If this device provides a brightness_move_up or brightness_move_down action it is possible to specify the update interval and delta. The action_brightness_delta indicates the delta for each interval.',
features: [
{
access: 2,
-35
View File
@@ -1720,38 +1720,12 @@ describe('Extension: HomeAssistant', () => {
expect.any(Function),
);
const discoverPayloadClick = {
automation_type: 'trigger',
type: 'click',
subtype: 'single',
payload: 'single',
topic: 'zigbee2mqtt/button/click',
origin: origin,
device: {
identifiers: ['zigbee2mqtt_0x0017880104e45520'],
name: 'button',
model: 'Wireless mini switch (WXKG11LM)',
manufacturer: 'Aqara',
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
},
};
expect(mockMQTT.publish).toHaveBeenCalledWith(
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
stringify(discoverPayloadClick),
{retain: true, qos: 1},
expect.any(Function),
);
expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function));
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
stringify({
action: 'single',
click: 'single',
battery: null,
linkquality: null,
voltage: null,
@@ -1773,17 +1747,8 @@ describe('Extension: HomeAssistant', () => {
expect.any(Function),
);
expect(mockMQTT.publish).not.toHaveBeenCalledWith(
'homeassistant/device_automation/0x0017880104e45520/click_single/config',
stringify(discoverPayloadClick),
{retain: true, qos: 1},
expect.any(Function),
);
expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/action', 'single', {retain: false, qos: 0}, expect.any(Function));
expect(mockMQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/button/click', 'single', {retain: false, qos: 0}, expect.any(Function));
// Shouldn't rediscover when already discovered in previous session
clearDiscoveredTrigger('0x0017880104e45520');
await mockMQTTEvents.message(
+112 -49
View File
@@ -11,18 +11,22 @@ import stringify from 'json-stable-stringify-without-jsonify';
import OTAUpdate from 'lib/extension/otaUpdate';
import * as zhc from 'zigbee-herdsman-converters';
import {zigbeeOTA} from 'zigbee-herdsman-converters/lib/ota';
import {Controller} from '../../lib/controller';
import * as settings from '../../lib/util/settings';
const mocksClear = [mockMQTT.publish, devices.bulb.save, mockLogger.info];
const mocksClear = [mockMQTT.publish, mockLogger.info];
const DEFAULT_CONFIG: zhc.Ota.Settings = {
dataDir: data.mockDir,
imageBlockResponseDelay: 250,
defaultMaximumDataSize: 50,
};
describe('Extension: OTAUpdate', () => {
let controller: Controller;
let mapped: zhc.Definition;
let updateToLatestSpy: jest.SpyInstance;
let isUpdateAvailableSpy: jest.SpyInstance;
const updateSpy = jest.spyOn(zhc.ota, 'update');
const isUpdateAvailableSpy = jest.spyOn(zhc.ota, 'isUpdateAvailable');
const resetExtension = async (): Promise<void> => {
await controller.enableDisableExtension(false, 'OTAUpdate');
@@ -34,14 +38,9 @@ describe('Extension: OTAUpdate', () => {
mockSleep.mock();
data.writeDefaultConfiguration();
settings.reRead();
settings.set(['ota', 'ikea_ota_use_test_url'], true);
settings.reRead();
controller = new Controller(jest.fn(), jest.fn());
await controller.start();
// @ts-expect-error minimal mock
mapped = await zhc.findByDevice(devices.bulb);
updateToLatestSpy = jest.spyOn(mapped.ota!, 'updateToLatest');
isUpdateAvailableSpy = jest.spyOn(mapped.ota!, 'isUpdateAvailable');
await flushPromises();
});
@@ -51,6 +50,7 @@ describe('Extension: OTAUpdate', () => {
});
beforeEach(async () => {
zhc.ota.setConfiguration(DEFAULT_CONFIG);
// @ts-expect-error private
const extension: OTAUpdate = controller.extensions.find((e) => e.constructor.name === 'OTAUpdate');
// @ts-expect-error private
@@ -58,9 +58,8 @@ describe('Extension: OTAUpdate', () => {
// @ts-expect-error private
extension.inProgress = new Set();
mocksClear.forEach((m) => m.mockClear());
devices.bulb.save.mockClear();
devices.bulb.endpoints[0].commandResponse.mockClear();
updateToLatestSpy.mockClear();
devices.bulb.mockClear();
updateSpy.mockClear();
isUpdateAvailableSpy.mockClear();
// @ts-expect-error private
controller.state.state = {};
@@ -70,29 +69,41 @@ describe('Extension: OTAUpdate', () => {
settings.set(['ota', 'disable_automatic_update_check'], false);
});
it('Should OTA update a device', async () => {
let count = 0;
it.each(['update', 'update/downgrade'])('Should OTA update a device with topic %s', async (type) => {
const downgrade = type === 'update/downgrade';
let count = 10;
devices.bulb.endpoints[0].read.mockImplementation(() => {
count++;
return {swBuildId: count, dateCode: '2019010' + count};
if (downgrade) {
count--;
} else {
count++;
}
return {swBuildId: count, dateCode: `201901${count}`};
});
updateToLatestSpy.mockImplementationOnce((device, onProgress) => {
onProgress(0, null);
updateSpy.mockImplementationOnce(async (device, extraMetas, previous, onProgress) => {
expect(previous).toStrictEqual(downgrade);
onProgress(0, undefined);
onProgress(10, 3600.2123);
return 90;
});
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${type}`, 'bulb');
await flushPromises();
expect(mockLogger.info).toHaveBeenCalledWith(`Updating 'bulb' to latest firmware`);
const fromSwBuildId = 10 + (downgrade ? -1 : +1);
const toSwBuildId = 10 + (downgrade ? -2 : +2);
const fromDateCode = `201901${fromSwBuildId}`;
const toDateCode = `201901${toSwBuildId}`;
expect(mockLogger.info).toHaveBeenCalledWith(`Updating 'bulb' to ${downgrade ? 'previous' : 'latest'} firmware`);
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(0);
expect(updateToLatestSpy).toHaveBeenCalledTimes(1);
expect(updateToLatestSpy).toHaveBeenCalledWith(devices.bulb, expect.any(Function));
expect(updateSpy).toHaveBeenCalledTimes(1);
expect(updateSpy).toHaveBeenCalledWith(devices.bulb, {}, downgrade, expect.any(Function));
expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 0.00%`);
expect(mockLogger.info).toHaveBeenCalledWith(`Update of 'bulb' at 10.00%, ≈ 60 minutes remaining`);
expect(mockLogger.info).toHaveBeenCalledWith(`Finished update of 'bulb'`);
expect(mockLogger.info).toHaveBeenCalledWith(
`Device 'bulb' was updated from '{"dateCode":"20190101","softwareBuildID":1}' to '{"dateCode":"20190102","softwareBuildID":2}'`,
`Device 'bulb' was updated from '{"dateCode":"${fromDateCode}","softwareBuildID":${fromSwBuildId}}' to '{"dateCode":"${toDateCode}","softwareBuildID":${toSwBuildId}}'`,
);
expect(devices.bulb.save).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'});
@@ -118,7 +129,11 @@ describe('Extension: OTAUpdate', () => {
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/update',
stringify({
data: {from: {date_code: '20190101', software_build_id: 1}, id: 'bulb', to: {date_code: '20190102', software_build_id: 2}},
data: {
from: {date_code: fromDateCode, software_build_id: fromSwBuildId},
id: 'bulb',
to: {date_code: toDateCode, software_build_id: toSwBuildId},
},
status: 'ok',
}),
{retain: false, qos: 0},
@@ -132,9 +147,7 @@ describe('Extension: OTAUpdate', () => {
return {swBuildId: 1, dateCode: '2019010'};
});
devices.bulb.save.mockClear();
updateToLatestSpy.mockImplementationOnce(() => {
throw new Error('Update failed');
});
updateSpy.mockRejectedValueOnce(new Error('Update failed'));
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', stringify({id: 'bulb'}));
await flushPromises();
@@ -157,7 +170,8 @@ describe('Extension: OTAUpdate', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(updateToLatestSpy).toHaveBeenCalledTimes(0);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(1, devices.bulb, {}, undefined, false);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}),
@@ -170,24 +184,56 @@ describe('Extension: OTAUpdate', () => {
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(2);
expect(updateToLatestSpy).toHaveBeenCalledTimes(0);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(2, devices.bulb, {}, undefined, false);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check/downgrade', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(3);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(3, devices.bulb, {}, undefined, true);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: false}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
// @ts-expect-error private
const device = controller.zigbee.resolveDevice(devices.bulb.ieeeAddr)!;
const originalDefinition = device.definition;
device.definition = Object.assign({}, originalDefinition, {ota: {suppressElementImageParseFailure: true}});
mockMQTT.publish.mockClear();
isUpdateAvailableSpy.mockResolvedValueOnce({available: true, currentFileVersion: 10, otaFileVersion: 12});
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check/downgrade', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(4);
expect(isUpdateAvailableSpy).toHaveBeenNthCalledWith(4, devices.bulb, {suppressElementImageParseFailure: true}, undefined, true);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({data: {id: 'bulb', update_available: true}, status: 'ok'}),
{retain: false, qos: 0},
expect.any(Function),
);
device.definition = originalDefinition;
});
it('Should handle if OTA update check fails', async () => {
isUpdateAvailableSpy.mockImplementationOnce(() => {
throw new Error('RF signals disturbed because of dogs barking');
});
isUpdateAvailableSpy.mockRejectedValueOnce(new Error('RF signals disturbed because of dogs barking'));
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(updateToLatestSpy).toHaveBeenCalledTimes(0);
expect(updateSpy).toHaveBeenCalledTimes(0);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/ota_update/check',
stringify({
@@ -223,11 +269,14 @@ describe('Extension: OTAUpdate', () => {
});
it('Should refuse to check/update when already in progress', async () => {
isUpdateAvailableSpy.mockImplementationOnce(() => {
return new Promise<void>((resolve) => {
setTimeout(() => resolve(), 99999);
});
});
isUpdateAvailableSpy.mockImplementationOnce(
// @ts-expect-error mocked as needed
async () => {
await new Promise<void>((resolve) => {
setTimeout(() => resolve(), 99999);
});
},
);
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
await flushPromises();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/check', 'bulb');
@@ -245,7 +294,7 @@ describe('Extension: OTAUpdate', () => {
it('Shouldnt crash when read modelID before/after OTA update fails', async () => {
devices.bulb.endpoints[0].read.mockRejectedValueOnce('Failed from').mockRejectedValueOnce('Failed to');
updateToLatestSpy.mockImplementation();
updateSpy.mockImplementation();
mockMQTTEvents.message('zigbee2mqtt/bridge/request/device/ota_update/update', 'bulb');
await flushPromises();
@@ -273,7 +322,7 @@ describe('Extension: OTAUpdate', () => {
await mockZHEvents.message(payload);
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382});
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {}, {imageType: 12382}, false);
expect(mockLogger.info).toHaveBeenCalledWith(`Update available for 'bulb'`);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
@@ -310,7 +359,7 @@ describe('Extension: OTAUpdate', () => {
await mockZHEvents.message(payload);
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382});
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {}, {imageType: 12382}, false);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(mockMQTT.publish).toHaveBeenCalledWith(
@@ -336,7 +385,7 @@ describe('Extension: OTAUpdate', () => {
await mockZHEvents.message(payload);
await flushPromises();
expect(isUpdateAvailableSpy).toHaveBeenCalledTimes(1);
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {imageType: 12382});
expect(isUpdateAvailableSpy).toHaveBeenCalledWith(devices.bulb, {}, {imageType: 12382}, false);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledTimes(1);
expect(devices.bulb.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 0x98}, undefined, 10);
expect(mockMQTT.publish).toHaveBeenCalledWith(
@@ -401,17 +450,31 @@ describe('Extension: OTAUpdate', () => {
expect(device.endpoints[0].commandResponse).toHaveBeenCalledWith('genOta', 'queryNextImageResponse', {status: 152}, undefined, 10);
});
it('Set zigbee_ota_override_index_location', async () => {
const spyUseIndexOverride = jest.spyOn(zigbeeOTA, 'useIndexOverride');
it('Sets given configuration', async () => {
const setConfiguration = jest.spyOn(zhc.ota, 'setConfiguration');
settings.set(['ota', 'zigbee_ota_override_index_location'], 'local.index.json');
settings.set(['ota', 'image_block_response_delay'], 10000);
settings.set(['ota', 'default_maximum_data_size'], 10);
await resetExtension();
expect(spyUseIndexOverride).toHaveBeenCalledWith(path.join(data.mockDir, 'local.index.json'));
spyUseIndexOverride.mockClear();
expect(setConfiguration).toHaveBeenCalledWith({
...DEFAULT_CONFIG,
overrideIndexLocation: path.join(data.mockDir, 'local.index.json'),
imageBlockResponseDelay: 10000,
defaultMaximumDataSize: 10,
});
setConfiguration.mockClear();
settings.set(['ota', 'zigbee_ota_override_index_location'], 'http://my.site/index.json');
settings.set(['ota', 'image_block_response_delay'], 50);
settings.set(['ota', 'default_maximum_data_size'], 100);
await resetExtension();
expect(spyUseIndexOverride).toHaveBeenCalledWith('http://my.site/index.json');
spyUseIndexOverride.mockClear();
expect(setConfiguration).toHaveBeenCalledWith({
...DEFAULT_CONFIG,
overrideIndexLocation: 'http://my.site/index.json',
imageBlockResponseDelay: 50,
defaultMaximumDataSize: 100,
});
setConfiguration.mockClear();
});
it('Clear update state on startup', async () => {
+6 -7
View File
@@ -47,7 +47,7 @@ describe('Extension: Receive', () => {
expect(mockMQTT.publish).toHaveBeenCalledTimes(1);
expect(mockMQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/button',
stringify({action: 'single', click: 'single', linkquality: 10}),
stringify({action: 'single', linkquality: 10}),
{retain: false, qos: 0},
expect.any(Function),
);
@@ -61,7 +61,7 @@ describe('Extension: Receive', () => {
await flushPromises();
expect(mockMQTT.publish).toHaveBeenCalledTimes(1);
expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', action: 'single_left'});
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single_left'});
expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
@@ -73,7 +73,7 @@ describe('Extension: Receive', () => {
await flushPromises();
expect(mockMQTT.publish).toHaveBeenCalledTimes(1);
expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', action: 'single_right'});
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'single_right'});
expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
@@ -560,7 +560,6 @@ describe('Extension: Receive', () => {
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({
battery: 100,
illuminance: 381,
illuminance_lux: 381,
voltage: 3045,
device_temperature: 19,
power_outage_count: 34,
@@ -673,7 +672,7 @@ describe('Extension: Receive', () => {
await flushPromises();
expect(mockMQTT.publish).toHaveBeenCalledTimes(1);
expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'});
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
});
@@ -688,10 +687,10 @@ describe('Extension: Receive', () => {
await flushPromises();
expect(mockMQTT.publish).toHaveBeenCalledTimes(2);
expect(mockMQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'brightness_stop', action: 'brightness_stop'});
expect(JSON.parse(mockMQTT.publish.mock.calls[0][1])).toStrictEqual({action: 'brightness_stop'});
expect(mockMQTT.publish.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
expect(mockMQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff');
expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toMatchObject({click: 'brightness_stop', action: 'brightness_stop'});
expect(JSON.parse(mockMQTT.publish.mock.calls[1][1])).toMatchObject({action: 'brightness_stop'});
expect(JSON.parse(mockMQTT.publish.mock.calls[1][1]).elapsed).toBe(50);
expect(mockMQTT.publish.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
});
+29 -1
View File
@@ -204,6 +204,20 @@ export class Endpoint {
removeFromAllGroups(): void {
Object.values(groups).forEach((g) => this.removeFromGroup(g));
}
mockClear(): void {
this.command.mockClear();
this.commandResponse.mockClear();
this.read.mockClear();
this.write.mockClear();
this.bind.mockClear();
this.unbind.mockClear();
this.save.mockClear();
this.configureReporting.mockClear();
this.addToGroup.mockClear();
this.removeFromGroup.mockClear();
this.getClusterAttributeValue.mockClear();
}
}
export class Device {
@@ -277,6 +291,20 @@ export class Device {
getEndpoint(ID: number): Endpoint | undefined {
return this.endpoints.find((e) => e.ID === ID);
}
mockClear(): void {
this.interview.mockClear();
this.ping.mockClear();
this.removeFromNetwork.mockClear();
this.removeFromDatabase.mockClear();
this.addCustomCluster.mockClear();
this.save.mockClear();
this.lqi.mockClear();
this.routingTable.mockClear();
this.meta = {};
this.endpoints.forEach((e) => e.mockClear());
}
}
export class Group {
@@ -466,7 +494,7 @@ const groupMembersBackup = Object.fromEntries(Object.entries(groups).map((v) =>
export function resetGroupMembers(): void {
for (const key in groupMembersBackup) {
groups[key].members = [...groupMembersBackup[key]];
groups[key as keyof typeof groups].members = [...groupMembersBackup[key]];
}
}
-7
View File
@@ -914,13 +914,6 @@ describe('Settings', () => {
expect(settings.get().serial.baudrate).toStrictEqual(20);
});
it('ikea_ota_use_test_url config', () => {
write(configurationFile, {...minimalConfig, advanced: {ikea_ota_use_test_url: true}});
settings.reRead();
expect(settings.get().ota.ikea_ota_use_test_url).toStrictEqual(true);
});
it('transmit_power config', () => {
write(configurationFile, {...minimalConfig, experimental: {transmit_power: 1337}});