diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index bc4184678b..c2b7727fee 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -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 { 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 { - 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 => { + 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( diff --git a/lib/model/device.ts b/lib/model/device.ts index 2c8f124834..0032eea5b6 100644 --- a/lib/model/device.ts +++ b/lib/model/device.ts @@ -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; diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 23859a66ea..5f6398c82d 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -173,7 +173,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; diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index f30239472f..503e36d08e 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -337,19 +337,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 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. Note: This value may get ignored for manufacturers that require specific values.", + "default": 50, + "minimum": 10, + "maximum": 100, + "requiresRestart": true } } }, @@ -757,13 +767,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 } } }, diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 1fd12fbcc2..f4e769ff5a 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -21,7 +21,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; @@ -77,6 +76,8 @@ const defaults: RecursivePartial = { ota: { update_check_interval: 24 * 60, disable_automatic_update_check: false, + image_block_response_delay: 250, + default_maximum_data_size: 50, }, device_options: {}, advanced: { @@ -184,12 +185,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 diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 3db3810fd6..1f6418e8e8 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -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'}, }, diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts index 97e3a8aa14..c6aaf0c6a0 100644 --- a/test/extensions/otaUpdate.test.ts +++ b/test/extensions/otaUpdate.test.ts @@ -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 => { 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,31 +69,44 @@ 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) => { + devices.bulb.mockClear(); + 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.save).toHaveBeenCalledTimes(1); // TODO: problem with jest? detects x2 on second value in it.each array (no matter which one is there, extra mockClear doesn't work) expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: 'immediate'}); expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith('genBasic', ['dateCode', 'swBuildId'], {sendPolicy: undefined}); expect(mockMQTT.publish).toHaveBeenCalledWith( @@ -118,7 +130,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 +148,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 +171,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 +185,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 +270,14 @@ describe('Extension: OTAUpdate', () => { }); it('Should refuse to check/update when already in progress', async () => { - isUpdateAvailableSpy.mockImplementationOnce(() => { - return new Promise((resolve) => { - setTimeout(() => resolve(), 99999); - }); - }); + isUpdateAvailableSpy.mockImplementationOnce( + // @ts-expect-error mocked as needed + async () => { + await new Promise((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 +295,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 +323,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 +360,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 +386,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 +451,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 () => { diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 19e60b0f10..1d4179a658 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -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,19 @@ 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.endpoints.forEach((e) => e.mockClear()); + } } export class Group { @@ -466,7 +493,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]]; } } diff --git a/test/settings.test.ts b/test/settings.test.ts index 4c54734ae0..c1785c1bd6 100644 --- a/test/settings.test.ts +++ b/test/settings.test.ts @@ -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}});