diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index f5764389e7..b62d950c96 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -3,6 +3,7 @@ import bind from "bind-decorator"; import debounce from "debounce"; import stringify from "json-stable-stringify-without-jsonify"; import {Zcl} from "zigbee-herdsman"; +import type {TClusterAttributeKeys} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types"; import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype"; import Device from "../model/device"; import Group from "../model/group"; @@ -43,53 +44,33 @@ const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemper }; }; -const REPORT_CLUSTERS: Readonly< - Partial< - Record< - ClusterName, - Readonly<{ - attribute: string; - minimumReportInterval: number; - maximumReportInterval: number; - reportableChange: number; - condition?: (endpoint: zh.Endpoint) => Promise; - }>[] - > - > -> = { - genOnOff: [{attribute: "onOff", ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}], - genLevelCtrl: [{attribute: "currentLevel", ...DEFAULT_REPORT_CONFIG}], +const REPORT_CLUSTERS = { + genOnOff: [{attribute: "onOff" as const, ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}], + genLevelCtrl: [{attribute: "currentLevel" as const, ...DEFAULT_REPORT_CONFIG}], lightingColorCtrl: [ { - attribute: "colorTemperature", + attribute: "colorTemperature" as const, ...DEFAULT_REPORT_CONFIG, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorTemperature, + condition: async (endpoint: zh.Endpoint): Promise => (await getColorCapabilities(endpoint)).colorTemperature, }, { - attribute: "currentX", + attribute: "currentX" as const, ...DEFAULT_REPORT_CONFIG, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, + condition: async (endpoint: zh.Endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, { - attribute: "currentY", + attribute: "currentY" as const, ...DEFAULT_REPORT_CONFIG, - condition: async (endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, + condition: async (endpoint: zh.Endpoint): Promise => (await getColorCapabilities(endpoint)).colorXY, }, ], closuresWindowCovering: [ - {attribute: "currentPositionLiftPercentage", ...DEFAULT_REPORT_CONFIG}, - {attribute: "currentPositionTiltPercentage", ...DEFAULT_REPORT_CONFIG}, + {attribute: "currentPositionLiftPercentage" as const, ...DEFAULT_REPORT_CONFIG}, + {attribute: "currentPositionTiltPercentage" as const, ...DEFAULT_REPORT_CONFIG}, ], }; -type PollOnMessage = { - cluster: Readonly>>; - read: Readonly<{cluster: string; attributes: string[]; attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise}>; - manufacturerIDs: readonly Zcl.ManufacturerCode[]; - manufacturerNames: readonly string[]; -}[]; - -const POLL_ON_MESSAGE: Readonly = [ +const POLL_ON_MESSAGE = [ { // On messages that have the cluster and type of below cluster: { @@ -109,7 +90,7 @@ const POLL_ON_MESSAGE: Readonly = [ genScenes: [{type: "commandRecall", data: {}}], }, // Read the following attributes - read: {cluster: "genLevelCtrl", attributes: ["currentLevel"]}, + 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, @@ -141,7 +122,7 @@ const POLL_ON_MESSAGE: Readonly = [ {type: "commandHueNotification", data: {button: 4}}, ], }, - read: {cluster: "genOnOff", attributes: ["onOff"]}, + read: {cluster: "genOnOff" as const, attributes: ["onOff"] as TClusterAttributeKeys<"genOnOff">}, manufacturerIDs: [ Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V, Zcl.ManufacturerCode.ATMEL, @@ -157,13 +138,13 @@ const POLL_ON_MESSAGE: Readonly = [ genScenes: [{type: "commandRecall", data: {}}], }, read: { - cluster: "lightingColorCtrl", - attributes: [] as string[], + cluster: "lightingColorCtrl" as const, + attributes: [] as TClusterAttributeKeys<"lightingColorCtrl">, // Since not all devices support the same attributes they need to be calculated dynamically // depending on the capabilities of the endpoint. - attributesForEndpoint: async (endpoint): Promise => { + attributesForEndpoint: async (endpoint: zh.Endpoint): Promise> => { const supportedAttrs = await getColorCapabilities(endpoint); - const readAttrs: string[] = []; + const readAttrs: TClusterAttributeKeys<"lightingColorCtrl"> = []; if (supportedAttrs.colorXY) { readAttrs.push("currentX", "currentY"); @@ -480,16 +461,15 @@ export default class Bind extends Extension { const items = []; // biome-ignore lint/style/noNonNullAssertion: valid from outer `if` - for (const c of REPORT_CLUSTERS[bind.cluster.name as ClusterName]!) { - if (!c.condition || (await c.condition(endpoint))) { - const i = {...c}; - delete i.condition; + for (const c of REPORT_CLUSTERS[bind.cluster.name as keyof typeof REPORT_CLUSTERS]!) { + if (!("condition" in c) || !c.condition || (await c.condition(endpoint))) { + const {attribute, minimumReportInterval, maximumReportInterval, reportableChange} = c; - items.push(i); + items.push({attribute, minimumReportInterval, maximumReportInterval, reportableChange}); } } - await endpoint.configureReporting(bind.cluster.name, items); + await endpoint.configureReporting(bind.cluster.name as keyof typeof REPORT_CLUSTERS, items); logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`); } catch (error) { logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}' (${(error as Error).message})`); @@ -539,16 +519,15 @@ export default class Bind extends Extension { const items = []; // biome-ignore lint/style/noNonNullAssertion: valid from loop (pushed to array only if in) - for (const item of REPORT_CLUSTERS[cluster as ClusterName]!) { - if (!item.condition || (await item.condition(endpoint))) { - const i = {...item}; - delete i.condition; + for (const item of REPORT_CLUSTERS[cluster as keyof typeof REPORT_CLUSTERS]!) { + if (!("condition" in item) || !item.condition || (await item.condition(endpoint))) { + const {attribute, minimumReportInterval, reportableChange} = item; - items.push({...i, maximumReportInterval: 0xffff}); + items.push({attribute, minimumReportInterval, maximumReportInterval: 0xffff, reportableChange}); } } - await endpoint.configureReporting(cluster, items); + await endpoint.configureReporting(cluster as keyof typeof REPORT_CLUSTERS, items); logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`); } catch (error) { logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}' (${(error as Error).message})`); @@ -569,7 +548,7 @@ export default class Bind extends Extension { * When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound). */ const polls = POLL_ON_MESSAGE.filter((p) => - p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)), + p.cluster[data.cluster as keyof (typeof p)["cluster"]]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)), ); if (polls.length) { diff --git a/package.json b/package.json index cce4683393..ef37a5803e 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "winston-syslog": "^2.7.1", "winston-transport": "^4.9.0", "ws": "^8.18.1", - "zigbee-herdsman": "6.0.0", - "zigbee-herdsman-converters": "25.2.0", + "zigbee-herdsman": "6.0.1", + "zigbee-herdsman-converters": "25.4.0", "zigbee2mqtt-frontend": "0.9.20", "zigbee2mqtt-windfront": "^1.8.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4fe3ca393..383e013a8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,7 +5,7 @@ settings: excludeLinksFromLockfile: false overrides: - zigbee-herdsman: 6.0.0 + zigbee-herdsman: 6.0.1 importers: @@ -75,11 +75,11 @@ importers: specifier: ^8.18.1 version: 8.18.3 zigbee-herdsman: - specifier: 6.0.0 - version: 6.0.0 + specifier: 6.0.1 + version: 6.0.1 zigbee-herdsman-converters: - specifier: 25.2.0 - version: 25.2.0 + specifier: 25.4.0 + version: 25.4.0 zigbee2mqtt-frontend: specifier: 0.9.20 version: 0.9.20 @@ -1604,11 +1604,11 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} - zigbee-herdsman-converters@25.2.0: - resolution: {integrity: sha512-JbQw/b1CfB65fIAOY5Bgee1PXP1k5/rOjNEvnVLjfQ5ghMv/KKuMj6EUT1LQKxAFYZ0v5km/qq+DSVdXWTfirA==} + zigbee-herdsman-converters@25.4.0: + resolution: {integrity: sha512-E6VBqz9f+Ycq2cpdGippU0dJOQODRkDC9nRUSEZCsxkHq+dC61jqYDHRL90748GDAlbcoLnpyZ/kKFhcZoQeSw==} - zigbee-herdsman@6.0.0: - resolution: {integrity: sha512-ZSM4BBXaK9qxmQqGewv1epJlFiCUpfSckHMknjfJou9hMOWuQjeo3OB5bo4XO4JaY+3Ue6RwIFRq833+wI8IZw==} + zigbee-herdsman@6.0.1: + resolution: {integrity: sha512-hZA+52nC8GHVKsSYE3qISmI3OC3SdXrV3NgB4TX23h3rmDz/KTSat45ejhROgXtK+vUYscsC3a3DpHscI/rKxQ==} zigbee-on-host@0.1.13: resolution: {integrity: sha512-mL7s9ic7J85YI7wOG1/QlbRYE3haFzIHqgnM2+xLmjj8CazjhmGjop9TCfuY2JXyDI9ss7bNCLxuI9n6M+TXug==} @@ -3039,16 +3039,16 @@ snapshots: yocto-queue@1.2.1: {} - zigbee-herdsman-converters@25.2.0: + zigbee-herdsman-converters@25.4.0: dependencies: buffer-crc32: 1.0.0 iconv-lite: 0.6.3 semver: 7.7.2 - zigbee-herdsman: 6.0.0 + zigbee-herdsman: 6.0.1 transitivePeerDependencies: - supports-color - zigbee-herdsman@6.0.0: + zigbee-herdsman@6.0.1: dependencies: '@serialport/bindings-cpp': 13.0.1 '@serialport/parser-delimiter': 13.0.0 diff --git a/test/extensions/publish.test.ts b/test/extensions/publish.test.ts index aafadddeb2..e496f86fb5 100644 --- a/test/extensions/publish.test.ts +++ b/test/extensions/publish.test.ts @@ -239,7 +239,7 @@ describe("Extension: Publish", () => { expect(endpoint.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 2}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/TS0601_switch", stringify({state_l2: "ON"}), {retain: false, qos: 0}); @@ -256,19 +256,19 @@ describe("Extension: Publish", () => { expect(endpoint.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [0], datatype: 4, dp: 1}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([0]), datatype: 4, dp: 1}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); expect(endpoint.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [1], datatype: 1, dp: 102}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 102}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); expect(endpoint.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([0]), datatype: 1, dp: 101}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( @@ -425,7 +425,7 @@ describe("Extension: Publish", () => { expect(group.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [1], datatype: 1, dp: 7}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 7}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); }); @@ -1074,7 +1074,7 @@ describe("Extension: Publish", () => { expect(endpoint.command.mock.calls[0]).toEqual([ "lightingColorCtrl", "enhancedMoveToHueAndSaturation", - {direction: 0, enhancehue: 45510, saturation: 127, transtime: 0}, + {enhancehue: 45510, saturation: 127, transtime: 0}, {}, ]); expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1); @@ -1442,7 +1442,7 @@ describe("Extension: Publish", () => { await mockMQTTEvents.message("zigbee2mqtt/roller_shutter/set", stringify({state: "OPEN"})); await flushPromises(); expect(endpoint.command).toHaveBeenCalledTimes(1); - expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: "255", transtime: 0}, {}); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {}); expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1); expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/roller_shutter"); expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({position: 100}); @@ -1458,7 +1458,7 @@ describe("Extension: Publish", () => { expect(endpoint.command).toHaveBeenCalledWith( "manuSpecificTuya", "dataRequest", - {dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)}, + {dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 3}], seq: expect.any(Number)}, {disableDefaultResponse: true}, ); expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);