From 3baf67c1b0e44dd8644df5751dc8c79a8c264826 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Fri, 22 May 2026 23:25:20 +0200 Subject: [PATCH 01/16] Poll `manuSpecificPhilips2` for Hue lights --- lib/extension/bind.ts | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 444ff26150..2b1d548d8f 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -93,7 +93,7 @@ const POLL_ON_MESSAGE = [ read: {cluster: "genLevelCtrl" as const, attributes: ["currentLevel"] as TClusterAttributeKeys<"genLevelCtrl">}, // When the bound devices/members of group have the following manufacturerIDs manufacturerIDs: [ - Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, + // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -124,7 +124,7 @@ const POLL_ON_MESSAGE = [ }, read: {cluster: "genOnOff" as const, attributes: ["onOff"] as TClusterAttributeKeys<"genOnOff">}, manufacturerIDs: [ - Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, + // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -158,7 +158,7 @@ const POLL_ON_MESSAGE = [ }, }, manufacturerIDs: [ - Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, + // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -167,6 +167,36 @@ const POLL_ON_MESSAGE = [ ], manufacturerNames: ["GLEDOPTO", "Trust International B.V.\u0000"], }, + { + // Philips Hue + cluster: { + manuSpecificPhilips: [ + {type: "commandHueNotification", data: {button: 1}}, + {type: "commandHueNotification", data: {button: 2}}, + {type: "commandHueNotification", data: {button: 3}}, + {type: "commandHueNotification", data: {button: 4}}, + ], + genLevelCtrl: [ + {type: "commandStep", data: {}}, + {type: "commandStepWithOnOff", data: {}}, + {type: "commandStop", data: {}}, + {type: "commandMoveWithOnOff", data: {}}, + {type: "commandStopWithOnOff", data: {}}, + {type: "commandMove", data: {}}, + {type: "commandMoveToLevelWithOnOff", data: {}}, + ], + genOnOff: [ + {type: "commandOn", data: {}}, + {type: "commandOff", data: {}}, + {type: "commandOffWithEffect", data: {}}, + {type: "commandToggle", data: {}}, + ], + genScenes: [{type: "commandRecall", data: {}}], + }, + read: {cluster: "manuSpecificPhilips2" as const, attributes: ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">}, + manufacturerIDs: [Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V], + manufacturerNames: [], + }, ]; interface ParsedMQTTMessage { From 274e2404e376e165b53aa96c653790a90727a39e Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sat, 23 May 2026 20:13:17 +0200 Subject: [PATCH 02/16] Use `manuSpecificPhilips2` only when supported --- lib/extension/bind.ts | 45 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 2b1d548d8f..2d19c49c88 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -93,7 +93,7 @@ const POLL_ON_MESSAGE = [ read: {cluster: "genLevelCtrl" as const, attributes: ["currentLevel"] as TClusterAttributeKeys<"genLevelCtrl">}, // When the bound devices/members of group have the following manufacturerIDs manufacturerIDs: [ - // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead + Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -124,7 +124,7 @@ const POLL_ON_MESSAGE = [ }, read: {cluster: "genOnOff" as const, attributes: ["onOff"] as TClusterAttributeKeys<"genOnOff">}, manufacturerIDs: [ - // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead + Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -158,7 +158,7 @@ const POLL_ON_MESSAGE = [ }, }, manufacturerIDs: [ - // Note: Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, uses manuSpecificPhilips2.state instead + Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, Zcl.ManufacturerCode.ATMEL, Zcl.ManufacturerCode.GLEDOPTO_CO_LTD, Zcl.ManufacturerCode.MUELLER_LICHT_INTERNATIONAL_INC, @@ -167,36 +167,6 @@ const POLL_ON_MESSAGE = [ ], manufacturerNames: ["GLEDOPTO", "Trust International B.V.\u0000"], }, - { - // Philips Hue - cluster: { - manuSpecificPhilips: [ - {type: "commandHueNotification", data: {button: 1}}, - {type: "commandHueNotification", data: {button: 2}}, - {type: "commandHueNotification", data: {button: 3}}, - {type: "commandHueNotification", data: {button: 4}}, - ], - genLevelCtrl: [ - {type: "commandStep", data: {}}, - {type: "commandStepWithOnOff", data: {}}, - {type: "commandStop", data: {}}, - {type: "commandMoveWithOnOff", data: {}}, - {type: "commandStopWithOnOff", data: {}}, - {type: "commandMove", data: {}}, - {type: "commandMoveToLevelWithOnOff", data: {}}, - ], - genOnOff: [ - {type: "commandOn", data: {}}, - {type: "commandOff", data: {}}, - {type: "commandOffWithEffect", data: {}}, - {type: "commandToggle", data: {}}, - ], - genScenes: [{type: "commandRecall", data: {}}], - }, - read: {cluster: "manuSpecificPhilips2" as const, attributes: ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">}, - manufacturerIDs: [Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V], - manufacturerNames: [], - }, ]; interface ParsedMQTTMessage { @@ -644,18 +614,25 @@ export default class Bind extends Extension { } let readAttrs = poll.read.attributes; + let readCluster = poll.read.cluster; if (poll.read.attributesForEndpoint) { const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; } + // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead + if (endpoint.supportsInputCluster("manuSpecificPhilips2")) { + readCluster = "manuSpecificPhilips2" as const; + readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; + } + const key = `${device.ieeeAddr}_${endpoint.ID}_${POLL_ON_MESSAGE.indexOf(poll)}`; if (!this.pollDebouncers[key]) { this.pollDebouncers[key] = debounce(async () => { try { - await endpoint.read(poll.read.cluster, readAttrs); + await endpoint.read(readCluster, readAttrs); } catch (error) { // biome-ignore lint/style/noNonNullAssertion: TODO: biome migration: ??? const resolvedDevice = this.zigbee.resolveEntity(device)!; From 5642bd95900128b6b778e793030b6775f07359cb Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sat, 23 May 2026 20:20:07 +0200 Subject: [PATCH 03/16] Refactor readCluster and readAttrs type assertions Updated type assertions for readCluster and readAttrs. --- lib/extension/bind.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 2d19c49c88..6dcc84bc5d 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -623,8 +623,8 @@ export default class Bind extends Extension { // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead if (endpoint.supportsInputCluster("manuSpecificPhilips2")) { - readCluster = "manuSpecificPhilips2" as const; - readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; + readCluster = "manuSpecificPhilips2" as unknown as typeof poll.read.cluster; + readAttrs = ["state"] as unknown as typeof poll.read.attributes; } const key = `${device.ieeeAddr}_${endpoint.ID}_${POLL_ON_MESSAGE.indexOf(poll)}`; From 0dd6d4c22fecaaf423c7dc5e593acae3ae5d9951 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sat, 23 May 2026 20:28:36 +0200 Subject: [PATCH 04/16] Update test expectations for Hue bulb polling --- test/extensions/bind.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 448fc11d10..65e65537cf 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -738,7 +738,7 @@ describe("Extension: Bind", () => { await mockZHEvents.message(payload); await flushPromises(); expect(mockDebounce).toHaveBeenCalledTimes(1); - expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); it("Should poll bounded Hue bulb when receiving message from scene controller", async () => { @@ -760,9 +760,9 @@ describe("Extension: Bind", () => { await flushPromises(); // Calls to three clusters are expected in this case expect(mockDebounce).toHaveBeenCalledTimes(3); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genOnOff", ["onOff"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); it("Should poll grouped Hue bulb when receiving message from TRADFRI remote", async () => { @@ -783,8 +783,8 @@ describe("Extension: Bind", () => { await flushPromises(); expect(mockDebounce).toHaveBeenCalledTimes(2); expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(2); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genOnOff", ["onOff"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); // Should also only debounce once await mockZHEvents.message(payload); From a3b89d235823b5bdde990cc843fb2f2006d021ec Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sun, 24 May 2026 18:38:48 +0000 Subject: [PATCH 05/16] fix: improve attribute reading logic for manuSpecificPhilips2 cluster --- lib/extension/bind.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 6dcc84bc5d..6d82d4c0f7 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -613,18 +613,16 @@ export default class Bind extends Extension { continue; } - let readAttrs = poll.read.attributes; - let readCluster = poll.read.cluster; - - if (poll.read.attributesForEndpoint) { - const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); - readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; - } + let readAttrs = poll.read.attributes as TClusterAttributeKeys; + let readCluster: ClusterName = poll.read.cluster as ClusterName; // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead if (endpoint.supportsInputCluster("manuSpecificPhilips2")) { - readCluster = "manuSpecificPhilips2" as unknown as typeof poll.read.cluster; - readAttrs = ["state"] as unknown as typeof poll.read.attributes; + readCluster = "manuSpecificPhilips2" as ClusterName; + readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; + } else if (poll.read.attributesForEndpoint) { + const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); + readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; } const key = `${device.ieeeAddr}_${endpoint.ID}_${POLL_ON_MESSAGE.indexOf(poll)}`; From 29abc6b1ef8bbba544414296e958de3c00a241f4 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sun, 24 May 2026 18:59:32 +0000 Subject: [PATCH 06/16] new mock bulb for tests --- test/extensions/bind.test.ts | 31 +++++++++++++++++++++++++------ test/mocks/zigbeeHerdsman.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 65e65537cf..9effafa1e0 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -19,6 +19,7 @@ const mocksClear = [ devices.bulb_color.getEndpoint(1)!.configureReporting, devices.bulb_color.getEndpoint(1)!.bind, devices.bulb_color_2.getEndpoint(1)!.read, + devices.bulb_color_3.getEndpoint(1)!.read, ]; describe("Extension: Bind", () => { @@ -738,7 +739,7 @@ describe("Extension: Bind", () => { await mockZHEvents.message(payload); await flushPromises(); expect(mockDebounce).toHaveBeenCalledTimes(1); - expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); }); it("Should poll bounded Hue bulb when receiving message from scene controller", async () => { @@ -760,9 +761,9 @@ describe("Extension: Bind", () => { await flushPromises(); // Calls to three clusters are expected in this case expect(mockDebounce).toHaveBeenCalledTimes(3); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genOnOff", ["onOff"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]); }); it("Should poll grouped Hue bulb when receiving message from TRADFRI remote", async () => { @@ -783,8 +784,8 @@ describe("Extension: Bind", () => { await flushPromises(); expect(mockDebounce).toHaveBeenCalledTimes(2); expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(2); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); - expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + expect(devices.bulb_color_2.getEndpoint(1)!.read).toHaveBeenCalledWith("genOnOff", ["onOff"]); // Should also only debounce once await mockZHEvents.message(payload); @@ -796,6 +797,24 @@ describe("Extension: Bind", () => { expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); }); + it("Should poll manuSpecificPhilips2.state of bound Hue bulb when receiving message from dimmer", async () => { + const remote = devices.remote; + const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; + const payload = { + data, + cluster: "manuSpecificPhilips", + device: remote, + endpoint: remote.getEndpoint(2)!, + type: "commandHueNotification", + linkquality: 10, + groupID: 0, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(mockDebounce).toHaveBeenCalledTimes(1); + expect(devices.bulb_color_3.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); + }); + it("clears all bindings", async () => { const device = devices.remote; diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 23298bcd97..ebf1f562fc 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -419,6 +419,32 @@ const bulb_color_2 = new Device( "2019.09", "5.127.1.26581", ); +const bulb_color_3 = new Device( + "Router", + "0x000b57fffec6a5b5", + 401293, + 4107, + [ + new Endpoint( + 1, + [0, 3, 4, 5, 6, 8, 768, 2821, 4096, 64515], + [5, 25, 32, 4096], + "0x000b57fffec6a5b5", + [], + {lightingColorCtrl: {colorCapabilities: 254}}, + [], + undefined, + undefined, + {scenes: {"1_0": {name: "Chill scene", state: {state: "ON"}}, "4_9": {state: {state: "OFF"}}}}, + ), + ], + InterviewState.Successful, + "Mains (single phase)", + "LLC020", + "Philips", + "2019.09", + "5.127.1.26581", +); const bulb_2 = new Device( "Router", "0x000b57fffec6a5b7", @@ -610,6 +636,7 @@ export const devices = { bulb_2: bulb_2, hue_twilight, bulb_color_2: bulb_color_2, + bulb_color_3: bulb_color_3, remote: new Device( "EndDevice", "0x0017880104e45517", From 091e638eefe49c7ea4b53ad5330f54203a58d1b4 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sun, 24 May 2026 23:25:32 +0000 Subject: [PATCH 07/16] Use `manuSpecificPhilips2` only when Hue Native Control is enabled --- lib/extension/bind.ts | 2 +- test/extensions/bind.test.ts | 24 ++++---------------- test/mocks/zigbeeHerdsman.ts | 44 ++++++++++++++++++------------------ 3 files changed, 27 insertions(+), 43 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 6d82d4c0f7..8f43e88fe0 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -617,7 +617,7 @@ export default class Bind extends Extension { let readCluster: ClusterName = poll.read.cluster as ClusterName; // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead - if (endpoint.supportsInputCluster("manuSpecificPhilips2")) { + if (data.device.zh.meta?.options?.hue_native_control === true) { readCluster = "manuSpecificPhilips2" as ClusterName; readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; } else if (poll.read.attributesForEndpoint) { diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 9effafa1e0..b350d5137e 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -19,7 +19,9 @@ const mocksClear = [ devices.bulb_color.getEndpoint(1)!.configureReporting, devices.bulb_color.getEndpoint(1)!.bind, devices.bulb_color_2.getEndpoint(1)!.read, - devices.bulb_color_3.getEndpoint(1)!.read, + //devices.bulb_color_3.getEndpoint(1)!.configureReporting, + //devices.bulb_color_3.getEndpoint(1)!.bind, + //devices.bulb_color_3.getEndpoint(1)!.read, ]; describe("Extension: Bind", () => { @@ -739,7 +741,7 @@ describe("Extension: Bind", () => { await mockZHEvents.message(payload); await flushPromises(); expect(mockDebounce).toHaveBeenCalledTimes(1); - expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); it("Should poll bounded Hue bulb when receiving message from scene controller", async () => { @@ -797,24 +799,6 @@ describe("Extension: Bind", () => { expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); }); - it("Should poll manuSpecificPhilips2.state of bound Hue bulb when receiving message from dimmer", async () => { - const remote = devices.remote; - const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; - const payload = { - data, - cluster: "manuSpecificPhilips", - device: remote, - endpoint: remote.getEndpoint(2)!, - type: "commandHueNotification", - linkquality: 10, - groupID: 0, - }; - await mockZHEvents.message(payload); - await flushPromises(); - expect(mockDebounce).toHaveBeenCalledTimes(1); - expect(devices.bulb_color_3.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); - }); - it("clears all bindings", async () => { const device = devices.remote; diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index ebf1f562fc..697ab29002 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -384,32 +384,17 @@ const bulb_color = new Device( "0x000b57fffec6a5b3", 40399, 4107, - [ - new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5b3", [], { - lightingColorCtrl: {colorCapabilities: 254}, - }), - ], - InterviewState.Successful, - "Mains (single phase)", - "LLC020", -); -const bulb_color_2 = new Device( - "Router", - "0x000b57fffec6a5b4", - 401292, - 4107, [ new Endpoint( 1, - [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + [0, 3, 4, 5, 6, 8, 768, 2821, 4096, 64515], [5, 25, 32, 4096], - "0x000b57fffec6a5b4", + "0x000b57fffec6a5b3", [], {lightingColorCtrl: {colorCapabilities: 254}}, [], undefined, undefined, - {scenes: {"1_0": {name: "Chill scene", state: {state: "ON"}}, "4_9": {state: {state: "OFF"}}}}, ), ], InterviewState.Successful, @@ -418,18 +403,34 @@ const bulb_color_2 = new Device( "Philips", "2019.09", "5.127.1.26581", + { + manuSpecificPhilips2: { + ID: 0xfc03, + manufacturerCode: 0x100b, + attributes: { + state: {ID: 0x0002, type: 0x41}, + }, + commands: { + multiColor: { + ID: 0, + parameters: [{name: 'data', type: 1008}], + }, + }, + commandsResponse: {}, + }, + }, ); -const bulb_color_3 = new Device( +const bulb_color_2 = new Device( "Router", - "0x000b57fffec6a5b5", - 401293, + "0x000b57fffec6a5b4", + 401292, 4107, [ new Endpoint( 1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096, 64515], [5, 25, 32, 4096], - "0x000b57fffec6a5b5", + "0x000b57fffec6a5b4", [], {lightingColorCtrl: {colorCapabilities: 254}}, [], @@ -636,7 +637,6 @@ export const devices = { bulb_2: bulb_2, hue_twilight, bulb_color_2: bulb_color_2, - bulb_color_3: bulb_color_3, remote: new Device( "EndDevice", "0x0017880104e45517", From 914fc8f191ef7edcbc438750af6a5e48ddb8f87e Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 03:51:00 +0200 Subject: [PATCH 08/16] Update bind.test.ts --- test/extensions/bind.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index b350d5137e..91b7275271 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -19,9 +19,6 @@ const mocksClear = [ devices.bulb_color.getEndpoint(1)!.configureReporting, devices.bulb_color.getEndpoint(1)!.bind, devices.bulb_color_2.getEndpoint(1)!.read, - //devices.bulb_color_3.getEndpoint(1)!.configureReporting, - //devices.bulb_color_3.getEndpoint(1)!.bind, - //devices.bulb_color_3.getEndpoint(1)!.read, ]; describe("Extension: Bind", () => { From 8408fa7a0f92a4c93492668efba20fbd3334354a Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 03:10:50 +0000 Subject: [PATCH 09/16] Set device options in tests --- test/extensions/bind.test.ts | 2 ++ test/mocks/zigbeeHerdsman.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 91b7275271..05eb230b4e 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -724,6 +724,7 @@ describe("Extension: Bind", () => { }); it("Should poll bounded Hue bulb when receiving message from Hue dimmer", async () => { + devices.bulb_color.meta = {options: {hue_native_control: true}}; const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; const payload = { @@ -737,6 +738,7 @@ describe("Extension: Bind", () => { }; await mockZHEvents.message(payload); await flushPromises(); + expect(devices.bulb_color.meta.options).toStrictEqual({hue_native_control: true}); expect(mockDebounce).toHaveBeenCalledTimes(1); expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 697ab29002..53ba23d864 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -428,7 +428,7 @@ const bulb_color_2 = new Device( [ new Endpoint( 1, - [0, 3, 4, 5, 6, 8, 768, 2821, 4096, 64515], + [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5b4", [], From 96edd2e40f0906ab5ea377bc7dd40d08565bb092 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 03:16:03 +0000 Subject: [PATCH 10/16] Add dedicated test case --- test/extensions/bind.test.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 05eb230b4e..d725ccd8e8 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -724,6 +724,25 @@ describe("Extension: Bind", () => { }); it("Should poll bounded Hue bulb when receiving message from Hue dimmer", async () => { + const remote = devices.remote; + const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; + const payload = { + data, + cluster: "manuSpecificPhilips", + device: remote, + endpoint: remote.getEndpoint(2)!, + type: "commandHueNotification", + linkquality: 10, + groupID: 0, + }; + await mockZHEvents.message(payload); + await flushPromises(); + expect(devices.bulb_color.meta).toStrictEqual({}); + expect(mockDebounce).toHaveBeenCalledTimes(1); + expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); + }); + + it("Should poll manuSpecificPhilips2 of bounded Hue bulb when receiving message from Hue dimmer", async () => { devices.bulb_color.meta = {options: {hue_native_control: true}}; const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; @@ -738,7 +757,7 @@ describe("Extension: Bind", () => { }; await mockZHEvents.message(payload); await flushPromises(); - expect(devices.bulb_color.meta.options).toStrictEqual({hue_native_control: true}); + expect(devices.bulb_color.meta).toStrictEqual({options: {hue_native_control: true}}); expect(mockDebounce).toHaveBeenCalledTimes(1); expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); From 7d775503b0965128c4d4724884778d0f4afe1f22 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 03:59:11 +0000 Subject: [PATCH 11/16] Move device option to endpoint --- lib/extension/bind.ts | 2 +- test/extensions/bind.test.ts | 9 ++++++--- test/mocks/zigbeeHerdsman.ts | 33 +++------------------------------ 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 8f43e88fe0..7baa7573ea 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -617,7 +617,7 @@ export default class Bind extends Extension { let readCluster: ClusterName = poll.read.cluster as ClusterName; // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead - if (data.device.zh.meta?.options?.hue_native_control === true) { + if (endpoint.meta?.options?.hue_native_control === true) { readCluster = "manuSpecificPhilips2" as ClusterName; readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; } else if (poll.read.attributesForEndpoint) { diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index d725ccd8e8..b0c7db8bde 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -12,6 +12,8 @@ import {Controller} from "../../lib/controller"; import Bind from "../../lib/extension/bind"; import * as settings from "../../lib/util/settings"; import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils"; +import { options } from "zigbee-herdsman-converters/lib/exposes"; +import { command_move_to_hue_and_saturation } from "zigbee-herdsman-converters/converters/fromZigbee"; const mocksClear = [ mockDebounce, @@ -724,6 +726,7 @@ describe("Extension: Bind", () => { }); it("Should poll bounded Hue bulb when receiving message from Hue dimmer", async () => { + devices.bulb_color.getEndpoint(1)!.meta = {options: {hue_native_control: false}} const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; const payload = { @@ -737,13 +740,13 @@ describe("Extension: Bind", () => { }; await mockZHEvents.message(payload); await flushPromises(); - expect(devices.bulb_color.meta).toStrictEqual({}); + //expect(devices.bulb_color.getEndpoint(1)!.meta.options).toBe(undefined); expect(mockDebounce).toHaveBeenCalledTimes(1); expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); }); it("Should poll manuSpecificPhilips2 of bounded Hue bulb when receiving message from Hue dimmer", async () => { - devices.bulb_color.meta = {options: {hue_native_control: true}}; + devices.bulb_color.getEndpoint(1)!.meta = {options: {hue_native_control: true}}; const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; const payload = { @@ -757,7 +760,7 @@ describe("Extension: Bind", () => { }; await mockZHEvents.message(payload); await flushPromises(); - expect(devices.bulb_color.meta).toStrictEqual({options: {hue_native_control: true}}); + expect(devices.bulb_color.getEndpoint(1)!.meta).toStrictEqual({options: {hue_native_control: true}}); expect(mockDebounce).toHaveBeenCalledTimes(1); expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("manuSpecificPhilips2", ["state"]); }); diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 53ba23d864..23298bcd97 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -385,40 +385,13 @@ const bulb_color = new Device( 40399, 4107, [ - new Endpoint( - 1, - [0, 3, 4, 5, 6, 8, 768, 2821, 4096, 64515], - [5, 25, 32, 4096], - "0x000b57fffec6a5b3", - [], - {lightingColorCtrl: {colorCapabilities: 254}}, - [], - undefined, - undefined, - ), + new Endpoint(1, [0, 3, 4, 5, 6, 8, 768, 2821, 4096], [5, 25, 32, 4096], "0x000b57fffec6a5b3", [], { + lightingColorCtrl: {colorCapabilities: 254}, + }), ], InterviewState.Successful, "Mains (single phase)", "LLC020", - "Philips", - "2019.09", - "5.127.1.26581", - { - manuSpecificPhilips2: { - ID: 0xfc03, - manufacturerCode: 0x100b, - attributes: { - state: {ID: 0x0002, type: 0x41}, - }, - commands: { - multiColor: { - ID: 0, - parameters: [{name: 'data', type: 1008}], - }, - }, - commandsResponse: {}, - }, - }, ); const bulb_color_2 = new Device( "Router", From ac098f6904fe8d8e3ad6478641715d6bd3e78d13 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 04:16:22 +0000 Subject: [PATCH 12/16] Fix format --- lib/extension/bind.ts | 2 +- test/extensions/bind.test.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 7baa7573ea..c20403a812 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -613,7 +613,7 @@ export default class Bind extends Extension { continue; } - let readAttrs = poll.read.attributes as TClusterAttributeKeys; + let readAttrs = poll.read.attributes as TClusterAttributeKeys; let readCluster: ClusterName = poll.read.cluster as ClusterName; // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index b0c7db8bde..aa9ff9709d 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -12,8 +12,6 @@ import {Controller} from "../../lib/controller"; import Bind from "../../lib/extension/bind"; import * as settings from "../../lib/util/settings"; import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils"; -import { options } from "zigbee-herdsman-converters/lib/exposes"; -import { command_move_to_hue_and_saturation } from "zigbee-herdsman-converters/converters/fromZigbee"; const mocksClear = [ mockDebounce, @@ -726,7 +724,7 @@ describe("Extension: Bind", () => { }); it("Should poll bounded Hue bulb when receiving message from Hue dimmer", async () => { - devices.bulb_color.getEndpoint(1)!.meta = {options: {hue_native_control: false}} + devices.bulb_color.getEndpoint(1)!.meta = {options: {hue_native_control: false}}; const remote = devices.remote; const data = {button: 3, unknown1: 3145728, type: 2, unknown2: 0, time: 1}; const payload = { @@ -740,7 +738,6 @@ describe("Extension: Bind", () => { }; await mockZHEvents.message(payload); await flushPromises(); - //expect(devices.bulb_color.getEndpoint(1)!.meta.options).toBe(undefined); expect(mockDebounce).toHaveBeenCalledTimes(1); expect(devices.bulb_color.getEndpoint(1)!.read).toHaveBeenCalledWith("genLevelCtrl", ["currentLevel"]); }); From 6d19931fb90d5451e0aa0a2fdc55d25daee3484f Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 06:25:11 +0200 Subject: [PATCH 13/16] Update comment --- lib/extension/bind.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index c20403a812..11f42fce26 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -616,7 +616,7 @@ export default class Bind extends Extension { let readAttrs = poll.read.attributes as TClusterAttributeKeys; let readCluster: ClusterName = poll.read.cluster as ClusterName; - // For devices supporting manuSpecificPhilips2 cluster, read state attribute from that cluster instead + // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead if (endpoint.meta?.options?.hue_native_control === true) { readCluster = "manuSpecificPhilips2" as ClusterName; readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; From 3fa10d2a56ac23be0bc16f0661c718af95ebda7e Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 16:14:03 +0000 Subject: [PATCH 14/16] Do not asset `readAttrs` type --- lib/extension/bind.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 11f42fce26..719d0a3377 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -613,7 +613,7 @@ export default class Bind extends Extension { continue; } - let readAttrs = poll.read.attributes as TClusterAttributeKeys; + let readAttrs: TClusterAttributeKeys = poll.read.attributes; let readCluster: ClusterName = poll.read.cluster as ClusterName; // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead From d85c437c9c941742be53d2d8743c3b1dc9cb8e7c Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 19:47:58 +0000 Subject: [PATCH 15/16] Implement suggestions --- lib/extension/bind.ts | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 719d0a3377..766d5b8170 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -613,21 +613,19 @@ export default class Bind extends Extension { continue; } - let readAttrs: TClusterAttributeKeys = poll.read.attributes; - let readCluster: ClusterName = poll.read.cluster as ClusterName; - - // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead - if (endpoint.meta?.options?.hue_native_control === true) { - readCluster = "manuSpecificPhilips2" as ClusterName; - readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; - } else if (poll.read.attributesForEndpoint) { - const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); - readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; - } - const key = `${device.ieeeAddr}_${endpoint.ID}_${POLL_ON_MESSAGE.indexOf(poll)}`; if (!this.pollDebouncers[key]) { + let readAttrs: TClusterAttributeKeys = poll.read.attributes; + let readCluster: ClusterName = poll.read.cluster; + // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead + if (endpoint.meta?.options?.hue_native_control === true) { + readCluster = "manuSpecificPhilips2" as ClusterName; + readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; + } else if (poll.read.attributesForEndpoint) { + const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); + readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; + } this.pollDebouncers[key] = debounce(async () => { try { await endpoint.read(readCluster, readAttrs); From 7b1d70e20b5529e4a5a0ae95fa19eea916dafc77 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Mon, 25 May 2026 20:17:46 +0000 Subject: [PATCH 16/16] Revent code block move --- lib/extension/bind.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index 766d5b8170..9b2f68280b 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -613,19 +613,20 @@ export default class Bind extends Extension { continue; } + let readAttrs: TClusterAttributeKeys = poll.read.attributes; + let readCluster: ClusterName = poll.read.cluster; + // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead + if (endpoint.meta?.options?.hue_native_control === true) { + readCluster = "manuSpecificPhilips2" as ClusterName; + readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; + } else if (poll.read.attributesForEndpoint) { + const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); + readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; + } + const key = `${device.ieeeAddr}_${endpoint.ID}_${POLL_ON_MESSAGE.indexOf(poll)}`; if (!this.pollDebouncers[key]) { - let readAttrs: TClusterAttributeKeys = poll.read.attributes; - let readCluster: ClusterName = poll.read.cluster; - // For devices that have hue_native_control enabled, read state attribute from manuSpecificPhilips2 cluster instead - if (endpoint.meta?.options?.hue_native_control === true) { - readCluster = "manuSpecificPhilips2" as ClusterName; - readAttrs = ["state"] as TClusterAttributeKeys<"manuSpecificPhilips2">; - } else if (poll.read.attributesForEndpoint) { - const attrsForEndpoint = await poll.read.attributesForEndpoint(endpoint); - readAttrs = [...poll.read.attributes, ...attrsForEndpoint]; - } this.pollDebouncers[key] = debounce(async () => { try { await endpoint.read(readCluster, readAttrs);