From 476efaa6e18a1d2ea069ea9c128536356df547ac Mon Sep 17 00:00:00 2001 From: Nerivec <62446222+Nerivec@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:31:32 +0100 Subject: [PATCH] fix: OTA availability detection (#30815) Co-authored-by: Koen Kanters --- lib/extension/otaUpdate.ts | 11 +++++------ lib/types/api.ts | 2 +- test/extensions/otaUpdate.test.ts | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/extension/otaUpdate.ts b/lib/extension/otaUpdate.ts index 0cec9001a..9c8ed542e 100644 --- a/lib/extension/otaUpdate.ts +++ b/lib/extension/otaUpdate.ts @@ -218,7 +218,7 @@ export default class OTAUpdate extends Extension { latest_release_notes: deviceUpdateState?.latest_release_notes, } : { - state: state.available === 0 ? "idle" : "available", + state: state.available ? "available" : "idle", installed_version: state.current.fileVersion, latest_version: state.availableMeta?.fileVersion ?? state.current.fileVersion, latest_source: state.availableMeta?.url || null, @@ -303,13 +303,12 @@ export default class OTAUpdate extends Extension { await this.publishEntityState(device, this.#getEntityPublishPayload(device, availableResult)); this.#lastChecked.set(device.ieeeAddr, Date.now()); - const available = availableResult.available !== 0; const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, { id: ID, - update_available: available, + update_available: availableResult.available, + downgrade: source.downgrade, source: availableResult.availableMeta?.url, release_notes: availableResult.availableMeta?.releaseNotes, - downgrade: available ? availableResult.available === 1 : undefined, }); await this.mqtt.publish("bridge/response/device/ota_update/check", stringify(response)); @@ -495,7 +494,7 @@ export default class OTAUpdate extends Extension { if (to === undefined) { this.#removeProgressAndRemainingFromState(device); - await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: 0, current: from})); + await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: from})); return [from.fileVersion, undefined]; } @@ -503,7 +502,7 @@ export default class OTAUpdate extends Extension { logger.info(`Finished update of '${device.name}'`); this.#removeProgressAndRemainingFromState(device); - await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: 0, current: to})); + await this.publishEntityState(device, this.#getEntityPublishPayload(device, {available: false, current: to})); logger.info(() => `Device '${device.name}' was OTA updated from '${from.fileVersion}' to '${to.fileVersion}'`); diff --git a/lib/types/api.ts b/lib/types/api.ts index 9469e09bb..f9bb0eb8c 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -614,9 +614,9 @@ export interface Zigbee2MQTTAPI { "bridge/response/device/ota_update/check": { id: string; update_available: boolean; + downgrade?: boolean; source?: string; release_notes?: string; - downgrade?: boolean; }; "bridge/request/device/ota_update/update": { diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts index de3c2b8cd..522162d26 100644 --- a/test/extensions/otaUpdate.test.ts +++ b/test/extensions/otaUpdate.test.ts @@ -585,7 +585,7 @@ describe("Extension: OTAUpdate", () => { }); it("is able to check if OTA update is available", async () => { - devices.bulb.checkOta.mockResolvedValueOnce({available: 0, current: {...DEFAULT_CURRENT, fileVersion: 10}}); + devices.bulb.checkOta.mockResolvedValueOnce({available: false, current: {...DEFAULT_CURRENT, fileVersion: 10}}); mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/check", stringify({id: "bulb"})); await flushPromises(); expect(devices.bulb.checkOta).toHaveBeenCalledTimes(1); @@ -593,13 +593,13 @@ describe("Extension: OTAUpdate", () => { expect(devices.bulb.updateOta).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/ota_update/check", - stringify({data: {id: "bulb", update_available: false}, status: "ok"}), + stringify({data: {id: "bulb", update_available: false, downgrade: false}, status: "ok"}), {}, ); mockMQTTPublishAsync.mockClear(); devices.bulb.checkOta.mockResolvedValueOnce({ - available: -1, + available: true, current: {...DEFAULT_CURRENT, fileVersion: 10}, availableMeta: {...DEFAULT_AVAILABLE_META, fileVersion: 12}, }); @@ -613,7 +613,7 @@ describe("Extension: OTAUpdate", () => { stringify({data: {id: "bulb", update_available: true, downgrade: false, source: "https://example.com/my.ota"}, status: "ok"}), {}, ); - devices.bulb.checkOta.mockResolvedValueOnce({available: 0, current: {...DEFAULT_CURRENT, fileVersion: 10}}); + devices.bulb.checkOta.mockResolvedValueOnce({available: false, current: {...DEFAULT_CURRENT, fileVersion: 10}}); mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/check/downgrade", stringify({id: "bulb"})); await flushPromises(); expect(devices.bulb.checkOta).toHaveBeenCalledTimes(3); @@ -621,7 +621,7 @@ describe("Extension: OTAUpdate", () => { expect(devices.bulb.updateOta).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/ota_update/check", - stringify({data: {id: "bulb", update_available: false}, status: "ok"}), + stringify({data: {id: "bulb", update_available: false, downgrade: true}, status: "ok"}), {}, ); @@ -632,7 +632,7 @@ describe("Extension: OTAUpdate", () => { mockMQTTPublishAsync.mockClear(); devices.bulb.checkOta.mockResolvedValueOnce({ - available: 1, + available: true, current: {...DEFAULT_CURRENT, fileVersion: 10}, availableMeta: {...DEFAULT_AVAILABLE_META, fileVersion: 8}, }); @@ -690,7 +690,7 @@ describe("Extension: OTAUpdate", () => { it("allows check OTA with custom URL even when device does not support it", async () => { devices.HGZB04D.checkOta.mockResolvedValueOnce({ - available: -1, + available: true, current: {...DEFAULT_CURRENT, fileVersion: 10}, availableMeta: {...DEFAULT_AVAILABLE_META, fileVersion: 14, releaseNotes: "New features"}, }); @@ -730,7 +730,7 @@ describe("Extension: OTAUpdate", () => { setTimeout( () => resolve({ - available: 0, + available: false, current: {...DEFAULT_CURRENT, fileVersion: 1}, }), 99999, @@ -794,7 +794,7 @@ describe("Extension: OTAUpdate", () => { it("checks for update when device requests it", async () => { const data = {imageType: 12382, manufacturerCode: 2134, fileVersion: 33}; devices.bulb.checkOta.mockResolvedValueOnce({ - available: -1, + available: true, current: {...DEFAULT_CURRENT, ...data}, availableMeta: {...DEFAULT_AVAILABLE_META, fileVersion: 34}, }); @@ -864,7 +864,7 @@ describe("Extension: OTAUpdate", () => { it("checks for update when device requests it and it is not available", async () => { const data = {imageType: 12382, manufacturerCode: 2134, fileVersion: 33}; devices.bulb.checkOta.mockResolvedValueOnce({ - available: 0, + available: false, current: {...DEFAULT_CURRENT, ...data}, availableMeta: {...DEFAULT_AVAILABLE_META, fileVersion: 33}, });