From b71461e03cd8ceea5365f4c0b2e794e34341325e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Thu, 5 Feb 2026 21:24:58 +0100 Subject: [PATCH] fix(ignore): Changes for new ZHC `definition.version` (#30402) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Co-authored-by: rhysfred Co-authored-by: rhys Co-authored-by: Patrick ZAJDA Co-authored-by: Ignacio Hernandez-Ros Co-authored-by: Rohan Kapoor Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com> Co-authored-by: Stephan Garland Co-authored-by: Andrei LAZAROV --- lib/extension/configure.ts | 19 +++++++---- test/extensions/bind.test.ts | 2 +- test/extensions/configure.test.ts | 54 +++++++++++++++++++++++++++++-- test/extensions/otaUpdate.test.ts | 2 +- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/lib/extension/configure.ts b/lib/extension/configure.ts index 17b7c0541..04e4b60d5 100644 --- a/lib/extension/configure.ts +++ b/lib/extension/configure.ts @@ -1,6 +1,5 @@ import bind from "bind-decorator"; import stringify from "json-stable-stringify-without-jsonify"; -import * as zhc from "zigbee-herdsman-converters"; import Device from "../model/device"; import type {Zigbee2MQTTAPI} from "../types/api"; import logger from "../util/logger"; @@ -94,17 +93,25 @@ export default class Configure extends Extension { return; } + const definitionVersion = device.definition.version; if (!force) { if (device.options.disabled || !device.interviewed) { return; } - if (device.zh.meta?.configured !== undefined) { + // Only configure end devices when it is active, otherwise it will likely fails as they are sleeping. + if (device.zh.type === "EndDevice" && event !== "zigbee_event") { return; } - // Only configure end devices when it is active, otherwise it will likely fails as they are sleeping. - if (device.zh.type === "EndDevice" && event !== "zigbee_event") { + const shouldReconfigure = + // Should always reconfigure when not configured before + device.zh.meta?.configured === undefined || + // Or should reconfigure when definition.version is not '0.0.0' and differs from last `meta.configured`. + // In older Z2M versions the stored `meta.configured` was the hash of the configure function. + // Since we don't want to reconfigure all devices, we don't re-configure when the definition has the default version of '0.0.0'. + (definitionVersion !== "0.0.0" && device.zh.meta?.configured !== definitionVersion); + if (!shouldReconfigure) { return; } } @@ -122,8 +129,8 @@ export default class Configure extends Extension { logger.info(`Configuring '${device.name}'`); try { await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), device.definition); - logger.info(`Successfully configured '${device.name}'`); - device.zh.meta.configured = zhc.getConfigureKey(device.definition); + logger.info(`Successfully configured '${device.name}' (definition v${definitionVersion})`); + device.zh.meta.configured = definitionVersion; device.zh.save(); this.eventBus.emitDevicesChanged(); } catch (error) { diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index aec4638fa..448fc11d1 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -358,7 +358,7 @@ describe("Extension: Bind", () => { {attribute: "currentX", minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, {attribute: "currentY", minimumReportInterval: 5, maximumReportInterval: 0xffff, reportableChange: 1}, ]); - expect(devices.bulb_color.meta.configured).toBe(332242049); + expect(devices.bulb_color.meta.configured).toBe("0.0.0"); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/device/unbind", stringify({ diff --git a/test/extensions/configure.test.ts b/test/extensions/configure.test.ts index 7229254ac..7562b0ddf 100644 --- a/test/extensions/configure.test.ts +++ b/test/extensions/configure.test.ts @@ -4,13 +4,15 @@ import * as data from "../mocks/data"; import {mockLogger} from "../mocks/logger"; import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt"; import {flushPromises} from "../mocks/utils"; -import {type Device, devices, type Endpoint, events as mockZHEvents} from "../mocks/zigbeeHerdsman"; +import {devices, type Endpoint, events as mockZHEvents, type Device as ZhDevice} from "../mocks/zigbeeHerdsman"; import stringify from "json-stable-stringify-without-jsonify"; import {InterviewState} from "zigbee-herdsman/dist/controller/model/device"; import {Controller} from "../../lib/controller"; +import Device from "../../lib/model/device"; import Configure from "../../lib/extension/configure"; import * as settings from "../../lib/util/settings"; +import assert from "node:assert"; const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug]; @@ -23,7 +25,7 @@ describe("Extension: Configure", () => { await controller.addExtension(new Configure(...controller.extensionArgs)); }; - const mockClear = (device: Device): void => { + const mockClear = (device: ZhDevice): void => { for (const endpoint of device.endpoints) { endpoint.read.mockClear(); endpoint.write.mockClear(); @@ -42,7 +44,7 @@ describe("Extension: Configure", () => { const endpoint2 = device.getEndpoint(2)!; expect(endpoint2.write).toHaveBeenCalledTimes(1); expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {49: {type: 25, value: 11}}, {disableDefaultResponse: true, manufacturerCode: 4107}); - expect(device.meta.configured).toBe(332242049); + expect(device.meta.configured).toBe("0.0.0"); }; const expectBulbConfigured = (): void => { @@ -147,6 +149,52 @@ describe("Extension: Configure", () => { expectBulbConfigured(); }); + it("Should re-configure when the version of the definition changes to a different value than the default (0.0.0)", async () => { + // Device is initially configured (definition has uses default version of 0.0.0) + const device = devices.bulb; + expectBulbConfigured(); + + // Nothing happens when receiving a Zigbee message + mockClear(device); + await mockZHEvents.lastSeenChanged({device}); + await flushPromises(); + expectBulbNotConfigured(); + + // Simulate that the definition version changes + const resolvedEntity = controller.zigbee.resolveEntity(device); + assert(resolvedEntity instanceof Device && resolvedEntity.definition); + assert(resolvedEntity.definition.version === "0.0.0"); + + try { + // Change the version to a different value + resolvedEntity.definition.version = "0.0.1"; + + // Now it should re-configure upon receiving a Zigbee message + mockClear(device); + await mockZHEvents.lastSeenChanged({device}); + await flushPromises(); + expectBulbConfigured(); + } finally { + resolvedEntity.definition.version = "0.0.0"; + } + }); + + it("Should NOT re-configure when version of the definition is 0.0.0", async () => { + // This test the migration from the old configureKey (hash of the configure function) to the new definition version system. + // See commment in `configure.ts` -> `shouldReconfigure` for more details. + + // Device is initially configured (definition has uses default version of 0.0.0) + const device = devices.bulb; + expectBulbConfigured(); + device.meta.configured = 1321; + + // Nothing happens when receiving a Zigbee message + mockClear(device); + await mockZHEvents.lastSeenChanged({device}); + await flushPromises(); + expectBulbNotConfigured(); + }); + it("Should allow to configure via MQTT", async () => { mockClear(devices.remote); expectRemoteNotConfigured(); diff --git a/test/extensions/otaUpdate.test.ts b/test/extensions/otaUpdate.test.ts index 522162d26..f5e3903a4 100644 --- a/test/extensions/otaUpdate.test.ts +++ b/test/extensions/otaUpdate.test.ts @@ -143,7 +143,7 @@ describe("Extension: OTAUpdate", () => { expect(infoCalls[7]).toStrictEqual(`Interviewing 'bulb'`); expect(infoCalls[8]).toStrictEqual(`Configuring 'bulb'`); expect(infoCalls[10]).toStrictEqual(`Successfully interviewed 'bulb'`); - expect(infoCalls[11]).toStrictEqual(`Successfully configured 'bulb'`); + expect(infoCalls[11]).toStrictEqual(`Successfully configured 'bulb' (definition v0.0.0)`); expect(devices.bulb.save).toHaveBeenCalledTimes(1); expect(devices.bulb.endpoints[0].read).toHaveBeenCalledWith("genBasic", ["dateCode", "swBuildId"], {sendPolicy: "immediate"});