From d542043095a7002c8431a48cc08c3135f96dd5fb Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 1/6] chore: Add Prettier Picks up the style enforcement that went away with eslint-config-athom. .prettierrc was already in the repo, unused, with no dependency and no script to run it. prettier-plugin-jsdoc is deliberately left out: it rewrites JSDoc type expressions into TypeScript syntax (Promise<{ a: number[]; b: boolean }>) that jsdoc 4 cannot parse, which breaks npm run build and with it the docs and deploy workflows. index.d.ts is ignored because it is generated, and AGENTS.md because Prettier mangles its M* emphasis markers. --- .prettierignore | 7 +++++++ .prettierrc | 3 +-- package-lock.json | 17 +++++++++++++++++ package.json | 5 ++++- 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..a3ed78cc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +build +index.d.ts +*.pdf +.claude + +# Literal formatting is the content here, and Prettier mangles the M* emphasis markers. +AGENTS.md diff --git a/.prettierrc b/.prettierrc index 77e64750..24e8f6ce 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,6 +2,5 @@ "semi": true, "trailingComma": "all", "singleQuote": true, - "printWidth": 100, - "plugins": ["prettier-plugin-jsdoc"] + "printWidth": 100 } diff --git a/package-lock.json b/package-lock.json index 231a7fc6..c56e9e8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "jsdoc": "^4.0.5", "jsdoc-ts-utils": "^6.0.0", "mocha": "^12.0.3", + "prettier": "^3.9.10", "serve": "^14.0.1", "sinon": "^19.0.2", "typescript": "^5.9.3", @@ -3645,6 +3646,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 7b71286f..45508627 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "serve": "concurrently \"serve build/\" \"npm run build:watch\"", "build": "jsdoc --configure ./docs/jsdoc.json", "build:clean": "rm -rf ./build", - "build:watch": "watch \"npm run build:clean && npm run build\" lib docs \"node_modules/@athombv/jsdoc-template\"" + "build:watch": "watch \"npm run build:clean && npm run build\" lib docs \"node_modules/@athombv/jsdoc-template\"", + "format:check": "prettier --check .", + "format:write": "prettier --write ." }, "repository": { "type": "git", @@ -46,6 +48,7 @@ "jsdoc": "^4.0.5", "jsdoc-ts-utils": "^6.0.0", "mocha": "^12.0.3", + "prettier": "^3.9.10", "serve": "^14.0.1", "sinon": "^19.0.2", "typescript": "^5.9.3", From 3af13a52030e61a10d4cb731ddfa6b78770bd06c Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 2/6] refactor: Move the M/O annotations above the attribute key // Mandatory, // Optional and // Conditional describe the attribute or command as a whole. On the opening brace Prettier pulls them inside it, where they read as if they belong to the id field. Above the key they survive the formatter and say what they mean. 77 annotations across five cluster files, no other change. --- lib/clusters/doorLock.js | 135 ++++++++++++++++++++----------- lib/clusters/metering.js | 18 +++-- lib/clusters/occupancySensing.js | 24 ++++-- lib/clusters/ota.js | 33 +++++--- lib/clusters/windowCovering.js | 21 +++-- 5 files changed, 154 insertions(+), 77 deletions(-) diff --git a/lib/clusters/doorLock.js b/lib/clusters/doorLock.js index 831fa88f..8335940f 100644 --- a/lib/clusters/doorLock.js +++ b/lib/clusters/doorLock.js @@ -5,7 +5,8 @@ const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { // Lock Information (0x0000 - 0x000F) - lockState: { // Mandatory + // Mandatory + lockState: { id: 0x0000, type: ZCLDataTypes.enum8({ notFullyLocked: 0, @@ -14,7 +15,8 @@ const ATTRIBUTES = { undefined: 255, }), }, - lockType: { // Mandatory + // Mandatory + lockType: { id: 0x0001, type: ZCLDataTypes.enum8({ deadBolt: 0, @@ -31,7 +33,8 @@ const ATTRIBUTES = { }), }, actuatorEnabled: { id: 0x0002, type: ZCLDataTypes.bool }, // Mandatory - doorState: { // Optional + // Optional + doorState: { id: 0x0003, type: ZCLDataTypes.enum8({ open: 0, @@ -51,11 +54,13 @@ const ATTRIBUTES = { numberOfTotalUsersSupported: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Optional numberOfPINUsersSupported: { id: 0x0012, type: ZCLDataTypes.uint16 }, // 18, Conditional¹ numberOfRFIDUsersSupported: { id: 0x0013, type: ZCLDataTypes.uint16 }, // 19, Conditional² - numberOfWeekDaySchedulesSupportedPerUser: { // Conditional³ + // Conditional³ + numberOfWeekDaySchedulesSupportedPerUser: { id: 0x0014, // 20 type: ZCLDataTypes.uint8, }, - numberOfYearDaySchedulesSupportedPerUser: { // Conditional⁴ + // Conditional⁴ + numberOfYearDaySchedulesSupportedPerUser: { id: 0x0015, // 21 type: ZCLDataTypes.uint8, }, @@ -76,7 +81,8 @@ const ATTRIBUTES = { ledSettings: { id: 0x0022, type: ZCLDataTypes.uint8 }, // 34, Optional autoRelockTime: { id: 0x0023, type: ZCLDataTypes.uint32 }, // 35, Optional soundVolume: { id: 0x0024, type: ZCLDataTypes.uint8 }, // 36, Optional - operatingMode: { // Optional + // Optional + operatingMode: { id: 0x0025, // 37 type: ZCLDataTypes.enum8({ normal: 0, @@ -86,7 +92,8 @@ const ATTRIBUTES = { passage: 4, }), }, - supportedOperatingModes: { // Mandatory + // Mandatory + supportedOperatingModes: { id: 0x0026, // 38 type: ZCLDataTypes.map16( 'normal', @@ -96,7 +103,8 @@ const ATTRIBUTES = { 'passage', ), }, - defaultConfigurationRegister: { // Optional + // Optional + defaultConfigurationRegister: { id: 0x0027, // 39 type: ZCLDataTypes.map16( 'enableLocalProgramming', @@ -119,7 +127,8 @@ const ATTRIBUTES = { userCodeTemporaryDisableTime: { id: 0x0031, type: ZCLDataTypes.uint8 }, // 49, Optional sendPINOverTheAir: { id: 0x0032, type: ZCLDataTypes.bool }, // 50, Optional requirePINforRFOperation: { id: 0x0033, type: ZCLDataTypes.bool }, // 51, Optional - securityLevel: { // Optional + // Optional + securityLevel: { id: 0x0034, // 52 type: ZCLDataTypes.enum8({ network: 0, @@ -128,7 +137,8 @@ const ATTRIBUTES = { }, // Alarm and Event Masks (0x0040 - 0x004F) - alarmMask: { // Optional + // Optional + alarmMask: { id: 0x0040, // 64 type: ZCLDataTypes.map16( 'deadboltJammed', @@ -140,7 +150,8 @@ const ATTRIBUTES = { 'forcedDoorOpenUnderDoorLockedCondition', ), }, - keypadOperationEventMask: { // Optional + // Optional + keypadOperationEventMask: { id: 0x0041, // 65 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificKeypadOperationEvent', @@ -153,7 +164,8 @@ const ATTRIBUTES = { 'nonAccessUserOperationEventSourceKeypad', ), }, - rfOperationEventMask: { // Optional + // Optional + rfOperationEventMask: { id: 0x0042, // 66 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificKeypadOperationEvent', @@ -165,7 +177,8 @@ const ATTRIBUTES = { 'unlockSourceRFErrorInvalidSchedule', ), }, - manualOperationEventMask: { // Optional + // Optional + manualOperationEventMask: { id: 0x0043, // 67 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificManualOperationEvent', @@ -181,7 +194,8 @@ const ATTRIBUTES = { 'manualUnlock', ), }, - rfidOperationEventMask: { // Optional + // Optional + rfidOperationEventMask: { id: 0x0044, // 68 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificKeypadOperationEvent', @@ -193,7 +207,8 @@ const ATTRIBUTES = { 'unlockSourceRFIDErrorInvalidSchedule', ), }, - keypadProgrammingEventMask: { // Optional + // Optional + keypadProgrammingEventMask: { id: 0x0045, // 69 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificKeypadProgrammingEvent', @@ -203,7 +218,8 @@ const ATTRIBUTES = { 'pinCodeChanged', ), }, - rfProgrammingEventMask: { // Optional + // Optional + rfProgrammingEventMask: { id: 0x0046, // 70 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificRFProgrammingEvent', @@ -215,7 +231,8 @@ const ATTRIBUTES = { 'rfidCodeDeleted', ), }, - rfidProgrammingEventMask: { // Optional + // Optional + rfidProgrammingEventMask: { id: 0x0047, // 71 type: ZCLDataTypes.map16( 'unknownOrManufacturerSpecificRFIDProgrammingEvent', @@ -254,7 +271,8 @@ const COMMANDS = { // --- Client to Server Commands --- // Lock/Unlock Commands - lockDoor: { // Mandatory + // Mandatory + lockDoor: { id: 0x0000, args: { pinCode: ZCLDataTypes.octstr, @@ -264,7 +282,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - unlockDoor: { // Mandatory + // Mandatory + unlockDoor: { id: 0x0001, args: { pinCode: ZCLDataTypes.octstr, @@ -274,7 +293,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - toggle: { // Optional + // Optional + toggle: { id: 0x0002, args: { pinCode: ZCLDataTypes.octstr, @@ -284,7 +304,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - unlockWithTimeout: { // Optional + // Optional + unlockWithTimeout: { id: 0x0003, args: { timeout: ZCLDataTypes.uint16, @@ -297,7 +318,8 @@ const COMMANDS = { }, // Logging Commands - getLogRecord: { // Optional + // Optional + getLogRecord: { id: 0x0004, args: { logIndex: ZCLDataTypes.uint16, @@ -317,7 +339,8 @@ const COMMANDS = { }, // PIN Code Commands - setPINCode: { // Optional + // Optional + setPINCode: { id: 0x0005, args: { userId: ZCLDataTypes.uint16, @@ -330,7 +353,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getPINCode: { // Optional + // Optional + getPINCode: { id: 0x0006, args: { userId: ZCLDataTypes.uint16, @@ -345,7 +369,8 @@ const COMMANDS = { }, }, }, - clearPINCode: { // Optional + // Optional + clearPINCode: { id: 0x0007, args: { userId: ZCLDataTypes.uint16, @@ -355,14 +380,16 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - clearAllPINCodes: { // Optional + // Optional + clearAllPINCodes: { id: 0x0008, response: { id: 0x0008, args: { status: ZCLDataTypes.uint8 }, }, }, - setUserStatus: { // Optional + // Optional + setUserStatus: { id: 0x0009, args: { userId: ZCLDataTypes.uint16, @@ -373,7 +400,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getUserStatus: { // Optional + // Optional + getUserStatus: { id: 0x000A, // 10 args: { userId: ZCLDataTypes.uint16, @@ -388,7 +416,8 @@ const COMMANDS = { }, // Schedule Commands - setWeekDaySchedule: { // Optional + // Optional + setWeekDaySchedule: { id: 0x000B, // 11 args: { scheduleId: ZCLDataTypes.uint8, @@ -404,7 +433,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getWeekDaySchedule: { // Optional + // Optional + getWeekDaySchedule: { id: 0x000C, // 12 args: { scheduleId: ZCLDataTypes.uint8, @@ -424,7 +454,8 @@ const COMMANDS = { }, }, }, - clearWeekDaySchedule: { // Optional + // Optional + clearWeekDaySchedule: { id: 0x000D, // 13 args: { scheduleId: ZCLDataTypes.uint8, @@ -435,7 +466,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - setYearDaySchedule: { // Optional + // Optional + setYearDaySchedule: { id: 0x000E, // 14 args: { scheduleId: ZCLDataTypes.uint8, @@ -448,7 +480,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getYearDaySchedule: { // Optional + // Optional + getYearDaySchedule: { id: 0x000F, // 15 args: { scheduleId: ZCLDataTypes.uint8, @@ -465,7 +498,8 @@ const COMMANDS = { }, }, }, - clearYearDaySchedule: { // Optional + // Optional + clearYearDaySchedule: { id: 0x0010, // 16 args: { scheduleId: ZCLDataTypes.uint8, @@ -476,7 +510,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - setHolidaySchedule: { // Optional + // Optional + setHolidaySchedule: { id: 0x0011, // 17 args: { holidayScheduleId: ZCLDataTypes.uint8, @@ -489,7 +524,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getHolidaySchedule: { // Optional + // Optional + getHolidaySchedule: { id: 0x0012, // 18 args: { holidayScheduleId: ZCLDataTypes.uint8, @@ -505,7 +541,8 @@ const COMMANDS = { }, }, }, - clearHolidaySchedule: { // Optional + // Optional + clearHolidaySchedule: { id: 0x0013, // 19 args: { holidayScheduleId: ZCLDataTypes.uint8, @@ -517,7 +554,8 @@ const COMMANDS = { }, // User Type Commands - setUserType: { // Optional + // Optional + setUserType: { id: 0x0014, // 20 args: { userId: ZCLDataTypes.uint16, @@ -528,7 +566,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getUserType: { // Optional + // Optional + getUserType: { id: 0x0015, // 21 args: { userId: ZCLDataTypes.uint16, @@ -543,7 +582,8 @@ const COMMANDS = { }, // RFID Code Commands - setRFIDCode: { // Optional + // Optional + setRFIDCode: { id: 0x0016, // 22 args: { userId: ZCLDataTypes.uint16, @@ -556,7 +596,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - getRFIDCode: { // Optional + // Optional + getRFIDCode: { id: 0x0017, // 23 args: { userId: ZCLDataTypes.uint16, @@ -571,7 +612,8 @@ const COMMANDS = { }, }, }, - clearRFIDCode: { // Optional + // Optional + clearRFIDCode: { id: 0x0018, // 24 args: { userId: ZCLDataTypes.uint16, @@ -581,7 +623,8 @@ const COMMANDS = { args: { status: ZCLDataTypes.uint8 }, }, }, - clearAllRFIDCodes: { // Optional + // Optional + clearAllRFIDCodes: { id: 0x0019, // 25 response: { id: 0x0019, // 25 @@ -591,7 +634,8 @@ const COMMANDS = { // --- Server to Client Commands --- - operationEventNotification: { // Optional + // Optional + operationEventNotification: { id: 0x0020, // 32 direction: Cluster.DIRECTION_SERVER_TO_CLIENT, args: { @@ -603,7 +647,8 @@ const COMMANDS = { data: ZCLDataTypes.octstr, }, }, - programmingEventNotification: { // Optional + // Optional + programmingEventNotification: { id: 0x0021, // 33 direction: Cluster.DIRECTION_SERVER_TO_CLIENT, args: { diff --git a/lib/clusters/metering.js b/lib/clusters/metering.js index 93ededa5..592bbd74 100644 --- a/lib/clusters/metering.js +++ b/lib/clusters/metering.js @@ -36,7 +36,8 @@ const ATTRIBUTES = { currentTier4SummationReceived: { id: 0x0107, type: ZCLDataTypes.uint48 }, // 263, Optional // Meter Status (0x0200 - 0x02FF) - status: { // 512, Optional + // 512, Optional + status: { id: 0x0200, type: ZCLDataTypes.map8( 'checkMeter', @@ -91,19 +92,23 @@ const ATTRIBUTES = { currentDayConsumptionReceived: { id: 0x0402, type: ZCLDataTypes.uint24 }, // 1026, Optional previousDayConsumptionDelivered: { id: 0x0403, type: ZCLDataTypes.uint24 }, // 1027, Optional previousDayConsumptionReceived: { id: 0x0404, type: ZCLDataTypes.uint24 }, // 1028, Optional - currentPartialProfileIntervalStartTimeDelivered: { // Optional + // Optional + currentPartialProfileIntervalStartTimeDelivered: { id: 0x0405, // 1029 type: ZCLDataTypes.uint32, }, - currentPartialProfileIntervalStartTimeReceived: { // Optional + // Optional + currentPartialProfileIntervalStartTimeReceived: { id: 0x0406, // 1030 type: ZCLDataTypes.uint32, }, - currentPartialProfileIntervalValueDelivered: { // Optional + // Optional + currentPartialProfileIntervalValueDelivered: { id: 0x0407, // 1031 type: ZCLDataTypes.uint24, }, - currentPartialProfileIntervalValueReceived: { // Optional + // Optional + currentPartialProfileIntervalValueReceived: { id: 0x0408, // 1032 type: ZCLDataTypes.uint24, }, @@ -260,7 +265,8 @@ const ATTRIBUTES = { // billReceivedTrailingDigit: { id: 0x0A14, type: ZCLDataTypes.map8() }, // 2580, Optional // Supply Control (0x0B00 - 0x0BFF) - proposedChangeSupplyImplementationTime: { // Optional + // Optional + proposedChangeSupplyImplementationTime: { id: 0x0B00, // 2816 type: ZCLDataTypes.uint32, }, diff --git a/lib/clusters/occupancySensing.js b/lib/clusters/occupancySensing.js index 9a0aecce..4baf6623 100644 --- a/lib/clusters/occupancySensing.js +++ b/lib/clusters/occupancySensing.js @@ -6,7 +6,8 @@ const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { // Occupancy Sensor Information (0x0000 - 0x000F) occupancy: { id: 0x0000, type: ZCLDataTypes.map8('occupied') }, // Mandatory - occupancySensorType: { // Mandatory + // Mandatory + occupancySensorType: { id: 0x0001, type: ZCLDataTypes.enum8({ pir: 0, @@ -15,7 +16,8 @@ const ATTRIBUTES = { physicalContact: 3, }), }, - occupancySensorTypeBitmap: { // Mandatory + // Mandatory + occupancySensorTypeBitmap: { id: 0x0002, type: ZCLDataTypes.map8('pir', 'ultrasonic', 'physicalContact'), }, @@ -27,30 +29,36 @@ const ATTRIBUTES = { // ¹ PIR sensor type supported // Ultrasonic Configuration (0x0020 - 0x002F) - ultrasonicOccupiedToUnoccupiedDelay: { // 32, Conditional² + // 32, Conditional² + ultrasonicOccupiedToUnoccupiedDelay: { id: 0x0020, type: ZCLDataTypes.uint16, }, - ultrasonicUnoccupiedToOccupiedDelay: { // 33, Conditional² + // 33, Conditional² + ultrasonicUnoccupiedToOccupiedDelay: { id: 0x0021, type: ZCLDataTypes.uint16, }, - ultrasonicUnoccupiedToOccupiedThreshold: { // 34, Conditional² + // 34, Conditional² + ultrasonicUnoccupiedToOccupiedThreshold: { id: 0x0022, type: ZCLDataTypes.uint8, }, // ² Ultrasonic sensor type supported // Physical Contact Configuration (0x0030 - 0x003F) - physicalContactOccupiedToUnoccupiedDelay: { // 48, Conditional³ + // 48, Conditional³ + physicalContactOccupiedToUnoccupiedDelay: { id: 0x0030, type: ZCLDataTypes.uint16, }, - physicalContactUnoccupiedToOccupiedDelay: { // 49, Conditional³ + // 49, Conditional³ + physicalContactUnoccupiedToOccupiedDelay: { id: 0x0031, type: ZCLDataTypes.uint16, }, - physicalContactUnoccupiedToOccupiedThreshold: { // 50, Conditional³ + // 50, Conditional³ + physicalContactUnoccupiedToOccupiedThreshold: { id: 0x0032, type: ZCLDataTypes.uint8, }, diff --git a/lib/clusters/ota.js b/lib/clusters/ota.js index b7d31f59..b13e3a95 100644 --- a/lib/clusters/ota.js +++ b/lib/clusters/ota.js @@ -37,7 +37,8 @@ const ATTRIBUTES = { // The upgrade status of the client device. The status indicates where the client // device is at in terms of the download and upgrade process. - imageUpgradeStatus: { // Mandatory + // Mandatory + imageUpgradeStatus: { id: 0x0006, type: ZCLDataTypes.enum8({ normal: 0x00, @@ -69,7 +70,8 @@ const ATTRIBUTES = { // Indicates what behavior the client device supports for activating a fully // downloaded but not installed upgrade image. - upgradeActivationPolicy: { // Optional + // Optional + upgradeActivationPolicy: { id: 0x000B, // 11 type: ZCLDataTypes.enum8({ otaServerActivationAllowed: 0x00, @@ -79,7 +81,8 @@ const ATTRIBUTES = { // Dictates the behavior of the device in situations where an explicit activation // command cannot be retrieved. - upgradeTimeoutPolicy: { // Optional + // Optional + upgradeTimeoutPolicy: { id: 0x000C, // 12 type: ZCLDataTypes.enum8({ applyUpgradeAfterTimeout: 0x00, @@ -128,7 +131,8 @@ const COMMANDS = { // The purpose of sending Image Notify command is so the server has a way to // notify client devices of when the OTA upgrade images are available for them. - imageNotify: { // Optional + // Optional + imageNotify: { id: 0x0000, direction: Cluster.DIRECTION_SERVER_TO_CLIENT, encodeMissingFieldsBehavior: 'skip', @@ -153,7 +157,8 @@ const COMMANDS = { // --- Client to Server Commands --- // Client queries the server if new OTA upgrade image is available. - queryNextImageRequest: { // Mandatory + // Mandatory + queryNextImageRequest: { id: 0x0001, encodeMissingFieldsBehavior: 'skip', args: { @@ -179,7 +184,8 @@ const COMMANDS = { }, // Client requests a block of data from the OTA upgrade image. - imageBlockRequest: { // Mandatory + // Mandatory + imageBlockRequest: { id: 0x0003, encodeMissingFieldsBehavior: 'skip', args: { @@ -203,7 +209,8 @@ const COMMANDS = { // Client requests pages of data from the OTA upgrade image. Using Image Page // Request reduces the number of requests sent from the client to the upgrade // server, compared to using Image Block Request command. - imagePageRequest: { // Optional + // Optional + imagePageRequest: { id: 0x0004, encodeMissingFieldsBehavior: 'skip', args: { @@ -221,7 +228,8 @@ const COMMANDS = { response: IMAGE_BLOCK_RESPONSE, }, - imageBlockResponse: { // Mandatory + // Mandatory + imageBlockResponse: { ...IMAGE_BLOCK_RESPONSE, direction: Cluster.DIRECTION_SERVER_TO_CLIENT, frameControl: ['directionToClient', 'clusterSpecific', 'disableDefaultResponse'], @@ -229,7 +237,8 @@ const COMMANDS = { // Sent by the client when it has completed downloading an image. The status // value SHALL be SUCCESS, INVALID_IMAGE, REQUIRE_MORE_IMAGE, or ABORT. - upgradeEndRequest: { // Mandatory + // Mandatory + upgradeEndRequest: { id: 0x0006, args: { status: ZCLDataTypes.enum8Status, @@ -244,7 +253,8 @@ const COMMANDS = { // Response to the Upgrade End Request, indicating when the client should upgrade // to the new image. - upgradeEndResponse: { // Mandatory + // Mandatory + upgradeEndResponse: { ...UPGRADE_END_RESPONSE, direction: Cluster.DIRECTION_SERVER_TO_CLIENT, frameControl: ['directionToClient', 'clusterSpecific', 'disableDefaultResponse'], @@ -252,7 +262,8 @@ const COMMANDS = { // Client requests a device specific file such as security credential, // configuration or log. - queryDeviceSpecificFileRequest: { // Optional + // Optional + queryDeviceSpecificFileRequest: { id: 0x0008, args: { requestNodeAddress: ZCLDataTypes.EUI64, diff --git a/lib/clusters/windowCovering.js b/lib/clusters/windowCovering.js index 3e88aa9a..572b4e52 100644 --- a/lib/clusters/windowCovering.js +++ b/lib/clusters/windowCovering.js @@ -5,7 +5,8 @@ const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { // Window Covering Information (0x0000 - 0x000F) - windowCoveringType: { // Mandatory + // Mandatory + windowCoveringType: { id: 0x0000, type: ZCLDataTypes.enum8({ rollershade: 0, @@ -26,7 +27,8 @@ const ATTRIBUTES = { currentPositionTilt: { id: 0x0004, type: ZCLDataTypes.uint16 }, // Optional numberofActuationsLift: { id: 0x0005, type: ZCLDataTypes.uint16 }, // Optional numberofActuationsTilt: { id: 0x0006, type: ZCLDataTypes.uint16 }, // Optional - configStatus: { // Mandatory + // Mandatory + configStatus: { id: 0x0007, type: ZCLDataTypes.map8( 'operational', @@ -51,7 +53,8 @@ const ATTRIBUTES = { velocityLift: { id: 0x0014, type: ZCLDataTypes.uint16 }, // 20, Optional accelerationTimeLift: { id: 0x0015, type: ZCLDataTypes.uint16 }, // 21, Optional decelerationTimeLift: { id: 0x0016, type: ZCLDataTypes.uint16 }, // 22, Optional - mode: { // Mandatory + // Mandatory + mode: { id: 0x0017, // 23 type: ZCLDataTypes.map8( 'motorDirectionReversed', @@ -69,25 +72,29 @@ const COMMANDS = { upOpen: { id: 0x0000 }, // Mandatory downClose: { id: 0x0001 }, // Mandatory stop: { id: 0x0002 }, // Mandatory - goToLiftValue: { // Optional + // Optional + goToLiftValue: { id: 0x0004, args: { liftValue: ZCLDataTypes.uint16, }, }, - goToLiftPercentage: { // Optional + // Optional + goToLiftPercentage: { id: 0x0005, args: { percentageLiftValue: ZCLDataTypes.uint8, }, }, - goToTiltValue: { // Optional + // Optional + goToTiltValue: { id: 0x0007, args: { tiltValue: ZCLDataTypes.uint16, }, }, - goToTiltPercentage: { // Optional + // Optional + goToTiltPercentage: { id: 0x0008, args: { percentageTiltValue: ZCLDataTypes.uint8, From 692f0e33923464d3dd79fd8f5ff3fd6c5d806ca6 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 3/6] chore: Apply Prettier Pure npm run format:write output, except for the prettier-ignore guard on the enum8Status table in lib/zclTypes.js, which keeps its column alignment. --- .github/workflows/deploy.yml | 5 +- .github/workflows/docs.yml | 40 +- .github/workflows/generate-types.yml | 14 +- .github/workflows/lint.yml | 1 - README.md | 31 +- docs/jsdoc.json | 5 +- index.js | 6 +- lib/BoundCluster.js | 234 +++++---- lib/Cluster.js | 353 +++++++++----- lib/Endpoint.js | 29 +- lib/Node.js | 9 +- lib/clusters/alarms.js | 2 - lib/clusters/analogInput.js | 7 +- lib/clusters/analogOutput.js | 7 +- lib/clusters/analogValue.js | 7 +- lib/clusters/ballastConfiguration.js | 3 - lib/clusters/basic.js | 2 - lib/clusters/binaryInput.js | 7 +- lib/clusters/binaryOutput.js | 7 +- lib/clusters/binaryValue.js | 7 +- lib/clusters/colorControl.js | 18 +- lib/clusters/dehumidificationControl.js | 5 +- lib/clusters/deviceTemperature.js | 7 +- lib/clusters/diagnostics.js | 2 - lib/clusters/doorLock.js | 60 ++- lib/clusters/electricalMeasurement.js | 33 +- lib/clusters/fanControl.js | 5 +- lib/clusters/flowMeasurement.js | 2 - lib/clusters/groups.js | 7 +- lib/clusters/iasACE.js | 5 +- lib/clusters/iasWD.js | 5 +- lib/clusters/iasZone.js | 15 +- lib/clusters/identify.js | 2 - lib/clusters/illuminanceLevelSensing.js | 2 - lib/clusters/illuminanceMeasurement.js | 2 - lib/clusters/index.js | 9 +- lib/clusters/levelControl.js | 2 - lib/clusters/metering.js | 126 +++-- lib/clusters/multistateInput.js | 7 +- lib/clusters/multistateOutput.js | 7 +- lib/clusters/multistateValue.js | 7 +- lib/clusters/occupancySensing.js | 2 - lib/clusters/onOff.js | 2 - lib/clusters/onOffSwitch.js | 2 - lib/clusters/ota.js | 14 +- lib/clusters/pollControl.js | 2 - lib/clusters/powerConfiguration.js | 2 - lib/clusters/powerProfile.js | 5 +- lib/clusters/pressureMeasurement.js | 2 - lib/clusters/pumpConfigurationAndControl.js | 5 +- lib/clusters/relativeHumidity.js | 2 - lib/clusters/scenes.js | 2 - lib/clusters/shadeConfiguration.js | 5 +- lib/clusters/temperatureMeasurement.js | 2 - lib/clusters/thermostat.js | 12 +- .../thermostatUserInterfaceConfiguration.js | 2 - lib/clusters/time.js | 7 +- lib/clusters/touchlink.js | 12 +- lib/clusters/windowCovering.js | 2 - lib/util/index.js | 8 +- lib/zclFrames.js | 455 ++++++++++-------- lib/zclTypes.js | 1 + scripts/generate-types.mts | 339 +++++++------ test/attributeReport.js | 16 +- test/boundClusterAttributeStatus.js | 180 +++---- test/colorControl.js | 146 +++--- test/defaultResponse.js | 27 +- test/doorLock.js | 154 +++--- test/endpointStandalone.js | 8 +- test/iasZone.js | 71 +-- test/levelControl.js | 83 ++-- test/manual/xiaomiHacks.js | 103 ++-- test/onOff.js | 110 +++-- test/ota.js | 149 +++--- test/testClusterResponseTimeout.js | 237 ++++----- test/testConfigureReportingRecordsStruct.js | 28 +- test/testMfgSpecificCommands.js | 321 ++++++------ test/testNode.js | 433 +++++++++-------- test/testTrxSequenceNumber.js | 21 +- test/testZCLError.js | 220 +++++---- test/util/mockNode.js | 268 ++++++----- tsconfig.json | 2 +- 82 files changed, 2493 insertions(+), 2073 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d375a26..0a5e925c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,8 +35,8 @@ jobs: # Only run this job if initiator is not the Homey Github Actions Bot to prevent loops and check if if a version bump is provided and current branch is production or testing if: github.actor != 'homey-bot' - && (github.ref == 'refs/heads/production' || github.ref == 'refs/heads/testing') - && (github.event.inputs.versionBumpType == 'patch' || github.event.inputs.versionBumpType == 'minor' || github.event.inputs.versionBumpType == 'major' || contains(github.event.head_commit.message, '#patch') || contains(github.event.head_commit.message, '#minor') || contains(github.event.head_commit.message, '#major')) + && (github.ref == 'refs/heads/production' || github.ref == 'refs/heads/testing') + && (github.event.inputs.versionBumpType == 'patch' || github.event.inputs.versionBumpType == 'minor' || github.event.inputs.versionBumpType == 'major' || contains(github.event.head_commit.message, '#patch') || contains(github.event.head_commit.message, '#minor') || contains(github.event.head_commit.message, '#major')) runs-on: ubuntu-latest @@ -47,7 +47,6 @@ jobs: id-token: write steps: - # Checks out the current repository. - name: Checkout git repository uses: actions/checkout@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 67806c8b..ecf72bdd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ name: Deploy Documentation To GitHub Pages on: push: - branches: [ production ] + branches: [production] workflow_dispatch: jobs: @@ -27,22 +27,22 @@ jobs: name: Build & Deploy to GitHub Pages runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc - - # Build - - name: Build - run: | - npm ci - npm run build - - # Deploy - - name: Deploy To GitHub Pages - uses: peaceiris/actions-gh-pages@v3.8.0 - with: - personal_token: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} - publish_dir: ./build \ No newline at end of file + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + # Build + - name: Build + run: | + npm ci + npm run build + + # Deploy + - name: Deploy To GitHub Pages + uses: peaceiris/actions-gh-pages@v3.8.0 + with: + personal_token: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} + publish_dir: ./build diff --git a/.github/workflows/generate-types.yml b/.github/workflows/generate-types.yml index e37e92af..7b6149fc 100644 --- a/.github/workflows/generate-types.yml +++ b/.github/workflows/generate-types.yml @@ -6,12 +6,12 @@ on: - production - testing paths: - - "lib/clusters/**/*.js" - - "scripts/generate-types.mts" - - "scripts/template.d.ts.txt" - - "index.js" - - "lib/Cluster.js" - - "lib/constants.js" + - 'lib/clusters/**/*.js' + - 'scripts/generate-types.mts' + - 'scripts/template.d.ts.txt' + - 'index.js' + - 'lib/Cluster.js' + - 'lib/constants.js' workflow_dispatch: jobs: @@ -32,7 +32,7 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: .nvmrc - cache: "npm" + cache: 'npm' - name: Install dependencies run: npm ci diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea438fc9..6759d2c2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,6 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - # Checks out the current repository. - uses: actions/checkout@v4 diff --git a/README.md b/README.md index c9284e81..0a8a5ad9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ v2.0.0 - Changed `Cluster.readAttributes` signature, attributes must now be specified as an array of strings. ```js -zclNode.endpoints[1].clusters.basic.readAttributes(["modelId", "manufacturerName"]); +zclNode.endpoints[1].clusters.basic.readAttributes(['modelId', 'manufacturerName']); ``` ## Release @@ -70,8 +70,8 @@ In order to communicate with a Zigbee node retrieve a `node` instance from `Mana `/drivers/my-driver/device.js` ```js -const Homey = require("homey"); -const { ZCLNode, CLUSTER } = require("zigbee-clusters"); +const Homey = require('homey'); +const { ZCLNode, CLUSTER } = require('zigbee-clusters'); class MyDevice extends Homey.Device { onInit() { @@ -101,7 +101,7 @@ class MyDevice extends Homey.Device { // timeout (10000ms) before the command is considered rejected. // The timeout starts after sending the frame and receiving a low-level ack. timeout: 5000, - } + }, ); }); } @@ -113,8 +113,8 @@ class MyDevice extends Homey.Device { `/drivers/my-driver/device.js` ```js -const Homey = require("homey"); -const { ZCLNode, CLUSTER } = require("zigbee-clusters"); +const Homey = require('homey'); +const { ZCLNode, CLUSTER } = require('zigbee-clusters'); class MyDevice extends Homey.Device { onInit() { @@ -134,10 +134,10 @@ class MyDevice extends Homey.Device { // And listen for incoming attribute reports by binding a listener on the cluster zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].on( - "attr.currentSaturation", + 'attr.currentSaturation', (currentSaturation) => { // handle reported attribute value - } + }, ); }); } @@ -181,7 +181,7 @@ class OnOffCluster extends Cluster { } static get NAME() { - return "onOff"; // The cluster name + return 'onOff'; // The cluster name } static get ATTRIBUTES() { @@ -214,7 +214,7 @@ Zigbee nodes can send commands to Homey via bound clusters. This requires a bind `/lib/LevelControlBoundCluster.js` ```js -const { BoundCluster } = require("zigbee-clusters"); +const { BoundCluster } = require('zigbee-clusters'); class LevelControlBoundCluster extends BoundCluster { constructor({ onMove }) { @@ -237,7 +237,7 @@ module.exports = LevelControlBoundCluster; `/drivers/my-driver/device.js` ```js -const LevelControlBoundCluster = require("../../lib/LevelControlBoundCluster"); +const LevelControlBoundCluster = require('../../lib/LevelControlBoundCluster'); // Register the `BoundCluster` implementation with the `ZCLNode` zclNode.endpoints[1].bind( @@ -246,7 +246,7 @@ zclNode.endpoints[1].bind( onMove: (payload) => { // Do something with the received payload }, - }) + }), ); ``` @@ -257,7 +257,7 @@ There are cases where it is required to implement a custom cluster, for example `lib/IkeaSpecificSceneCluster.js` ```js -const { ScenesCluster, ZCLDataTypes } = require("zigbee-clusters"); +const { ScenesCluster, ZCLDataTypes } = require('zigbee-clusters'); class IkeaSpecificSceneCluster extends ScenesCluster { // Here we override the `COMMANDS` getter from the `ScenesClusters` by @@ -299,14 +299,14 @@ module.exports = IkeaSpecificSceneCluster; `/drivers/my-driver/device.js` ```js -const IkeaSpecificSceneCluster = require("../../lib/IkeaSpecificSceneCluster"); +const IkeaSpecificSceneCluster = require('../../lib/IkeaSpecificSceneCluster'); // Important: we have created a new `Cluster` instance which needs to be added before // it becomes available on any `ZCLNode` instance. Cluster.addCluster(IkeaSpecificSceneCluster); // Example invocation of custom cluster command -zclNode.endpoints[1].clusters["scenes"].ikeaSceneMove({ mode: 0, transitionTime: 10 }); +zclNode.endpoints[1].clusters['scenes'].ikeaSceneMove({ mode: 0, transitionTime: 10 }); ``` This also works for `BoundClusters`, if a node sends commands to Homey using a custom cluster it is necessary to implement a custom `BoundCluster` and bind it to the `ZCLNode` instance. For an example check the implementation in the `com.ikea.tradfri` driver [remote_control](https://github.com/athombv/com.ikea.tradfri-example/tree/master/drivers/remote_control/device.js). @@ -328,6 +328,7 @@ This runs `scripts/generate-types.js` which loads all cluster modules and genera ### Automatic Generation (GitHub Actions) TypeScript types are automatically regenerated when changes are pushed to the `develop` branch that affect: + - `lib/clusters/**/*.js` - cluster definitions - `scripts/generate-types.js` - the generator script diff --git a/docs/jsdoc.json b/docs/jsdoc.json index 32fedb29..803baca7 100755 --- a/docs/jsdoc.json +++ b/docs/jsdoc.json @@ -9,10 +9,7 @@ "readme": "./README.md", "destination": "./build" }, - "plugins": [ - "jsdoc-ts-utils", - "plugins/markdown" - ], + "plugins": ["jsdoc-ts-utils", "plugins/markdown"], "templates": { "default": { "outputSourceFiles": false diff --git a/index.js b/index.js index b02760de..048c3315 100644 --- a/index.js +++ b/index.js @@ -39,11 +39,7 @@ function debug(flag = true, namespaces = '*') { _debug.disable(); } -const { - ZCLDataTypes, - ZCLDataType, - ZCLStruct, -} = zclTypes; +const { ZCLDataTypes, ZCLDataType, ZCLStruct } = zclTypes; const { ZIGBEE_PROFILE_ID, ZIGBEE_DEVICE_ID, IAS_ZONE_TYPE } = require('./lib/constants'); diff --git a/lib/BoundCluster.js b/lib/BoundCluster.js index 9af80b27..63031bd7 100644 --- a/lib/BoundCluster.js +++ b/lib/BoundCluster.js @@ -25,7 +25,6 @@ debug = debug.extend('bound-cluster'); * zclNode.endpoints[1].bind('onOff', new MyBoundCluster()); */ class BoundCluster { - /** * Create a new BoundCluster instance. */ @@ -52,43 +51,42 @@ class BoundCluster { async readAttributes({ attributes }) { debug(this.logId, 'received read attributes command'); const result = Buffer.alloc(255); - const attributeMap = attributes - .map(aId => { - const attr = this.cluster.attributesById[aId]; - try { - if (!attr) { - throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); - } - const value = this[attr.name]; - if (typeof value === 'undefined') { - throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); - } - attr.type.toBuffer(result, value, 0); + const attributeMap = attributes.map((aId) => { + const attr = this.cluster.attributesById[aId]; + try { + if (!attr) { + throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); + } + const value = this[attr.name]; + if (typeof value === 'undefined') { + throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); + } + attr.type.toBuffer(result, value, 0); + return { + id: aId, + status: 'SUCCESS', + value, + }; + } catch (e) { + // A ZCLError names the status to report for this one attribute. Thrown just above for an + // attribute that does not exist here, but a getter may throw one too - honour that rather + // than flattening it to FAILURE, so an implementation can pick its own per-attribute + // status. + if (e instanceof ZCLError) { + debug(this.logId, 'Attribute rejected:', attr ? attr.name || aId : aId, e.zclStatus); return { id: aId, - status: 'SUCCESS', - value, + status: e.zclStatus, }; - } catch (e) { - // A ZCLError names the status to report for this one attribute. Thrown just above for an - // attribute that does not exist here, but a getter may throw one too - honour that rather - // than flattening it to FAILURE, so an implementation can pick its own per-attribute - // status. - if (e instanceof ZCLError) { - debug(this.logId, 'Attribute rejected:', attr ? attr.name || aId : aId, e.zclStatus); - return { - id: aId, - status: e.zclStatus, - }; - } - debug(this.logId, 'Failed to parse attribute:', attr ? attr.name || aId : aId, e.message); } + debug(this.logId, 'Failed to parse attribute:', attr ? attr.name || aId : aId, e.message); + } - return { - id: aId, - status: 'FAILURE', - }; - }); + return { + id: aId, + status: 'FAILURE', + }; + }); const len = this.cluster.attributeArrayStatusDataType.toBuffer(result, attributeMap, 0); const attributesResult = { attributes: result.slice(0, len) }; @@ -108,49 +106,58 @@ class BoundCluster { debug(this.logId, 'received write attributes command', attributes); attributes = this.cluster.attributeArrayDataType.fromBuffer(attributes, 0); - const attributeMap = attributes - .map(attrValue => { - const attr = this.cluster.attributesById[attrValue.id]; - try { - if (!attr) { - throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); - } - if (typeof attrValue.value === 'undefined') { - throw new Error('not_parsable'); - } - const descriptor = getPropertyDescriptor(this, attr.name); - if (!descriptor) { - // Neither a getter nor a setter: this device does not implement the attribute at all. - throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); - } - if (!descriptor.set) { - throw new ZCLError('READ_ONLY'); - } - - this[attr.name] = attrValue.value; + const attributeMap = attributes.map((attrValue) => { + const attr = this.cluster.attributesById[attrValue.id]; + try { + if (!attr) { + throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); + } + if (typeof attrValue.value === 'undefined') { + throw new Error('not_parsable'); + } + const descriptor = getPropertyDescriptor(this, attr.name); + if (!descriptor) { + // Neither a getter nor a setter: this device does not implement the attribute at all. + throw new ZCLError('UNSUPPORTED_ATTRIBUTE'); + } + if (!descriptor.set) { + throw new ZCLError('READ_ONLY'); + } + + this[attr.name] = attrValue.value; + return { + id: attrValue.id, + status: 'SUCCESS', + }; + } catch (e) { + // As in readAttributes: a ZCLError carries the status for this record, whether it came + // from the checks above or from a setter that wants to reject the write on its own terms. + if (e instanceof ZCLError) { + debug( + this.logId, + 'Rejected write to attribute:', + attr ? attr.name || attrValue.id : attrValue.id, + e.zclStatus, + ); return { id: attrValue.id, - status: 'SUCCESS', + status: e.zclStatus, }; - } catch (e) { - // As in readAttributes: a ZCLError carries the status for this record, whether it came - // from the checks above or from a setter that wants to reject the write on its own terms. - if (e instanceof ZCLError) { - debug(this.logId, 'Rejected write to attribute:', attr ? attr.name || attrValue.id : attrValue.id, e.zclStatus); - return { - id: attrValue.id, - status: e.zclStatus, - }; - } - debug(this.logId, 'Error: failed to parse attribute:', attr ? attr.name || attrValue.id : attrValue.id, e.message); } + debug( + this.logId, + 'Error: failed to parse attribute:', + attr ? attr.name || attrValue.id : attrValue.id, + e.message, + ); + } - return { - id: attrValue.id, - status: 'FAILURE', - }; - }); + return { + id: attrValue.id, + status: 'FAILURE', + }; + }); return { attributes: attributeMap }; } @@ -167,11 +174,12 @@ class BoundCluster { async discoverCommandsReceived({ startValue = 0, maxResults = 250 } = {}) { debug(this.logId, 'received discover commands received command'); - const cmds = [].concat(...Object.values(this.cluster.commandsById)) - .filter(c => !c.global && !c.isResponse && this[c.name]) - .map(c => c.id) + const cmds = [] + .concat(...Object.values(this.cluster.commandsById)) + .filter((c) => !c.global && !c.isResponse && this[c.name]) + .map((c) => c.id) .sort() - .filter(cId => cId >= startValue); + .filter((cId) => cId >= startValue); const result = cmds.slice(0, maxResults); const response = { @@ -194,11 +202,12 @@ class BoundCluster { async discoverCommandsGenerated({ startValue = 0, maxResults = 250 } = {}) { debug(this.logId, 'received discover commands generated command'); - const cmds = [].concat(...Object.values(this.cluster.commandsById)) - .filter(c => !c.global && c.response && this[c.name]) - .map(c => c.response.id) + const cmds = [] + .concat(...Object.values(this.cluster.commandsById)) + .filter((c) => !c.global && c.response && this[c.name]) + .map((c) => c.response.id) .sort() - .filter(cId => cId >= startValue); + .filter((cId) => cId >= startValue); const result = cmds.slice(0, maxResults); const response = { @@ -234,12 +243,13 @@ class BoundCluster { * attributes and lastResponse true, whatever the device asked for. */ async discoverAttributes({ startValue: _startValue = 0, maxResults: _maxResults = 250 } = {}) { - const attributes = Object.values(this.cluster.attributesById) - .filter(attr => attr.type instanceof ZCLDataType && getPropertyDescriptor(this, attr.name)); + const attributes = Object.values(this.cluster.attributesById).filter( + (attr) => attr.type instanceof ZCLDataType && getPropertyDescriptor(this, attr.name), + ); const response = { lastResponse: true, - attributes: attributes.map(a => ({ id: a.id, dataTypeId: a.type.id })), + attributes: attributes.map((a) => ({ id: a.id, dataTypeId: a.type.id })), }; debug(this.logId, 'received discover attributes command, response:', response); return response; @@ -270,16 +280,20 @@ class BoundCluster { * TODO: paginate. startValue and maxResults are ignored, so every call answers with all * attributes and lastResponse true, whatever the device asked for. */ - async discoverAttributesExtended({ startValue: _startValue = 0, maxResults: _maxResults = 250 } = {}) { - const attributes = Object.values(this.cluster.attributesById) - .filter(attr => attr.type instanceof ZCLDataType && getPropertyDescriptor(this, attr.name)); + async discoverAttributesExtended({ + startValue: _startValue = 0, + maxResults: _maxResults = 250, + } = {}) { + const attributes = Object.values(this.cluster.attributesById).filter( + (attr) => attr.type instanceof ZCLDataType && getPropertyDescriptor(this, attr.name), + ); let { REPORTABLE_ATTRIBUTES } = this; if (!Array.isArray(REPORTABLE_ATTRIBUTES)) REPORTABLE_ATTRIBUTES = []; const response = { lastResponse: true, - attributes: attributes.map(a => { + attributes: attributes.map((a) => { const acl = []; const prop = getPropertyDescriptor(this, a.name); @@ -315,11 +329,14 @@ class BoundCluster { async handleFrame(frame, meta, rawFrame) { const commands = this.cluster.commandsById[frame.cmdId] || []; - let filteredCommands = commands - .filter(cmd => frame.frameControl.clusterSpecific === !cmd.global - && (cmd.global || frame.frameControl.manufacturerSpecific === !!cmd.manufacturerId) - && (cmd.global || !frame.frameControl.manufacturerSpecific - || frame.manufacturerId === cmd.manufacturerId)); + let filteredCommands = commands.filter( + (cmd) => + frame.frameControl.clusterSpecific === !cmd.global && + (cmd.global || frame.frameControl.manufacturerSpecific === !!cmd.manufacturerId) && + (cmd.global || + !frame.frameControl.manufacturerSpecific || + frame.manufacturerId === cmd.manufacturerId), + ); // Try to filter based on frame direction, note: this is optional as a cluster command // does not always have a 'direction' property. The filter ensure that multiple commands @@ -327,13 +344,15 @@ class BoundCluster { // to both command definitions (see iasZone.js as an example). Cluster.js only receives // frames with directionToClient=true. BoundCluster.js receives frames with // directionToClient=false. - filteredCommands = filteredCommands.filter(command => { + filteredCommands = filteredCommands.filter((command) => { // Only filter commands that have a direction string property if (typeof command.direction === 'string') { // Filter out commands marked as DIRECTION_SERVER_TO_CLIENT when // frameControl.directionToClient = false - if (frame.frameControl.directionToClient === false - && command.direction === Cluster.DIRECTION_SERVER_TO_CLIENT) { + if ( + frame.frameControl.directionToClient === false && + command.direction === Cluster.DIRECTION_SERVER_TO_CLIENT + ) { return false; } } @@ -345,9 +364,7 @@ class BoundCluster { .pop(); if (command) { - const args = command.args - ? command.args.fromBuffer(frame.data, 0) - : undefined; + const args = command.args ? command.args.fromBuffer(frame.data, 0) : undefined; if (this[command.name]) { debug(this.logId, 'received command', command.name, args); @@ -411,10 +428,12 @@ class BoundCluster { // TODO: implement when needed async readAttributesStructured({ - attributes: _attributes = [{ - // attributeId, - // indexPath, - }], + attributes: _attributes = [ + { + // attributeId, + // indexPath, + }, + ], }) { throw new Error('not_implemented'); // return { attributes }; @@ -422,17 +441,18 @@ class BoundCluster { // TODO: implement when needed async writeAttributesStructured({ - attributes: _attributes = [{ - // attributeId, - // indexPath, - // dataTypeId, - // value, - }], + attributes: _attributes = [ + { + // attributeId, + // indexPath, + // dataTypeId, + // value, + }, + ], }) { throw new Error('not_implemented'); // return { attributes }; } - } module.exports = BoundCluster; diff --git a/lib/Cluster.js b/lib/Cluster.js index f3f8ea06..2d8a52e1 100644 --- a/lib/Cluster.js +++ b/lib/Cluster.js @@ -8,7 +8,10 @@ const { getLogId, ZCLError } = require('./util'); debug = debug.extend('cluster'); const { - ZCLStandardHeader, ZCLMfgSpecificHeader, ZCLAttributeDataRecord, ZCLConfigureReportingRecords, + ZCLStandardHeader, + ZCLMfgSpecificHeader, + ZCLAttributeDataRecord, + ZCLConfigureReportingRecords, } = require('./zclFrames'); const { ZCLStruct, ZCLDataTypes } = require('./zclTypes'); @@ -46,10 +49,12 @@ const GLOBAL_COMMANDS = { response: { id: 0x04, args: { - attributes: ZCLDataTypes.Array0(ZCLStruct('AttributeResponse', { - status: ZCLDataTypes.enum8Status, - id: ZCLDataTypes.uint16, - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('AttributeResponse', { + status: ZCLDataTypes.enum8Status, + id: ZCLDataTypes.uint16, + }), + ), }, }, global: true, @@ -63,10 +68,12 @@ const GLOBAL_COMMANDS = { response: { id: 0x04, args: { - attributes: ZCLDataTypes.Array0(ZCLStruct('AttributeResponse', { - status: ZCLDataTypes.enum8Status, - id: ZCLDataTypes.uint16, - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('AttributeResponse', { + status: ZCLDataTypes.enum8Status, + id: ZCLDataTypes.uint16, + }), + ), }, }, global: true, @@ -88,14 +95,16 @@ const GLOBAL_COMMANDS = { response: { id: 0x07, args: { - reports: ZCLDataTypes.Array0(ZCLStruct('ConfigureReportingResponse', { - status: ZCLDataTypes.enum8Status, - direction: ZCLDataTypes.enum8({ - reported: 0, - received: 1, + reports: ZCLDataTypes.Array0( + ZCLStruct('ConfigureReportingResponse', { + status: ZCLDataTypes.enum8Status, + direction: ZCLDataTypes.enum8({ + reported: 0, + received: 1, + }), + attributeId: ZCLDataTypes.uint16, }), - attributeId: ZCLDataTypes.uint16, - })), + ), }, }, global: true, @@ -104,13 +113,15 @@ const GLOBAL_COMMANDS = { readReportingConfiguration: { id: 0x08, args: { - attributes: ZCLDataTypes.Array0(ZCLStruct('ReadReportingConfiguration', { - direction: ZCLDataTypes.enum8({ - reported: 0, - received: 1, + attributes: ZCLDataTypes.Array0( + ZCLStruct('ReadReportingConfiguration', { + direction: ZCLDataTypes.enum8({ + reported: 0, + received: 1, + }), + attributeId: ZCLDataTypes.uint16, }), - attributeId: ZCLDataTypes.uint16, - })), + ), }, response: { id: 0x09, @@ -122,7 +133,7 @@ const GLOBAL_COMMANDS = { }, reportAttributes: { - id: 0x0A, + id: 0x0a, args: { attributes: ZCLDataTypes.buffer, }, @@ -130,7 +141,7 @@ const GLOBAL_COMMANDS = { }, defaultResponse: { - id: 0x0B, + id: 0x0b, args: { cmdId: ZCLDataTypes.uint8, status: ZCLDataTypes.enum8Status, @@ -139,31 +150,35 @@ const GLOBAL_COMMANDS = { }, discoverAttributes: { - id: 0x0C, + id: 0x0c, args: { startValue: ZCLDataTypes.uint16, maxResults: ZCLDataTypes.uint8, }, response: { - id: 0x0D, + id: 0x0d, args: { lastResponse: ZCLDataTypes.bool, - attributes: ZCLDataTypes.Array0(ZCLStruct('DiscoveredAttribute', { - id: ZCLDataTypes.uint16, - dataTypeId: ZCLDataTypes.uint8, - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('DiscoveredAttribute', { + id: ZCLDataTypes.uint16, + dataTypeId: ZCLDataTypes.uint8, + }), + ), }, }, global: true, }, readAttributesStructured: { - id: 0x0E, + id: 0x0e, args: { - attributes: ZCLDataTypes.Array0(ZCLStruct('AttributeSelector', { - attributeId: ZCLDataTypes.uint16, - indexPath: ZCLDataTypes.Array8(ZCLDataTypes.uint16), - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('AttributeSelector', { + attributeId: ZCLDataTypes.uint16, + indexPath: ZCLDataTypes.Array8(ZCLDataTypes.uint16), + }), + ), }, response: { id: 0x01, @@ -175,14 +190,16 @@ const GLOBAL_COMMANDS = { }, writeAttributesStructured: { - id: 0x0F, + id: 0x0f, args: { - attributes: ZCLDataTypes.Array0(ZCLStruct('AttributeSelector', { - attributeId: ZCLDataTypes.uint16, - indexPath: ZCLDataTypes.Array8(ZCLDataTypes.uint16), - dataTypeId: ZCLDataTypes.uint8, - value: ZCLDataTypes.buffer, - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('AttributeSelector', { + attributeId: ZCLDataTypes.uint16, + indexPath: ZCLDataTypes.Array8(ZCLDataTypes.uint16), + dataTypeId: ZCLDataTypes.uint8, + value: ZCLDataTypes.buffer, + }), + ), }, response: { id: 0x10, @@ -235,11 +252,13 @@ const GLOBAL_COMMANDS = { id: 0x16, args: { lastResponse: ZCLDataTypes.bool, - attributes: ZCLDataTypes.Array0(ZCLStruct('DiscoveredAttributeExtended', { - id: ZCLDataTypes.uint16, - dataTypeId: ZCLDataTypes.uint8, - acl: ZCLDataTypes.map8('readable', 'writable', 'reportable'), - })), + attributes: ZCLDataTypes.Array0( + ZCLStruct('DiscoveredAttributeExtended', { + id: ZCLDataTypes.uint16, + dataTypeId: ZCLDataTypes.uint8, + acl: ZCLDataTypes.map8('readable', 'writable', 'reportable'), + }), + ), }, }, global: true, @@ -250,7 +269,6 @@ const GLOBAL_COMMANDS = { * The base cluster class every other cluster implementation must extend from. */ class Cluster extends EventEmitter { - /** * Create a new cluster instance. * @param {Endpoint} endpoint - A node {@link Endpoint} instance @@ -347,16 +365,23 @@ class Cluster extends EventEmitter { * @returns {Promise} */ async discoverCommandsGenerated({ startValue = 0, maxResults = 250 } = {}, opts) { - const { commandIds } = await super.discoverCommandsGenerated({ - startValue, - maxResults, - }, opts); + const { commandIds } = await super.discoverCommandsGenerated( + { + startValue, + maxResults, + }, + opts, + ); - const res = commandIds.map(cId => ((this.constructor.commandsById[cId] || []) - .filter(c => !c.global) - .sort((a, b) => (a.isResponse ? 1 : 0) - (b.isResponse ? 1 : 0)) // TODO - .pop() || {}) - .name || cId); + const res = commandIds.map( + (cId) => + ( + (this.constructor.commandsById[cId] || []) + .filter((c) => !c.global) + .sort((a, b) => (a.isResponse ? 1 : 0) - (b.isResponse ? 1 : 0)) // TODO + .pop() || {} + ).name || cId, + ); debug(this.logId, 'discoverCommandsGenerated', res); return res; @@ -384,16 +409,23 @@ class Cluster extends EventEmitter { * @returns {Promise} */ async discoverCommandsReceived({ startValue = 0, maxResults = 255 } = {}, opts) { - const { commandIds } = await super.discoverCommandsReceived({ - startValue, - maxResults, - }, opts); + const { commandIds } = await super.discoverCommandsReceived( + { + startValue, + maxResults, + }, + opts, + ); - const res = commandIds.map(cId => ((this.constructor.commandsById[cId] || []) - .filter(c => !c.global) - .sort((a, b) => (a.isResponse ? 0 : 1) - (b.isResponse ? 0 : 1)) // TODO - .pop() || {}) - .name || cId); + const res = commandIds.map( + (cId) => + ( + (this.constructor.commandsById[cId] || []) + .filter((c) => !c.global) + .sort((a, b) => (a.isResponse ? 0 : 1) - (b.isResponse ? 0 : 1)) // TODO + .pop() || {} + ).name || cId, + ); debug(this.logId, 'discoverCommandsReceived', res); return res; @@ -410,7 +442,9 @@ class Cluster extends EventEmitter { */ async readAttributes(attributes, opts) { if (attributes instanceof Array === false) { - throw new Error('Expected attributes array, as of zigbee-clusters@2.0.0 call readAttributes([\'myAttr\'])'); + throw new Error( + "Expected attributes array, as of zigbee-clusters@2.0.0 call readAttributes(['myAttr'])", + ); } if (!attributes.length) { @@ -422,8 +456,10 @@ class Cluster extends EventEmitter { const attrIds = new Set(); for (const attr of attributes) { if (typeof attr === 'number') { - if (!Number.isInteger(attr) || attr < 0 || attr > 0xFFFF) { - throw new TypeError(`${attr} is not a valid numeric attribute ID for ${this.name}, must be an integer between 0 and 0xFFFF`); + if (!Number.isInteger(attr) || attr < 0 || attr > 0xffff) { + throw new TypeError( + `${attr} is not a valid numeric attribute ID for ${this.name}, must be an integer between 0 and 0xFFFF`, + ); } numericIds.add(attr); if (idToName[attr] === undefined) idToName[attr] = attr; @@ -440,19 +476,27 @@ class Cluster extends EventEmitter { while (attrIds.size) { // Check if command should get manufacturerSpecific flag const manufacturerId = this._checkForManufacturerSpecificAttributes(Array.from(attrIds)); - debug(this.logId, 'read attributes', [...attrIds], manufacturerId ? `manufacturer specific id ${manufacturerId}` : ''); + debug( + this.logId, + 'read attributes', + [...attrIds], + manufacturerId ? `manufacturer specific id ${manufacturerId}` : '', + ); - const response = await super.readAttributes({ - attributes: [...attrIds], - manufacturerId, - }, opts); + const response = await super.readAttributes( + { + attributes: [...attrIds], + manufacturerId, + }, + opts, + ); debug(this.logId, 'read attributes result', response); const { attributeArrayStatusDataType } = this.constructor; const result = attributeArrayStatusDataType.fromBuffer(response.attributes, 0); if (!result.length) break; - result.forEach(a => { + result.forEach((a) => { attrIds.delete(a.id); if (a.status === 'SUCCESS') { resultObj[idToName[a.id]] = a.value; @@ -476,7 +520,7 @@ class Cluster extends EventEmitter { * @returns {Promise<*|{attributes: *}>} */ async writeAttributes(attributes = {}, opts) { - const arr = Object.keys(attributes).map(n => { + const arr = Object.keys(attributes).map((n) => { const attr = this.constructor.attributes[n]; if (!attr) { throw new TypeError(`${n} is not a valid attribute of ${this.name}`); @@ -489,13 +533,18 @@ class Cluster extends EventEmitter { // Check if command should get manufacturerSpecific flag const manufacturerId = this._checkForManufacturerSpecificAttributes( - Object.keys(attributes).map(n => this.constructor.attributes[n].id), + Object.keys(attributes).map((n) => this.constructor.attributes[n].id), ); let data = Buffer.alloc(1024); data = data.slice(0, this.constructor.attributeArrayDataType.toBuffer(data, arr, 0)); - debug(this.logId, 'write attributes', attributes, manufacturerId ? `manufacturer specific id ${manufacturerId}` : ''); + debug( + this.logId, + 'write attributes', + attributes, + manufacturerId ? `manufacturer specific id ${manufacturerId}` : '', + ); return super.writeAttributes({ attributes: data, manufacturerId }, opts); } @@ -540,10 +589,15 @@ class Cluster extends EventEmitter { if (req.length) { // Check if command should get manufacturerSpecific flag const manufacturerId = this._checkForManufacturerSpecificAttributes( - Object.keys(attributes).map(n => this.constructor.attributes[n].id), + Object.keys(attributes).map((n) => this.constructor.attributes[n].id), ); - debug(this.logId, 'configure reporting', req, manufacturerId ? `manufacturer specific id ${manufacturerId}` : ''); + debug( + this.logId, + 'configure reporting', + req, + manufacturerId ? `manufacturer specific id ${manufacturerId}` : '', + ); const { reports } = await super.configureReporting({ reports: req, manufacturerId }, opts); @@ -609,22 +663,29 @@ class Cluster extends EventEmitter { // Check if command should get manufacturerSpecific flag const manufacturerId = this._checkForManufacturerSpecificAttributes( - attributes - .map(attributeNameOrId => { - if (typeof attributeNameOrId === 'number') return attributeNameOrId; - return this.constructor.attributes[attributeNameOrId].id; - }), + attributes.map((attributeNameOrId) => { + if (typeof attributeNameOrId === 'number') return attributeNameOrId; + return this.constructor.attributes[attributeNameOrId].id; + }), ); - debug(this.logId, 'read reporting configuration', req, manufacturerId ? `manufacturer specific id ${manufacturerId}` : ''); + debug( + this.logId, + 'read reporting configuration', + req, + manufacturerId ? `manufacturer specific id ${manufacturerId}` : '', + ); // If a request has been constructed execute it if (req.length) { // Perform request, it returns a buffer that needs to be parsed - const { reports } = await super.readReportingConfiguration({ - attributes: req, - manufacturerId, - }, opts); + const { reports } = await super.readReportingConfiguration( + { + attributes: req, + manufacturerId, + }, + opts, + ); debug(this.logId, 'read reporting configuration result', reports); // Return the parsed reports @@ -654,17 +715,22 @@ class Cluster extends EventEmitter { * is implemented in zigbee-clusters or not). */ async discoverAttributes(opts) { - const { attributes } = await super.discoverAttributes({ - startValue: 0, - maxResults: 255, - }, opts); + const { attributes } = await super.discoverAttributes( + { + startValue: 0, + maxResults: 255, + }, + opts, + ); const result = []; for (const attr of attributes) { // Push the name if attribute is implemented in zigbee-clusters otherwise push attribute id - result.push(this.constructor.attributesById[attr.id] - ? this.constructor.attributesById[attr.id].name - : attr.id); + result.push( + this.constructor.attributesById[attr.id] + ? this.constructor.attributesById[attr.id].name + : attr.id, + ); } debug(this.logId, 'discover attributes', result); return result; @@ -693,10 +759,13 @@ class Cluster extends EventEmitter { * attribute is implemented in zigbee-clusters. */ async discoverAttributesExtended(opts) { - const { attributes } = await super.discoverAttributesExtended({ - startValue: 0, - maxResults: 250, - }, opts); + const { attributes } = await super.discoverAttributesExtended( + { + startValue: 0, + maxResults: 250, + }, + opts, + ); const result = []; for (const attr of attributes) { @@ -741,10 +810,14 @@ class Cluster extends EventEmitter { // Filter commands of this cluster based on cluster specific vs global, // and manufacturer specific vs not manufacturer specific. - let filteredCommands = commands.filter(cmd => frame.frameControl.clusterSpecific === !cmd.global - && (cmd.global || frame.frameControl.manufacturerSpecific === !!cmd.manufacturerId) - && (cmd.global || !frame.frameControl.manufacturerSpecific - || frame.manufacturerId === cmd.manufacturerId)); + let filteredCommands = commands.filter( + (cmd) => + frame.frameControl.clusterSpecific === !cmd.global && + (cmd.global || frame.frameControl.manufacturerSpecific === !!cmd.manufacturerId) && + (cmd.global || + !frame.frameControl.manufacturerSpecific || + frame.manufacturerId === cmd.manufacturerId), + ); // Try to filter based on frame direction, note: this is optional as a cluster command // does not always have a 'direction' property. The filter ensure that multiple commands @@ -752,13 +825,15 @@ class Cluster extends EventEmitter { // to both command definitions (see iasZone.js as an example). Cluster.js only receives // frames with directionToClient=true. BoundCluster.js receives frames with // directionToClient=false. - filteredCommands = filteredCommands.filter(command => { + filteredCommands = filteredCommands.filter((command) => { // Only filter commands that have a direction string property if (typeof command.direction === 'string') { // Filter out commands marked as DIRECTION_CLIENT_TO_SERVER when // frameControl.directionToClient = true - if (frame.frameControl.directionToClient - && command.direction === Cluster.DIRECTION_CLIENT_TO_SERVER) { + if ( + frame.frameControl.directionToClient && + command.direction === Cluster.DIRECTION_CLIENT_TO_SERVER + ) { return false; } } @@ -774,9 +849,7 @@ class Cluster extends EventEmitter { const handlerName = `on${command.name.charAt(0).toUpperCase()}${command.name.slice(1)}`; // Parse the command arguments - const args = command.args - ? command.args.fromBuffer(frame.data, 0) - : undefined; + const args = command.args ? command.args.fromBuffer(frame.data, 0) : undefined; debug(this.logId, 'received frame', command.name, args); @@ -785,8 +858,8 @@ class Cluster extends EventEmitter { const pendingTrx = this._trxHandlers[frame.trxSequenceNumber]; let isTrxResponse = false; if (pendingTrx) { - const isDefaultResponseToCommand = command.name === 'defaultResponse' - && args.cmdId === pendingTrx.cmdId; + const isDefaultResponseToCommand = + command.name === 'defaultResponse' && args.cmdId === pendingTrx.cmdId; let isReply; if (pendingTrx.responseCmdId === undefined) { @@ -800,7 +873,13 @@ class Cluster extends EventEmitter { isTrxResponse = isReply || isDefaultResponseToCommand; } if (pendingTrx && !isTrxResponse) { - debug(this.logId, 'frame with pending trx', frame.trxSequenceNumber, 'is not its response:', command.name); + debug( + this.logId, + 'frame with pending trx', + frame.trxSequenceNumber, + 'is not its response:', + command.name, + ); } // Invoke the right handler @@ -860,7 +939,7 @@ class Cluster extends EventEmitter { attributes = this.constructor.attributeArrayDataType.fromBuffer(attributes, 0); // Only expose fields chosen here, not the stack's frame meta const reportMeta = { transactionSequenceNumber: frame.trxSequenceNumber }; - attributes.forEach(attr => this.emit(`attr.${attr.name}`, attr.value, reportMeta)); + attributes.forEach((attr) => this.emit(`attr.${attr.name}`, attr.value, reportMeta)); } /** @@ -872,11 +951,12 @@ class Cluster extends EventEmitter { * @private */ async onDiscoverCommandsGenerated({ startValue = 0, maxResults = 250 } = {}) { - const cmds = [].concat(...Object.values(this.constructor.commandsById)) - .filter(c => !c.global && !c.isResponse && this[c.name]) - .map(c => c.id) + const cmds = [] + .concat(...Object.values(this.constructor.commandsById)) + .filter((c) => !c.global && !c.isResponse && this[c.name]) + .map((c) => c.id) .sort() - .filter(cId => cId >= startValue); + .filter((cId) => cId >= startValue); const result = cmds.slice(0, maxResults); debug(this.logId, 'onDiscoverCommandsGenerated', { @@ -899,12 +979,17 @@ class Cluster extends EventEmitter { * @private */ async onDiscoverCommandsReceived({ startValue = 0, maxResults = 250 } = {}) { - const cmds = [].concat(...Object.values(this.constructor.commandsById)) - .filter(c => !c.global && c.response - && (this[c.name] || this[`on${c.name.charAt(0).toUpperCase()}${c.name.slice(1)}`])) - .map(c => c.response.id) + const cmds = [] + .concat(...Object.values(this.constructor.commandsById)) + .filter( + (c) => + !c.global && + c.response && + (this[c.name] || this[`on${c.name.charAt(0).toUpperCase()}${c.name.slice(1)}`]), + ) + .map((c) => c.response.id) .sort() - .filter(cId => cId >= startValue); + .filter((cId) => cId >= startValue); const result = cmds.slice(0, maxResults); debug(this.logId, 'onDiscoverCommandsReceived', { @@ -1174,10 +1259,11 @@ class Cluster extends EventEmitter { // Lets handleFrame accept only a response to this command cmdId: cmd.id, // A declared response without id reuses the command id (see _addPrototypeMethods) - responseCmdId: cmd.response - && (typeof cmd.response.id === 'number' ? cmd.response.id : cmd.id), + responseCmdId: + cmd.response && + (typeof cmd.response.id === 'number' ? cmd.response.id : cmd.id), global: !!cmd.global, - handle: async frame => { + handle: async (frame) => { delete this._trxHandlers[payload.trxSequenceNumber]; if (awaitResponseTimeout) clearTimeout(awaitResponseTimeout); resolveAwaitResponse(frame); @@ -1200,7 +1286,7 @@ class Cluster extends EventEmitter { rejectAwaitResponse(new Error('Timeout: Expected Response')); }, responseTimeout); }) - .catch(err => { + .catch((err) => { // The stack can report a send failure (e.g. no APS ACK in time) while the device // already answered; that response proves delivery, so don't fail the command. if (this._trxHandlers[payload.trxSequenceNumber] == null) return; @@ -1247,20 +1333,25 @@ class Cluster extends EventEmitter { // Do not allow different manufacturer ids in one command if (new Set(manufacturerIds).size > 1) { - throw new Error('Error: detected multiple manufacturer ids, can only read from one at a time'); + throw new Error( + 'Error: detected multiple manufacturer ids, can only read from one at a time', + ); } // Show warning if a manufacturer specific attribute was found amongst non-manufacturer // specific attributes if (manufacturerIds.length > 0 && attrIdsSet.size !== manufacturerIds.length) { - debug(this.logId, 'WARNING expected only manufacturer specific attributes got:', manufacturerIds); + debug( + this.logId, + 'WARNING expected only manufacturer specific attributes got:', + manufacturerIds, + ); } // Return the manufacturerId that was found in the attributes if (attrIdsSet.size === manufacturerIds.length) return manufacturerIds[0]; return null; } - } Cluster.clusters = {}; diff --git a/lib/Endpoint.js b/lib/Endpoint.js index 74cc1509..94e4faf2 100644 --- a/lib/Endpoint.js +++ b/lib/Endpoint.js @@ -16,7 +16,6 @@ debug = debug.extend('endpoint'); * Class representing an endpoint on a node. It contains a reference to its clusters and bindings. */ class Endpoint extends EventEmitter { - /** * Creates a new Endpoint instance. * @param {ZCLNode} node @@ -34,7 +33,7 @@ class Endpoint extends EventEmitter { this._nextTrxSeqNr = Math.floor(Math.random() * 256); // Iterate all the input clusters and instantiate a Cluster for each - descriptor.inputClusters.forEach(cId => { + descriptor.inputClusters.forEach((cId) => { const InputCluster = Cluster.getCluster(cId); if (InputCluster) { this.clusters[InputCluster.NAME] = new InputCluster(this); @@ -127,12 +126,15 @@ class Endpoint extends EventEmitter { clusterSpecificResponse = await this.handleZCLFrame(clusterId, frame, meta, rawFrame); } catch (err) { clusterSpecificError = err; - debug(`${this.getLogId(clusterId)}, error while handling frame`, err.message, { meta, frame }); + debug(`${this.getLogId(clusterId)}, error while handling frame`, err.message, { + meta, + frame, + }); } // Don't respond to a default response or a group cast (ZCL spec 2.5.12.2). Only global // command 0x0B is a default response, cluster-specific ids are per cluster (2.4.1.4). - const isDefaultResponse = frame.cmdId === 0x0B && !frame.frameControl.clusterSpecific; + const isDefaultResponse = frame.cmdId === 0x0b && !frame.frameControl.clusterSpecific; if (isDefaultResponse || (meta && typeof meta.groupId === 'number')) return; // Default responses are for unicast only (ZCL spec 2.5.12.2). `broadcast` is set by the stack; @@ -143,12 +145,14 @@ class Endpoint extends EventEmitter { if (clusterSpecificError) { if (isBroadcast) return; - const status = clusterSpecificError instanceof ZCLError - ? clusterSpecificError.zclStatus : undefined; + const status = + clusterSpecificError instanceof ZCLError ? clusterSpecificError.zclStatus : undefined; const defaultResponseErrorFrame = this.makeDefaultResponseFrame(frame, false, status); - this.sendFrame(clusterId, defaultResponseErrorFrame.toBuffer()).catch(err => { - debug(`${this.getLogId(clusterId)}, error while sending default error response`, err, { response: defaultResponseErrorFrame }); + this.sendFrame(clusterId, defaultResponseErrorFrame.toBuffer()).catch((err) => { + debug(`${this.getLogId(clusterId)}, error while sending default error response`, err, { + response: defaultResponseErrorFrame, + }); }); // No further handling for this frame @@ -178,7 +182,11 @@ class Endpoint extends EventEmitter { try { await this.sendFrame(clusterId, responseFrame.toBuffer()); } catch (err) { - debug(`${this.getLogId(clusterId)}, error while sending cluster specific or default success response`, err, { response: responseFrame }); + debug( + `${this.getLogId(clusterId)}, error while sending cluster specific or default success response`, + err, + { response: responseFrame }, + ); } } @@ -235,7 +243,7 @@ class Endpoint extends EventEmitter { responseFrame.frameControl.directionToClient = !receivedFrame.frameControl.directionToClient; responseFrame.trxSequenceNumber = receivedFrame.trxSequenceNumber; - responseFrame.cmdId = 0x0B; + responseFrame.cmdId = 0x0b; responseFrame.data = Buffer.from([receivedFrame.cmdId, success ? 0 : 1]); // If not successful, set the status code in the response frame data @@ -259,7 +267,6 @@ class Endpoint extends EventEmitter { } return ZCLStandardHeader.fromBuffer(frame); } - } module.exports = Endpoint; diff --git a/lib/Node.js b/lib/Node.js index f3f6b04e..002ff4ac 100644 --- a/lib/Node.js +++ b/lib/Node.js @@ -26,7 +26,6 @@ debug = debug.extend('node'); * @alias ZCLNode */ class Node extends EventEmitter { - /** * Create a new ZCLNode instance. * @param {Homey.ZigBeeNode} node - As provided by `Homey.ManagerZigBee.getNode()`. @@ -62,7 +61,7 @@ class Node extends EventEmitter { this.endpoints = {}; if (Array.isArray(node.endpointDescriptors)) { - node.endpointDescriptors.forEach(ep => { + node.endpointDescriptors.forEach((ep) => { this.endpoints[ep.endpointId] = new Endpoint(this, ep); }); } @@ -108,10 +107,12 @@ class Node extends EventEmitter { if (this.endpoints[endpointId]) { await this.endpoints[endpointId].handleFrame(clusterId, frame, meta); } else { - debug(`${this.getLogId(endpointId, clusterId)}, error while handling frame, endpoint unavailable`, { meta, frame }); + debug( + `${this.getLogId(endpointId, clusterId)}, error while handling frame, endpoint unavailable`, + { meta, frame }, + ); } } - } module.exports = Node; diff --git a/lib/clusters/alarms.js b/lib/clusters/alarms.js index a80c5ae8..d153596b 100644 --- a/lib/clusters/alarms.js +++ b/lib/clusters/alarms.js @@ -29,7 +29,6 @@ const COMMANDS = { }; class AlarmsCluster extends Cluster { - static get ID() { return 9; // 0x9 } @@ -45,7 +44,6 @@ class AlarmsCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(AlarmsCluster); diff --git a/lib/clusters/analogInput.js b/lib/clusters/analogInput.js index db6536b3..eff398bd 100644 --- a/lib/clusters/analogInput.js +++ b/lib/clusters/analogInput.js @@ -25,14 +25,16 @@ const ATTRIBUTES = { }), }, resolution: { id: 106, type: ZCLDataTypes.single }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class AnalogInputCluster extends Cluster { - static get ID() { return 12; // 0x0c } @@ -48,7 +50,6 @@ class AnalogInputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(AnalogInputCluster); diff --git a/lib/clusters/analogOutput.js b/lib/clusters/analogOutput.js index d8ef234c..302339b2 100644 --- a/lib/clusters/analogOutput.js +++ b/lib/clusters/analogOutput.js @@ -24,14 +24,16 @@ const ATTRIBUTES = { }, relinquishDefault: { id: 104, type: ZCLDataTypes.single }, resolution: { id: 106, type: ZCLDataTypes.single }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class AnalogOutputCluster extends Cluster { - static get ID() { return 13; } @@ -47,7 +49,6 @@ class AnalogOutputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(AnalogOutputCluster); diff --git a/lib/clusters/analogValue.js b/lib/clusters/analogValue.js index 073a7cdc..a7fdd8fa 100644 --- a/lib/clusters/analogValue.js +++ b/lib/clusters/analogValue.js @@ -21,14 +21,16 @@ const ATTRIBUTES = { }), }, relinquishDefault: { id: 104, type: ZCLDataTypes.single }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class AnalogValueCluster extends Cluster { - static get ID() { return 14; } @@ -44,7 +46,6 @@ class AnalogValueCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(AnalogValueCluster); diff --git a/lib/clusters/ballastConfiguration.js b/lib/clusters/ballastConfiguration.js index 92b1f0a5..d66114a5 100644 --- a/lib/clusters/ballastConfiguration.js +++ b/lib/clusters/ballastConfiguration.js @@ -6,7 +6,6 @@ const { ZCLDataTypes } = require('../zclTypes'); const COMMANDS = {}; const ATTRIBUTES = { - /* 0x0000: PhysicalMinLevel (optional, read-only) * Description: Minimum light level the ballast can achieve. * Default: 0x01 @@ -185,7 +184,6 @@ const ATTRIBUTES = { }; class BallastConfigurationCluster extends Cluster { - static get ID() { return 769; // 0x0301 } @@ -201,7 +199,6 @@ class BallastConfigurationCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(BallastConfigurationCluster); diff --git a/lib/clusters/basic.js b/lib/clusters/basic.js index 9f91fce4..ac926f71 100644 --- a/lib/clusters/basic.js +++ b/lib/clusters/basic.js @@ -191,7 +191,6 @@ const COMMANDS = { }; class BasicCluster extends Cluster { - static get ID() { return 0; } @@ -207,7 +206,6 @@ class BasicCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(BasicCluster); diff --git a/lib/clusters/binaryInput.js b/lib/clusters/binaryInput.js index a6d62f6b..0192de38 100644 --- a/lib/clusters/binaryInput.js +++ b/lib/clusters/binaryInput.js @@ -31,14 +31,16 @@ const ATTRIBUTES = { configurationError: 10, // CONFIGURATION-ERROR (10) }), }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class BinaryInputCluster extends Cluster { - static get ID() { return 15; } @@ -54,7 +56,6 @@ class BinaryInputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(BinaryInputCluster); diff --git a/lib/clusters/binaryOutput.js b/lib/clusters/binaryOutput.js index bd7b33f4..163b11bb 100644 --- a/lib/clusters/binaryOutput.js +++ b/lib/clusters/binaryOutput.js @@ -32,14 +32,16 @@ const ATTRIBUTES = { }), }, relinquishDefault: { id: 104, type: ZCLDataTypes.bool }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class BinaryOutputCluster extends Cluster { - static get ID() { return 16; } @@ -55,7 +57,6 @@ class BinaryOutputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(BinaryOutputCluster); diff --git a/lib/clusters/binaryValue.js b/lib/clusters/binaryValue.js index 718263b9..170047c5 100644 --- a/lib/clusters/binaryValue.js +++ b/lib/clusters/binaryValue.js @@ -32,14 +32,16 @@ const ATTRIBUTES = { }), }, relinquishDefault: { id: 104, type: ZCLDataTypes.bool }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class BinaryValueCluster extends Cluster { - static get ID() { return 17; } @@ -55,7 +57,6 @@ class BinaryValueCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(BinaryValueCluster); diff --git a/lib/clusters/colorControl.js b/lib/clusters/colorControl.js index ef699186..90a3a10e 100644 --- a/lib/clusters/colorControl.js +++ b/lib/clusters/colorControl.js @@ -113,7 +113,16 @@ const ATTRIBUTES = { // Bit 4: colorTemperature mandatory commands: moveToColorTemperature, moveColorTemperature, // stepColorTemperature, stopMoveStep, related attributes: colorTemperatureMireds, // colorTempPhysicalMinMireds, colorTempPhysicalMaxMireds - colorCapabilities: { id: 16394, type: ZCLDataTypes.map16('hueAndSaturation', 'enhancedHue', 'colorLoop', 'xy', 'colorTemperature') }, + colorCapabilities: { + id: 16394, + type: ZCLDataTypes.map16( + 'hueAndSaturation', + 'enhancedHue', + 'colorLoop', + 'xy', + 'colorTemperature', + ), + }, // Mandatory if colorCapabilities = colorTemperature // Read-only @@ -193,7 +202,10 @@ const COMMANDS = { // action, direction, time and startHue fields below are ignored by the device unless their // flag is set here. updateFlags: ZCLDataTypes.map8( - 'updateAction', 'updateDirection', 'updateTime', 'updateStartHue', + 'updateAction', + 'updateDirection', + 'updateTime', + 'updateStartHue', ), // Action to take for the color loop, only applied when updateFlags.updateAction is set @@ -222,7 +234,6 @@ const COMMANDS = { }; class ColorControlCluster extends Cluster { - static get ID() { return 768; // 0x0300 } @@ -238,7 +249,6 @@ class ColorControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ColorControlCluster); diff --git a/lib/clusters/dehumidificationControl.js b/lib/clusters/dehumidificationControl.js index cae9ca67..a8338967 100644 --- a/lib/clusters/dehumidificationControl.js +++ b/lib/clusters/dehumidificationControl.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class DehumidificationControlCluster extends Cluster { - static get ID() { return 515; } @@ -24,7 +22,6 @@ class DehumidificationControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(DehumidificationControlCluster); diff --git a/lib/clusters/deviceTemperature.js b/lib/clusters/deviceTemperature.js index 2b73ede0..62588bc1 100644 --- a/lib/clusters/deviceTemperature.js +++ b/lib/clusters/deviceTemperature.js @@ -8,7 +8,10 @@ const ATTRIBUTES = { minTempExperienced: { id: 1, type: ZCLDataTypes.int16 }, maxTempExperienced: { id: 2, type: ZCLDataTypes.int16 }, overTempTotalDwell: { id: 3, type: ZCLDataTypes.uint16 }, - deviceTempAlarmMask: { id: 16, type: ZCLDataTypes.map8('deviceTemperatureTooLow', 'deviceTemperatureTooHigh') }, + deviceTempAlarmMask: { + id: 16, + type: ZCLDataTypes.map8('deviceTemperatureTooLow', 'deviceTemperatureTooHigh'), + }, lowTempThreshold: { id: 17, type: ZCLDataTypes.int16 }, highTempThreshold: { id: 18, type: ZCLDataTypes.int16 }, lowTempDwellTripPoint: { id: 19, type: ZCLDataTypes.uint24 }, @@ -18,7 +21,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class DeviceTemperatureCluster extends Cluster { - static get ID() { return 2; } @@ -34,7 +36,6 @@ class DeviceTemperatureCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(DeviceTemperatureCluster); diff --git a/lib/clusters/diagnostics.js b/lib/clusters/diagnostics.js index f13d27bb..61cc151e 100644 --- a/lib/clusters/diagnostics.js +++ b/lib/clusters/diagnostics.js @@ -7,7 +7,6 @@ const ATTRIBUTES = {}; const COMMANDS = {}; class DiagnosticsCluster extends Cluster { - static get ID() { return 2821; // 0x0b05 } @@ -23,7 +22,6 @@ class DiagnosticsCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(DiagnosticsCluster); diff --git a/lib/clusters/doorLock.js b/lib/clusters/doorLock.js index 8335940f..03beb134 100644 --- a/lib/clusters/doorLock.js +++ b/lib/clusters/doorLock.js @@ -68,7 +68,7 @@ const ATTRIBUTES = { maxPINCodeLength: { id: 0x0017, type: ZCLDataTypes.uint8 }, // 23, Conditional¹ minPINCodeLength: { id: 0x0018, type: ZCLDataTypes.uint8 }, // 24, Conditional¹ maxRFIDCodeLength: { id: 0x0019, type: ZCLDataTypes.uint8 }, // 25, Conditional² - minRFIDCodeLength: { id: 0x001A, type: ZCLDataTypes.uint8 }, // 26, Conditional² + minRFIDCodeLength: { id: 0x001a, type: ZCLDataTypes.uint8 }, // 26, Conditional² // ¹ PIN credential support // ² RFID credential support // ³ Week Day Schedule support @@ -95,13 +95,7 @@ const ATTRIBUTES = { // Mandatory supportedOperatingModes: { id: 0x0026, // 38 - type: ZCLDataTypes.map16( - 'normal', - 'vacation', - 'privacy', - 'noRFLockOrUnlock', - 'passage', - ), + type: ZCLDataTypes.map16('normal', 'vacation', 'privacy', 'noRFLockOrUnlock', 'passage'), }, // Optional defaultConfigurationRegister: { @@ -119,8 +113,8 @@ const ATTRIBUTES = { }, enableLocalProgramming: { id: 0x0028, type: ZCLDataTypes.bool }, // 40, Optional enableOneTouchLocking: { id: 0x0029, type: ZCLDataTypes.bool }, // 41, Optional - enableInsideStatusLED: { id: 0x002A, type: ZCLDataTypes.bool }, // 42, Optional - enablePrivacyModeButton: { id: 0x002B, type: ZCLDataTypes.bool }, // 43, Optional + enableInsideStatusLED: { id: 0x002a, type: ZCLDataTypes.bool }, // 42, Optional + enablePrivacyModeButton: { id: 0x002b, type: ZCLDataTypes.bool }, // 43, Optional // Security Settings (0x0030 - 0x003F) wrongCodeEntryLimit: { id: 0x0030, type: ZCLDataTypes.uint8 }, // 48, Optional @@ -402,12 +396,12 @@ const COMMANDS = { }, // Optional getUserStatus: { - id: 0x000A, // 10 + id: 0x000a, // 10 args: { userId: ZCLDataTypes.uint16, }, response: { - id: 0x000A, // 10 + id: 0x000a, // 10 args: { userId: ZCLDataTypes.uint16, userStatus: USER_STATUS_ENUM, @@ -418,35 +412,51 @@ const COMMANDS = { // Schedule Commands // Optional setWeekDaySchedule: { - id: 0x000B, // 11 + id: 0x000b, // 11 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, - daysMask: ZCLDataTypes.map8('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'), + daysMask: ZCLDataTypes.map8( + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + ), startHour: ZCLDataTypes.uint8, startMinute: ZCLDataTypes.uint8, endHour: ZCLDataTypes.uint8, endMinute: ZCLDataTypes.uint8, }, response: { - id: 0x000B, // 11 + id: 0x000b, // 11 args: { status: ZCLDataTypes.uint8 }, }, }, // Optional getWeekDaySchedule: { - id: 0x000C, // 12 + id: 0x000c, // 12 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, }, response: { - id: 0x000C, // 12 + id: 0x000c, // 12 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, status: ZCLDataTypes.uint8, - daysMask: ZCLDataTypes.map8('sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'), + daysMask: ZCLDataTypes.map8( + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + ), startHour: ZCLDataTypes.uint8, startMinute: ZCLDataTypes.uint8, endHour: ZCLDataTypes.uint8, @@ -456,19 +466,19 @@ const COMMANDS = { }, // Optional clearWeekDaySchedule: { - id: 0x000D, // 13 + id: 0x000d, // 13 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, }, response: { - id: 0x000D, // 13 + id: 0x000d, // 13 args: { status: ZCLDataTypes.uint8 }, }, }, // Optional setYearDaySchedule: { - id: 0x000E, // 14 + id: 0x000e, // 14 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, @@ -476,19 +486,19 @@ const COMMANDS = { localEndTime: ZCLDataTypes.uint32, }, response: { - id: 0x000E, // 14 + id: 0x000e, // 14 args: { status: ZCLDataTypes.uint8 }, }, }, // Optional getYearDaySchedule: { - id: 0x000F, // 15 + id: 0x000f, // 15 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, }, response: { - id: 0x000F, // 15 + id: 0x000f, // 15 args: { scheduleId: ZCLDataTypes.uint8, userId: ZCLDataTypes.uint16, @@ -665,7 +675,6 @@ const COMMANDS = { }; class DoorLockCluster extends Cluster { - static get ID() { return 0x0101; // 257 } @@ -681,7 +690,6 @@ class DoorLockCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(DoorLockCluster); diff --git a/lib/clusters/electricalMeasurement.js b/lib/clusters/electricalMeasurement.js index 8474f859..ccfa69b9 100644 --- a/lib/clusters/electricalMeasurement.js +++ b/lib/clusters/electricalMeasurement.js @@ -4,7 +4,20 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { - measurementType: { id: 0, type: ZCLDataTypes.map32('activeMeasurementAC', 'reactiveMeasurementAC', 'apparentMeasurementAC', 'phaseAMeasurement', 'phaseBMeasurement', 'phaseCMeasurement', 'dcMeasurement', 'harmonicsMeasurement', 'powerQualityMeasurement') }, + measurementType: { + id: 0, + type: ZCLDataTypes.map32( + 'activeMeasurementAC', + 'reactiveMeasurementAC', + 'apparentMeasurementAC', + 'phaseAMeasurement', + 'phaseBMeasurement', + 'phaseCMeasurement', + 'dcMeasurement', + 'harmonicsMeasurement', + 'powerQualityMeasurement', + ), + }, acFrequency: { id: 768, type: ZCLDataTypes.uint16 }, measuredPhase1stHarmonicCurrent: { id: 781, type: ZCLDataTypes.int16 }, acFrequencyMultiplier: { id: 1024, type: ZCLDataTypes.uint16 }, @@ -20,7 +33,21 @@ const ATTRIBUTES = { acCurrentDivisor: { id: 1539, type: ZCLDataTypes.uint16 }, acPowerMultiplier: { id: 1540, type: ZCLDataTypes.uint16 }, acPowerDivisor: { id: 1541, type: ZCLDataTypes.uint16 }, - acAlarmsMask: { id: 2048, type: ZCLDataTypes.map16('voltageOverload', 'currentOverload', 'activePowerOverload', 'reactivePowerOverload', 'averageRMSOverVoltage', 'averageRMSUnderVoltage', 'rmsExtremeOverVoltage', 'rmsExtremeUnderVoltage', 'rmsVoltageSag', 'rmsVoltageSwell') }, + acAlarmsMask: { + id: 2048, + type: ZCLDataTypes.map16( + 'voltageOverload', + 'currentOverload', + 'activePowerOverload', + 'reactivePowerOverload', + 'averageRMSOverVoltage', + 'averageRMSUnderVoltage', + 'rmsExtremeOverVoltage', + 'rmsExtremeUnderVoltage', + 'rmsVoltageSag', + 'rmsVoltageSwell', + ), + }, acVoltageOverload: { id: 2049, type: ZCLDataTypes.uint16 }, acCurrentOverload: { id: 2050, type: ZCLDataTypes.uint16 }, acActivePowerOverload: { id: 2051, type: ZCLDataTypes.uint16 }, @@ -29,7 +56,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class ElectricalMeasurementCluster extends Cluster { - static get ID() { return 2820; // 0x0b04 } @@ -45,7 +71,6 @@ class ElectricalMeasurementCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ElectricalMeasurementCluster); diff --git a/lib/clusters/fanControl.js b/lib/clusters/fanControl.js index 82c83bb9..367c217b 100644 --- a/lib/clusters/fanControl.js +++ b/lib/clusters/fanControl.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class FanControlCluster extends Cluster { - static get ID() { return 514; } @@ -24,7 +22,6 @@ class FanControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(FanControlCluster); diff --git a/lib/clusters/flowMeasurement.js b/lib/clusters/flowMeasurement.js index 1e3936b8..79c25a29 100644 --- a/lib/clusters/flowMeasurement.js +++ b/lib/clusters/flowMeasurement.js @@ -13,7 +13,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class FlowMeasurementCluster extends Cluster { - static get ID() { return 1028; } @@ -29,7 +28,6 @@ class FlowMeasurementCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(FlowMeasurementCluster); diff --git a/lib/clusters/groups.js b/lib/clusters/groups.js index 878228fc..15863672 100644 --- a/lib/clusters/groups.js +++ b/lib/clusters/groups.js @@ -4,7 +4,10 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { - nameSupport: { id: 0, type: ZCLDataTypes.map8(null, null, null, null, null, null, null, 'groupNames') }, + nameSupport: { + id: 0, + type: ZCLDataTypes.map8(null, null, null, null, null, null, null, 'groupNames'), + }, }; const COMMANDS = { @@ -73,7 +76,6 @@ const COMMANDS = { }; class GroupsCluster extends Cluster { - static get ID() { return 4; } @@ -89,7 +91,6 @@ class GroupsCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(GroupsCluster); diff --git a/lib/clusters/iasACE.js b/lib/clusters/iasACE.js index 250b1d84..43f0b2e7 100644 --- a/lib/clusters/iasACE.js +++ b/lib/clusters/iasACE.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class IASACECluster extends Cluster { - static get ID() { return 1281; } @@ -24,7 +22,6 @@ class IASACECluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IASACECluster); diff --git a/lib/clusters/iasWD.js b/lib/clusters/iasWD.js index 91ce66b3..ddef804e 100644 --- a/lib/clusters/iasWD.js +++ b/lib/clusters/iasWD.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class IASWDCluster extends Cluster { - static get ID() { return 1282; } @@ -24,7 +22,6 @@ class IASWDCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IASWDCluster); diff --git a/lib/clusters/iasZone.js b/lib/clusters/iasZone.js index fbb13287..c4079124 100644 --- a/lib/clusters/iasZone.js +++ b/lib/clusters/iasZone.js @@ -3,7 +3,18 @@ const Cluster = require('../Cluster'); const { ZCLDataTypes } = require('../zclTypes'); -const ZONE_STATUS_DATA_TYPE = ZCLDataTypes.map16('alarm1', 'alarm2', 'tamper', 'battery', 'supervisionReports', 'restoreReports', 'trouble', 'acMains', 'test', 'batteryDefect'); +const ZONE_STATUS_DATA_TYPE = ZCLDataTypes.map16( + 'alarm1', + 'alarm2', + 'tamper', + 'battery', + 'supervisionReports', + 'restoreReports', + 'trouble', + 'acMains', + 'test', + 'batteryDefect', +); const ZONE_TYPE_VALUES = { standardCIE: 0x0000, @@ -93,7 +104,6 @@ const COMMANDS = { }; class IASZoneCluster extends Cluster { - static get ID() { return 1280; } @@ -109,7 +119,6 @@ class IASZoneCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IASZoneCluster); diff --git a/lib/clusters/identify.js b/lib/clusters/identify.js index 2da309d4..eb907cbd 100644 --- a/lib/clusters/identify.js +++ b/lib/clusters/identify.js @@ -40,7 +40,6 @@ const COMMANDS = { }; class IdentifyCluster extends Cluster { - static get ID() { return 3; } @@ -56,7 +55,6 @@ class IdentifyCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IdentifyCluster); diff --git a/lib/clusters/illuminanceLevelSensing.js b/lib/clusters/illuminanceLevelSensing.js index 8b07c1ae..6f7fe862 100644 --- a/lib/clusters/illuminanceLevelSensing.js +++ b/lib/clusters/illuminanceLevelSensing.js @@ -27,7 +27,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class IlluminanceLevelSensingCluster extends Cluster { - static get ID() { return 1025; } @@ -43,7 +42,6 @@ class IlluminanceLevelSensingCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IlluminanceLevelSensingCluster); diff --git a/lib/clusters/illuminanceMeasurement.js b/lib/clusters/illuminanceMeasurement.js index 656cc226..2bcdf1d8 100644 --- a/lib/clusters/illuminanceMeasurement.js +++ b/lib/clusters/illuminanceMeasurement.js @@ -21,7 +21,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class IlluminanceMeasurementCluster extends Cluster { - static get ID() { return 1024; // 0x0400 } @@ -37,7 +36,6 @@ class IlluminanceMeasurementCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(IlluminanceMeasurementCluster); diff --git a/lib/clusters/index.js b/lib/clusters/index.js index cf5718b3..25e81d72 100644 --- a/lib/clusters/index.js +++ b/lib/clusters/index.js @@ -57,11 +57,12 @@ const TouchLinkCluster = require('./touchlink'); * @returns {Readonly<{ATTRIBUTES: *, COMMANDS: *, ID: *, NAME: *}>} * @private */ -function destructConstProps({ - ID, NAME, ATTRIBUTES, COMMANDS, -}) { +function destructConstProps({ ID, NAME, ATTRIBUTES, COMMANDS }) { return Object.freeze({ - ID, NAME, ATTRIBUTES, COMMANDS, + ID, + NAME, + ATTRIBUTES, + COMMANDS, }); } diff --git a/lib/clusters/levelControl.js b/lib/clusters/levelControl.js index 04c20816..bd1bab0a 100644 --- a/lib/clusters/levelControl.js +++ b/lib/clusters/levelControl.js @@ -76,7 +76,6 @@ const COMMANDS = { }; class LevelControlCluster extends Cluster { - static get ID() { return 8; } @@ -92,7 +91,6 @@ class LevelControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(LevelControlCluster); diff --git a/lib/clusters/metering.js b/lib/clusters/metering.js index 592bbd74..5c3ea486 100644 --- a/lib/clusters/metering.js +++ b/lib/clusters/metering.js @@ -15,10 +15,10 @@ const ATTRIBUTES = { readingSnapShotTime: { id: 0x0007, type: ZCLDataTypes.uint32 }, // Optional currentMaxDemandDeliveredTime: { id: 0x0008, type: ZCLDataTypes.uint32 }, // Optional currentMaxDemandReceivedTime: { id: 0x0009, type: ZCLDataTypes.uint32 }, // Optional - defaultUpdatePeriod: { id: 0x000A, type: ZCLDataTypes.uint8 }, // 10, Optional - fastPollUpdatePeriod: { id: 0x000B, type: ZCLDataTypes.uint8 }, // 11, Optional - currentBlockPeriodConsumptionDelivered: { id: 0x000C, type: ZCLDataTypes.uint48 }, // 12, Optional - dailyConsumptionTarget: { id: 0x000D, type: ZCLDataTypes.uint24 }, // 13, Optional + defaultUpdatePeriod: { id: 0x000a, type: ZCLDataTypes.uint8 }, // 10, Optional + fastPollUpdatePeriod: { id: 0x000b, type: ZCLDataTypes.uint8 }, // 11, Optional + currentBlockPeriodConsumptionDelivered: { id: 0x000c, type: ZCLDataTypes.uint48 }, // 12, Optional + dailyConsumptionTarget: { id: 0x000d, type: ZCLDataTypes.uint24 }, // 13, Optional // currentBlock: { id: 0x000E, type: ZCLDataTypes.enum8({/* TODO fix */}) }, // 14, Optional // profileIntervalPeriod: { // Optional // id: 0x000F, // 15 @@ -76,8 +76,8 @@ const ATTRIBUTES = { // temperatureUnitOfMeasure: { id: 0x030C, type: ZCLDataTypes.enum8({/* TODO fix */}) }, // TODO: map8 packed fields — bits 0-2: right digits, 3-6: left digits, 7: suppress zeros // temperatureFormatting: { id: 0x030D, type: ZCLDataTypes.map8() }, // 781, Optional - moduleSerialNumber: { id: 0x030E, type: ZCLDataTypes.octstr }, // 782, Optional - operatingTariffLabelDelivered: { id: 0x030F, type: ZCLDataTypes.octstr }, // 783, Optional + moduleSerialNumber: { id: 0x030e, type: ZCLDataTypes.octstr }, // 782, Optional + operatingTariffLabelDelivered: { id: 0x030f, type: ZCLDataTypes.octstr }, // 783, Optional operatingTariffLabelReceived: { id: 0x0310, type: ZCLDataTypes.octstr }, // 784, Optional customerIdNumber: { id: 0x0311, type: ZCLDataTypes.octstr }, // 785, Optional /* 786, Optional */ @@ -113,12 +113,12 @@ const ATTRIBUTES = { type: ZCLDataTypes.uint24, }, currentDayMaxPressure: { id: 0x0409, type: ZCLDataTypes.uint48 }, // 1033, Optional - currentDayMinPressure: { id: 0x040A, type: ZCLDataTypes.uint48 }, // 1034, Optional - previousDayMaxPressure: { id: 0x040B, type: ZCLDataTypes.uint48 }, // 1035, Optional - previousDayMinPressure: { id: 0x040C, type: ZCLDataTypes.uint48 }, // 1036, Optional - currentDayMaxDemand: { id: 0x040D, type: ZCLDataTypes.int24 }, // 1037, Optional - previousDayMaxDemand: { id: 0x040E, type: ZCLDataTypes.int24 }, // 1038, Optional - currentMonthMaxDemand: { id: 0x040F, type: ZCLDataTypes.int24 }, // 1039, Optional + currentDayMinPressure: { id: 0x040a, type: ZCLDataTypes.uint48 }, // 1034, Optional + previousDayMaxPressure: { id: 0x040b, type: ZCLDataTypes.uint48 }, // 1035, Optional + previousDayMinPressure: { id: 0x040c, type: ZCLDataTypes.uint48 }, // 1036, Optional + currentDayMaxDemand: { id: 0x040d, type: ZCLDataTypes.int24 }, // 1037, Optional + previousDayMaxDemand: { id: 0x040e, type: ZCLDataTypes.int24 }, // 1038, Optional + currentMonthMaxDemand: { id: 0x040f, type: ZCLDataTypes.int24 }, // 1039, Optional currentYearMaxDemand: { id: 0x0410, type: ZCLDataTypes.int24 }, // 1040, Optional currentDayMaxEnergyCarrierDemand: { id: 0x0411, type: ZCLDataTypes.int24 }, // 1041, Optional previousDayMaxEnergyCarrierDemand: { id: 0x0412, type: ZCLDataTypes.int24 }, // 1042, Optional @@ -149,12 +149,12 @@ const ATTRIBUTES = { currentNoTierBlock8SummationDelivered: { id: 0x0707, type: ZCLDataTypes.uint48 }, currentNoTierBlock9SummationDelivered: { id: 0x0708, type: ZCLDataTypes.uint48 }, currentNoTierBlock10SummationDelivered: { id: 0x0709, type: ZCLDataTypes.uint48 }, - currentNoTierBlock11SummationDelivered: { id: 0x070A, type: ZCLDataTypes.uint48 }, - currentNoTierBlock12SummationDelivered: { id: 0x070B, type: ZCLDataTypes.uint48 }, - currentNoTierBlock13SummationDelivered: { id: 0x070C, type: ZCLDataTypes.uint48 }, - currentNoTierBlock14SummationDelivered: { id: 0x070D, type: ZCLDataTypes.uint48 }, - currentNoTierBlock15SummationDelivered: { id: 0x070E, type: ZCLDataTypes.uint48 }, - currentNoTierBlock16SummationDelivered: { id: 0x070F, type: ZCLDataTypes.uint48 }, + currentNoTierBlock11SummationDelivered: { id: 0x070a, type: ZCLDataTypes.uint48 }, + currentNoTierBlock12SummationDelivered: { id: 0x070b, type: ZCLDataTypes.uint48 }, + currentNoTierBlock13SummationDelivered: { id: 0x070c, type: ZCLDataTypes.uint48 }, + currentNoTierBlock14SummationDelivered: { id: 0x070d, type: ZCLDataTypes.uint48 }, + currentNoTierBlock15SummationDelivered: { id: 0x070e, type: ZCLDataTypes.uint48 }, + currentNoTierBlock16SummationDelivered: { id: 0x070f, type: ZCLDataTypes.uint48 }, currentTier1Block1SummationDelivered: { id: 0x0710, type: ZCLDataTypes.uint48 }, currentTier1Block2SummationDelivered: { id: 0x0711, type: ZCLDataTypes.uint48 }, currentTier1Block3SummationDelivered: { id: 0x0712, type: ZCLDataTypes.uint48 }, @@ -165,12 +165,12 @@ const ATTRIBUTES = { currentTier1Block8SummationDelivered: { id: 0x0717, type: ZCLDataTypes.uint48 }, currentTier1Block9SummationDelivered: { id: 0x0718, type: ZCLDataTypes.uint48 }, currentTier1Block10SummationDelivered: { id: 0x0719, type: ZCLDataTypes.uint48 }, - currentTier1Block11SummationDelivered: { id: 0x071A, type: ZCLDataTypes.uint48 }, - currentTier1Block12SummationDelivered: { id: 0x071B, type: ZCLDataTypes.uint48 }, - currentTier1Block13SummationDelivered: { id: 0x071C, type: ZCLDataTypes.uint48 }, - currentTier1Block14SummationDelivered: { id: 0x071D, type: ZCLDataTypes.uint48 }, - currentTier1Block15SummationDelivered: { id: 0x071E, type: ZCLDataTypes.uint48 }, - currentTier1Block16SummationDelivered: { id: 0x071F, type: ZCLDataTypes.uint48 }, + currentTier1Block11SummationDelivered: { id: 0x071a, type: ZCLDataTypes.uint48 }, + currentTier1Block12SummationDelivered: { id: 0x071b, type: ZCLDataTypes.uint48 }, + currentTier1Block13SummationDelivered: { id: 0x071c, type: ZCLDataTypes.uint48 }, + currentTier1Block14SummationDelivered: { id: 0x071d, type: ZCLDataTypes.uint48 }, + currentTier1Block15SummationDelivered: { id: 0x071e, type: ZCLDataTypes.uint48 }, + currentTier1Block16SummationDelivered: { id: 0x071f, type: ZCLDataTypes.uint48 }, currentTier2Block1SummationDelivered: { id: 0x0720, type: ZCLDataTypes.uint48 }, currentTier2Block2SummationDelivered: { id: 0x0721, type: ZCLDataTypes.uint48 }, currentTier2Block3SummationDelivered: { id: 0x0722, type: ZCLDataTypes.uint48 }, @@ -181,12 +181,12 @@ const ATTRIBUTES = { currentTier2Block8SummationDelivered: { id: 0x0727, type: ZCLDataTypes.uint48 }, currentTier2Block9SummationDelivered: { id: 0x0728, type: ZCLDataTypes.uint48 }, currentTier2Block10SummationDelivered: { id: 0x0729, type: ZCLDataTypes.uint48 }, - currentTier2Block11SummationDelivered: { id: 0x072A, type: ZCLDataTypes.uint48 }, - currentTier2Block12SummationDelivered: { id: 0x072B, type: ZCLDataTypes.uint48 }, - currentTier2Block13SummationDelivered: { id: 0x072C, type: ZCLDataTypes.uint48 }, - currentTier2Block14SummationDelivered: { id: 0x072D, type: ZCLDataTypes.uint48 }, - currentTier2Block15SummationDelivered: { id: 0x072E, type: ZCLDataTypes.uint48 }, - currentTier2Block16SummationDelivered: { id: 0x072F, type: ZCLDataTypes.uint48 }, + currentTier2Block11SummationDelivered: { id: 0x072a, type: ZCLDataTypes.uint48 }, + currentTier2Block12SummationDelivered: { id: 0x072b, type: ZCLDataTypes.uint48 }, + currentTier2Block13SummationDelivered: { id: 0x072c, type: ZCLDataTypes.uint48 }, + currentTier2Block14SummationDelivered: { id: 0x072d, type: ZCLDataTypes.uint48 }, + currentTier2Block15SummationDelivered: { id: 0x072e, type: ZCLDataTypes.uint48 }, + currentTier2Block16SummationDelivered: { id: 0x072f, type: ZCLDataTypes.uint48 }, currentTier3Block1SummationDelivered: { id: 0x0730, type: ZCLDataTypes.uint48 }, currentTier3Block2SummationDelivered: { id: 0x0731, type: ZCLDataTypes.uint48 }, currentTier3Block3SummationDelivered: { id: 0x0732, type: ZCLDataTypes.uint48 }, @@ -197,12 +197,12 @@ const ATTRIBUTES = { currentTier3Block8SummationDelivered: { id: 0x0737, type: ZCLDataTypes.uint48 }, currentTier3Block9SummationDelivered: { id: 0x0738, type: ZCLDataTypes.uint48 }, currentTier3Block10SummationDelivered: { id: 0x0739, type: ZCLDataTypes.uint48 }, - currentTier3Block11SummationDelivered: { id: 0x073A, type: ZCLDataTypes.uint48 }, - currentTier3Block12SummationDelivered: { id: 0x073B, type: ZCLDataTypes.uint48 }, - currentTier3Block13SummationDelivered: { id: 0x073C, type: ZCLDataTypes.uint48 }, - currentTier3Block14SummationDelivered: { id: 0x073D, type: ZCLDataTypes.uint48 }, - currentTier3Block15SummationDelivered: { id: 0x073E, type: ZCLDataTypes.uint48 }, - currentTier3Block16SummationDelivered: { id: 0x073F, type: ZCLDataTypes.uint48 }, + currentTier3Block11SummationDelivered: { id: 0x073a, type: ZCLDataTypes.uint48 }, + currentTier3Block12SummationDelivered: { id: 0x073b, type: ZCLDataTypes.uint48 }, + currentTier3Block13SummationDelivered: { id: 0x073c, type: ZCLDataTypes.uint48 }, + currentTier3Block14SummationDelivered: { id: 0x073d, type: ZCLDataTypes.uint48 }, + currentTier3Block15SummationDelivered: { id: 0x073e, type: ZCLDataTypes.uint48 }, + currentTier3Block16SummationDelivered: { id: 0x073f, type: ZCLDataTypes.uint48 }, currentTier4Block1SummationDelivered: { id: 0x0740, type: ZCLDataTypes.uint48 }, currentTier4Block2SummationDelivered: { id: 0x0741, type: ZCLDataTypes.uint48 }, currentTier4Block3SummationDelivered: { id: 0x0742, type: ZCLDataTypes.uint48 }, @@ -213,12 +213,12 @@ const ATTRIBUTES = { currentTier4Block8SummationDelivered: { id: 0x0747, type: ZCLDataTypes.uint48 }, currentTier4Block9SummationDelivered: { id: 0x0748, type: ZCLDataTypes.uint48 }, currentTier4Block10SummationDelivered: { id: 0x0749, type: ZCLDataTypes.uint48 }, - currentTier4Block11SummationDelivered: { id: 0x074A, type: ZCLDataTypes.uint48 }, - currentTier4Block12SummationDelivered: { id: 0x074B, type: ZCLDataTypes.uint48 }, - currentTier4Block13SummationDelivered: { id: 0x074C, type: ZCLDataTypes.uint48 }, - currentTier4Block14SummationDelivered: { id: 0x074D, type: ZCLDataTypes.uint48 }, - currentTier4Block15SummationDelivered: { id: 0x074E, type: ZCLDataTypes.uint48 }, - currentTier4Block16SummationDelivered: { id: 0x074F, type: ZCLDataTypes.uint48 }, + currentTier4Block11SummationDelivered: { id: 0x074a, type: ZCLDataTypes.uint48 }, + currentTier4Block12SummationDelivered: { id: 0x074b, type: ZCLDataTypes.uint48 }, + currentTier4Block13SummationDelivered: { id: 0x074c, type: ZCLDataTypes.uint48 }, + currentTier4Block14SummationDelivered: { id: 0x074d, type: ZCLDataTypes.uint48 }, + currentTier4Block15SummationDelivered: { id: 0x074e, type: ZCLDataTypes.uint48 }, + currentTier4Block16SummationDelivered: { id: 0x074f, type: ZCLDataTypes.uint48 }, // Alarms (0x0800 - 0x08FF) // TODO: map16/32/48 alarm masks — bits map to alarm codes in their group (spec 10.4.2.2.9.1) @@ -243,50 +243,49 @@ const ATTRIBUTES = { currentNoTierBlock8SummationReceived: { id: 0x0907, type: ZCLDataTypes.uint48 }, currentNoTierBlock9SummationReceived: { id: 0x0908, type: ZCLDataTypes.uint48 }, currentNoTierBlock10SummationReceived: { id: 0x0909, type: ZCLDataTypes.uint48 }, - currentNoTierBlock11SummationReceived: { id: 0x090A, type: ZCLDataTypes.uint48 }, - currentNoTierBlock12SummationReceived: { id: 0x090B, type: ZCLDataTypes.uint48 }, - currentNoTierBlock13SummationReceived: { id: 0x090C, type: ZCLDataTypes.uint48 }, - currentNoTierBlock14SummationReceived: { id: 0x090D, type: ZCLDataTypes.uint48 }, - currentNoTierBlock15SummationReceived: { id: 0x090E, type: ZCLDataTypes.uint48 }, - currentNoTierBlock16SummationReceived: { id: 0x090F, type: ZCLDataTypes.uint48 }, + currentNoTierBlock11SummationReceived: { id: 0x090a, type: ZCLDataTypes.uint48 }, + currentNoTierBlock12SummationReceived: { id: 0x090b, type: ZCLDataTypes.uint48 }, + currentNoTierBlock13SummationReceived: { id: 0x090c, type: ZCLDataTypes.uint48 }, + currentNoTierBlock14SummationReceived: { id: 0x090d, type: ZCLDataTypes.uint48 }, + currentNoTierBlock15SummationReceived: { id: 0x090e, type: ZCLDataTypes.uint48 }, + currentNoTierBlock16SummationReceived: { id: 0x090f, type: ZCLDataTypes.uint48 }, // Meter Billing (0x0A00 - 0x0AFF) - billToDateDelivered: { id: 0x0A00, type: ZCLDataTypes.uint32 }, // 2560, Optional - billToDateTimeStampDelivered: { id: 0x0A01, type: ZCLDataTypes.uint32 }, // 2561, Optional - projectedBillDelivered: { id: 0x0A02, type: ZCLDataTypes.uint32 }, // 2562, Optional - projectedBillTimeStampDelivered: { id: 0x0A03, type: ZCLDataTypes.uint32 }, // 2563, Optional + billToDateDelivered: { id: 0x0a00, type: ZCLDataTypes.uint32 }, // 2560, Optional + billToDateTimeStampDelivered: { id: 0x0a01, type: ZCLDataTypes.uint32 }, // 2561, Optional + projectedBillDelivered: { id: 0x0a02, type: ZCLDataTypes.uint32 }, // 2562, Optional + projectedBillTimeStampDelivered: { id: 0x0a03, type: ZCLDataTypes.uint32 }, // 2563, Optional // TODO: map8 packed field — most significant nibble = digits right of decimal // billDeliveredTrailingDigit: { id: 0x0A04, type: ZCLDataTypes.map8() }, // 2564, Optional - billToDateReceived: { id: 0x0A10, type: ZCLDataTypes.uint32 }, // 2576, Optional - billToDateTimeStampReceived: { id: 0x0A11, type: ZCLDataTypes.uint32 }, // 2577, Optional - projectedBillReceived: { id: 0x0A12, type: ZCLDataTypes.uint32 }, // 2578, Optional - projectedBillTimeStampReceived: { id: 0x0A13, type: ZCLDataTypes.uint32 }, // 2579, Optional + billToDateReceived: { id: 0x0a10, type: ZCLDataTypes.uint32 }, // 2576, Optional + billToDateTimeStampReceived: { id: 0x0a11, type: ZCLDataTypes.uint32 }, // 2577, Optional + projectedBillReceived: { id: 0x0a12, type: ZCLDataTypes.uint32 }, // 2578, Optional + projectedBillTimeStampReceived: { id: 0x0a13, type: ZCLDataTypes.uint32 }, // 2579, Optional // TODO: map8 packed field — most significant nibble = digits right of decimal // billReceivedTrailingDigit: { id: 0x0A14, type: ZCLDataTypes.map8() }, // 2580, Optional // Supply Control (0x0B00 - 0x0BFF) // Optional proposedChangeSupplyImplementationTime: { - id: 0x0B00, // 2816 + id: 0x0b00, // 2816 type: ZCLDataTypes.uint32, }, /* 2817, Optional */ // proposedChangeSupplyStatus: { id: 0x0B01, type: ZCLDataTypes.enum8({/* TODO fix */}) }, - uncontrolledFlowThreshold: { id: 0x0B10, type: ZCLDataTypes.uint16 }, // 2832, Optional + uncontrolledFlowThreshold: { id: 0x0b10, type: ZCLDataTypes.uint16 }, // 2832, Optional // uncontrolledFlowThresholdUnitOfMeasure: { // Optional // id: 0x0B11, // 2833 // type: ZCLDataTypes.enum8({/* TODO fix */}), // }, - uncontrolledFlowMultiplier: { id: 0x0B12, type: ZCLDataTypes.uint16 }, // 2834, Optional - uncontrolledFlowDivisor: { id: 0x0B13, type: ZCLDataTypes.uint16 }, // 2835, Optional - flowStabilisationPeriod: { id: 0x0B14, type: ZCLDataTypes.uint8 }, // 2836, Optional - flowMeasurementPeriod: { id: 0x0B15, type: ZCLDataTypes.uint16 }, // 2837, Optional + uncontrolledFlowMultiplier: { id: 0x0b12, type: ZCLDataTypes.uint16 }, // 2834, Optional + uncontrolledFlowDivisor: { id: 0x0b13, type: ZCLDataTypes.uint16 }, // 2835, Optional + flowStabilisationPeriod: { id: 0x0b14, type: ZCLDataTypes.uint8 }, // 2836, Optional + flowMeasurementPeriod: { id: 0x0b15, type: ZCLDataTypes.uint16 }, // 2837, Optional }; const COMMANDS = {}; class MeteringCluster extends Cluster { - static get ID() { return 0x0702; // 1794 } @@ -302,7 +301,6 @@ class MeteringCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(MeteringCluster); diff --git a/lib/clusters/multistateInput.js b/lib/clusters/multistateInput.js index 889c6eeb..882fce74 100644 --- a/lib/clusters/multistateInput.js +++ b/lib/clusters/multistateInput.js @@ -24,14 +24,16 @@ const ATTRIBUTES = { configurationError: 10, // CONFIGURATION-ERROR (10) }), }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class MultistateInputCluster extends Cluster { - static get ID() { return 18; // 0x12 } @@ -47,7 +49,6 @@ class MultistateInputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(MultistateInputCluster); diff --git a/lib/clusters/multistateOutput.js b/lib/clusters/multistateOutput.js index b737fa42..636fc868 100644 --- a/lib/clusters/multistateOutput.js +++ b/lib/clusters/multistateOutput.js @@ -23,14 +23,16 @@ const ATTRIBUTES = { }), }, relinquishDefault: { id: 104, type: ZCLDataTypes.uint16 }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class MultistateOutputCluster extends Cluster { - static get ID() { return 19; } @@ -46,7 +48,6 @@ class MultistateOutputCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(MultistateOutputCluster); diff --git a/lib/clusters/multistateValue.js b/lib/clusters/multistateValue.js index 157bb6c6..d1f678ee 100644 --- a/lib/clusters/multistateValue.js +++ b/lib/clusters/multistateValue.js @@ -25,14 +25,16 @@ const ATTRIBUTES = { }), }, relinquishDefault: { id: 104, type: ZCLDataTypes.uint16 }, - statusFlags: { id: 111, type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService') }, + statusFlags: { + id: 111, + type: ZCLDataTypes.map8('inAlarm', 'fault', 'overridden', 'outOfService'), + }, applicationType: { id: 256, type: ZCLDataTypes.uint32 }, }; const COMMANDS = {}; class MultistateValueCluster extends Cluster { - static get ID() { return 20; } @@ -48,7 +50,6 @@ class MultistateValueCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(MultistateValueCluster); diff --git a/lib/clusters/occupancySensing.js b/lib/clusters/occupancySensing.js index 4baf6623..905e2d52 100644 --- a/lib/clusters/occupancySensing.js +++ b/lib/clusters/occupancySensing.js @@ -68,7 +68,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class OccupancySensing extends Cluster { - static get ID() { return 0x0406; // 1030 } @@ -84,7 +83,6 @@ class OccupancySensing extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(OccupancySensing); diff --git a/lib/clusters/onOff.js b/lib/clusters/onOff.js index 038e2e91..4717e2bc 100644 --- a/lib/clusters/onOff.js +++ b/lib/clusters/onOff.js @@ -42,7 +42,6 @@ const COMMANDS = { }; class OnOffCluster extends Cluster { - static get ID() { return 6; } @@ -58,7 +57,6 @@ class OnOffCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(OnOffCluster); diff --git a/lib/clusters/onOffSwitch.js b/lib/clusters/onOffSwitch.js index 81a32197..bd5e9404 100644 --- a/lib/clusters/onOffSwitch.js +++ b/lib/clusters/onOffSwitch.js @@ -11,7 +11,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class OnOffSwitchCluster extends Cluster { - static get ID() { return 7; } @@ -27,7 +26,6 @@ class OnOffSwitchCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(OnOffSwitchCluster); diff --git a/lib/clusters/ota.js b/lib/clusters/ota.js index b13e3a95..5a2cc13a 100644 --- a/lib/clusters/ota.js +++ b/lib/clusters/ota.js @@ -66,13 +66,13 @@ const ATTRIBUTES = { // A 32 bit value used as a second verification to identify the image. The value // must be consistent during the lifetime of the same image and unique for each // different build of the image. - imageStamp: { id: 0x000A, type: ZCLDataTypes.uint32 }, // 10, Optional + imageStamp: { id: 0x000a, type: ZCLDataTypes.uint32 }, // 10, Optional // Indicates what behavior the client device supports for activating a fully // downloaded but not installed upgrade image. // Optional upgradeActivationPolicy: { - id: 0x000B, // 11 + id: 0x000b, // 11 type: ZCLDataTypes.enum8({ otaServerActivationAllowed: 0x00, outOfBandActivationOnly: 0x01, @@ -83,7 +83,7 @@ const ATTRIBUTES = { // command cannot be retrieved. // Optional upgradeTimeoutPolicy: { - id: 0x000C, // 12 + id: 0x000c, // 12 type: ZCLDataTypes.enum8({ applyUpgradeAfterTimeout: 0x00, doNotApplyUpgradeAfterTimeout: 0x01, @@ -189,10 +189,7 @@ const COMMANDS = { id: 0x0003, encodeMissingFieldsBehavior: 'skip', args: { - fieldControl: ZCLDataTypes.map8( - 'requestNodeAddressPresent', - 'minimumBlockPeriodPresent', - ), + fieldControl: ZCLDataTypes.map8('requestNodeAddressPresent', 'minimumBlockPeriodPresent'), manufacturerCode: ZCLDataTypes.uint16, imageType: ZCLDataTypes.uint16, fileVersion: ZCLDataTypes.uint32, @@ -285,11 +282,9 @@ const COMMANDS = { }, }, }, - }; class OTACluster extends Cluster { - static get ID() { return 0x0019; // 25 } @@ -305,7 +300,6 @@ class OTACluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(OTACluster); diff --git a/lib/clusters/pollControl.js b/lib/clusters/pollControl.js index a29aa485..681f9f0d 100644 --- a/lib/clusters/pollControl.js +++ b/lib/clusters/pollControl.js @@ -30,7 +30,6 @@ const COMMANDS = { }; class PollControlCluster extends Cluster { - static get ID() { return 32; // 0x0020 } @@ -46,7 +45,6 @@ class PollControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(PollControlCluster); diff --git a/lib/clusters/powerConfiguration.js b/lib/clusters/powerConfiguration.js index 7123b598..5ce20a95 100644 --- a/lib/clusters/powerConfiguration.js +++ b/lib/clusters/powerConfiguration.js @@ -65,7 +65,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class PowerConfigurationCluster extends Cluster { - static get ID() { return 1; } @@ -81,7 +80,6 @@ class PowerConfigurationCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(PowerConfigurationCluster); diff --git a/lib/clusters/powerProfile.js b/lib/clusters/powerProfile.js index a26a098f..e386dd5b 100644 --- a/lib/clusters/powerProfile.js +++ b/lib/clusters/powerProfile.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class PowerProfileCluster extends Cluster { - static get ID() { return 26; } @@ -24,7 +22,6 @@ class PowerProfileCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(PowerProfileCluster); diff --git a/lib/clusters/pressureMeasurement.js b/lib/clusters/pressureMeasurement.js index 4b8dbc47..92690122 100644 --- a/lib/clusters/pressureMeasurement.js +++ b/lib/clusters/pressureMeasurement.js @@ -18,7 +18,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class PressureMeasurementCluster extends Cluster { - static get ID() { return 1027; } @@ -34,7 +33,6 @@ class PressureMeasurementCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(PressureMeasurementCluster); diff --git a/lib/clusters/pumpConfigurationAndControl.js b/lib/clusters/pumpConfigurationAndControl.js index 3fd832c2..f69691e0 100644 --- a/lib/clusters/pumpConfigurationAndControl.js +++ b/lib/clusters/pumpConfigurationAndControl.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class PumpConfigurationAndControlCluster extends Cluster { - static get ID() { return 512; } @@ -24,7 +22,6 @@ class PumpConfigurationAndControlCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(PumpConfigurationAndControlCluster); diff --git a/lib/clusters/relativeHumidity.js b/lib/clusters/relativeHumidity.js index 526947c8..8f18fcbf 100644 --- a/lib/clusters/relativeHumidity.js +++ b/lib/clusters/relativeHumidity.js @@ -13,7 +13,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class RelativeHumidityCluster extends Cluster { - static get ID() { return 1029; } @@ -29,7 +28,6 @@ class RelativeHumidityCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(RelativeHumidityCluster); diff --git a/lib/clusters/scenes.js b/lib/clusters/scenes.js index c15deaac..4c56bb19 100644 --- a/lib/clusters/scenes.js +++ b/lib/clusters/scenes.js @@ -7,7 +7,6 @@ const ATTRIBUTES = {}; const COMMANDS = {}; class ScenesCluster extends Cluster { - static get ID() { return 5; } @@ -23,7 +22,6 @@ class ScenesCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ScenesCluster); diff --git a/lib/clusters/shadeConfiguration.js b/lib/clusters/shadeConfiguration.js index 3ede3b26..939308c8 100644 --- a/lib/clusters/shadeConfiguration.js +++ b/lib/clusters/shadeConfiguration.js @@ -2,13 +2,11 @@ const Cluster = require('../Cluster'); -const ATTRIBUTES = { -}; +const ATTRIBUTES = {}; const COMMANDS = {}; class ShadeConfigurationCluster extends Cluster { - static get ID() { return 256; } @@ -24,7 +22,6 @@ class ShadeConfigurationCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ShadeConfigurationCluster); diff --git a/lib/clusters/temperatureMeasurement.js b/lib/clusters/temperatureMeasurement.js index c5f8510b..b0f8ac8d 100644 --- a/lib/clusters/temperatureMeasurement.js +++ b/lib/clusters/temperatureMeasurement.js @@ -12,7 +12,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class TemperatureMeasurement extends Cluster { - static get ID() { return 1026; // 0x0402 } @@ -28,7 +27,6 @@ class TemperatureMeasurement extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(TemperatureMeasurement); diff --git a/lib/clusters/thermostat.js b/lib/clusters/thermostat.js index e8d73ce7..47101aed 100644 --- a/lib/clusters/thermostat.js +++ b/lib/clusters/thermostat.js @@ -23,7 +23,10 @@ const ATTRIBUTES = { minCoolSetpointLimit: { id: 23, type: ZCLDataTypes.int16 }, maxCoolSetpointLimit: { id: 24, type: ZCLDataTypes.int16 }, minSetpointDeadBand: { id: 25, type: ZCLDataTypes.int8 }, - remoteSensing: { id: 26, type: ZCLDataTypes.map8('localTemperature', 'outdoorTemperature', 'occupancy') }, + remoteSensing: { + id: 26, + type: ZCLDataTypes.map8('localTemperature', 'outdoorTemperature', 'occupancy'), + }, controlSequenceOfOperation: { id: 27, type: ZCLDataTypes.enum8({ @@ -49,7 +52,10 @@ const ATTRIBUTES = { sleep: 9, }), }, - alarmMask: { id: 29, type: ZCLDataTypes.map8('initializationFailure', 'hardwareFailure', 'selfCalibrationFailure') }, + alarmMask: { + id: 29, + type: ZCLDataTypes.map8('initializationFailure', 'hardwareFailure', 'selfCalibrationFailure'), + }, runningMode: { id: 30, type: ZCLDataTypes.enum8({ @@ -75,7 +81,6 @@ const COMMANDS = { }; class ThermostatCluster extends Cluster { - static get ID() { return 513; } @@ -91,7 +96,6 @@ class ThermostatCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ThermostatCluster); diff --git a/lib/clusters/thermostatUserInterfaceConfiguration.js b/lib/clusters/thermostatUserInterfaceConfiguration.js index 09531e39..74ae5c3f 100644 --- a/lib/clusters/thermostatUserInterfaceConfiguration.js +++ b/lib/clusters/thermostatUserInterfaceConfiguration.js @@ -34,7 +34,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class ThermostatUserInterfaceConfigurationCluster extends Cluster { - static get ID() { return 516; } @@ -50,7 +49,6 @@ class ThermostatUserInterfaceConfigurationCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(ThermostatUserInterfaceConfigurationCluster); diff --git a/lib/clusters/time.js b/lib/clusters/time.js index bf2d8242..2bf0283e 100644 --- a/lib/clusters/time.js +++ b/lib/clusters/time.js @@ -5,7 +5,10 @@ const { ZCLDataTypes } = require('../zclTypes'); const ATTRIBUTES = { time: { id: 0, type: ZCLDataTypes.UTC }, - timeStatus: { id: 1, type: ZCLDataTypes.map8('master', 'synchronized', 'masterZoneDst', 'superseding') }, + timeStatus: { + id: 1, + type: ZCLDataTypes.map8('master', 'synchronized', 'masterZoneDst', 'superseding'), + }, timeZone: { id: 2, type: ZCLDataTypes.int32 }, dstStart: { id: 3, type: ZCLDataTypes.uint32 }, dstEnd: { id: 4, type: ZCLDataTypes.uint32 }, @@ -19,7 +22,6 @@ const ATTRIBUTES = { const COMMANDS = {}; class TimeCluster extends Cluster { - static get ID() { return 10; // 0xA } @@ -35,7 +37,6 @@ class TimeCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(TimeCluster); diff --git a/lib/clusters/touchlink.js b/lib/clusters/touchlink.js index c548da4e..407aafed 100644 --- a/lib/clusters/touchlink.js +++ b/lib/clusters/touchlink.js @@ -16,17 +16,18 @@ const COMMANDS = { args: { total: ZCLDataTypes.uint8, startIndex: ZCLDataTypes.uint8, - groups: ZCLDataTypes.Array8(ZCLStruct('TLLGroupResponse', { - groupId: ZCLDataTypes.uint16, - groupType: ZCLDataTypes.uint8, - })), + groups: ZCLDataTypes.Array8( + ZCLStruct('TLLGroupResponse', { + groupId: ZCLDataTypes.uint16, + groupType: ZCLDataTypes.uint8, + }), + ), }, }, }, }; class TouchlinkCluster extends Cluster { - static get ID() { return 4096; // 0x1000 } @@ -42,7 +43,6 @@ class TouchlinkCluster extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(TouchlinkCluster); diff --git a/lib/clusters/windowCovering.js b/lib/clusters/windowCovering.js index 572b4e52..32f194e3 100644 --- a/lib/clusters/windowCovering.js +++ b/lib/clusters/windowCovering.js @@ -103,7 +103,6 @@ const COMMANDS = { }; class WindowCovering extends Cluster { - static get ID() { return 0x0102; // 258 } @@ -119,7 +118,6 @@ class WindowCovering extends Cluster { static get COMMANDS() { return COMMANDS; } - } Cluster.addCluster(WindowCovering); diff --git a/lib/util/index.js b/lib/util/index.js index 6ab457f4..6d8d2a04 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -5,8 +5,10 @@ const debug = require('debug')('zigbee-clusters'); function getPropertyDescriptor(obj, name) { if (!obj) return; - return Object.getOwnPropertyDescriptor(obj, name) - || getPropertyDescriptor(Object.getPrototypeOf(obj), name); + return ( + Object.getOwnPropertyDescriptor(obj, name) || + getPropertyDescriptor(Object.getPrototypeOf(obj), name) + ); } /** @@ -21,12 +23,10 @@ function getLogId(endpointId, clusterName, clusterId) { } class ZCLError extends Error { - constructor(zclStatus = 'FAILURE') { super(`ZCL Error: ${zclStatus}`); this.zclStatus = zclStatus; } - } module.exports = { diff --git a/lib/zclFrames.js b/lib/zclFrames.js index 525155df..9b0c3949 100644 --- a/lib/zclFrames.js +++ b/lib/zclFrames.js @@ -2,7 +2,13 @@ const { ZCLDataType, ZCLDataTypes, ZCLStruct } = require('./zclTypes'); -const ZCLFrameControlBitmap = ZCLDataTypes.map8('clusterSpecific', null, 'manufacturerSpecific', 'directionToClient', 'disableDefaultResponse'); +const ZCLFrameControlBitmap = ZCLDataTypes.map8( + 'clusterSpecific', + null, + 'manufacturerSpecific', + 'directionToClient', + 'disableDefaultResponse', +); const ZCLStandardHeader = ZCLStruct('ZCLStandardHeader', { frameControl: ZCLFrameControlBitmap, @@ -32,241 +38,278 @@ const ZCLConfigureReportingRecordStruct = ZCLStruct('ConfigureReportingRecord', timeoutPeriod: ZCLDataTypes.uint16, }); -const ZCLAttributeDataRecord = (withStatus, attributes) => new ZCLDataType(NaN, 'attributeRecord', -3, ((buf, v, i) => { - const startByte = i; - i += ZCLDataTypes.uint16.toBuffer(buf, v.id, i); - if (withStatus) { - i += ZCLDataTypes.enum8Status.toBuffer(buf, v.status, i); - } - if (!withStatus || v.status === 'SUCCESS') { - i += ZCLDataTypes.uint8.toBuffer(buf, attributes[v.id].type.id, i); - i += attributes[v.id].type.toBuffer(buf, v.value, i); - } - return i - startByte; -}), function fromBuf(buf, i, returnLength) { - i = i || 0; - const startByte = i; - const res = {}; - if (buf.length >= i + Math.abs(this.length)) { - res.id = ZCLDataTypes.uint16.fromBuffer(buf, i); - i += ZCLDataTypes.uint16.length; - - if (withStatus) { - res.status = ZCLDataTypes.enum8Status.fromBuffer(buf, i); - i += ZCLDataTypes.enum8Status.length; - } - - if (!withStatus || res.status === 'SUCCESS') { - const dataTypeId = ZCLDataTypes.uint8.fromBuffer(buf, i); - i += ZCLDataTypes.uint8.length; - - // don't use the attribute's type if the type in the data is 0 (noData). - // This breaks when the attribute ID is 0 - const DataType = attributes[res.id] && dataTypeId !== 0 - ? attributes[res.id].type - : Object.values(ZCLDataTypes).find(type => type && type.id === dataTypeId); - if (!DataType) throw new TypeError(`Invalid Type for Attribute: ${res.id}`); - - if (attributes[res.id]) { - res.name = attributes[res.id].name || `unknown_attr_${res.id}`; +const ZCLAttributeDataRecord = (withStatus, attributes) => + new ZCLDataType( + NaN, + 'attributeRecord', + -3, + (buf, v, i) => { + const startByte = i; + i += ZCLDataTypes.uint16.toBuffer(buf, v.id, i); + if (withStatus) { + i += ZCLDataTypes.enum8Status.toBuffer(buf, v.status, i); } - - const entry = DataType.fromBuffer(buf, i, true); - if (DataType.length > 0) { - i += DataType.length; - res.value = entry; - } else { - res.value = entry.result; - i += entry.length; + if (!withStatus || v.status === 'SUCCESS') { + i += ZCLDataTypes.uint8.toBuffer(buf, attributes[v.id].type.id, i); + i += attributes[v.id].type.toBuffer(buf, v.value, i); } - } - } - if (returnLength) { - return { result: res, length: i - startByte }; - } - return res; -}); + return i - startByte; + }, + function fromBuf(buf, i, returnLength) { + i = i || 0; + const startByte = i; + const res = {}; + if (buf.length >= i + Math.abs(this.length)) { + res.id = ZCLDataTypes.uint16.fromBuffer(buf, i); + i += ZCLDataTypes.uint16.length; + + if (withStatus) { + res.status = ZCLDataTypes.enum8Status.fromBuffer(buf, i); + i += ZCLDataTypes.enum8Status.length; + } -const ZCLConfigureReportingRecords = ({ withStatus } = {}) => new ZCLDataType( - NaN, - 'configureReportingRecords', - -0, - ((buf, value, i) => { - // toBuffer - const startByte = i; + if (!withStatus || res.status === 'SUCCESS') { + const dataTypeId = ZCLDataTypes.uint8.fromBuffer(buf, i); + i += ZCLDataTypes.uint8.length; - // Loop each configure reporting record - for (const configureReportingRecord of value) { - // Remove the minChange property (because its size will vary) - const { minChange, direction } = configureReportingRecord; + // don't use the attribute's type if the type in the data is 0 (noData). + // This breaks when the attribute ID is 0 + const DataType = + attributes[res.id] && dataTypeId !== 0 + ? attributes[res.id].type + : Object.values(ZCLDataTypes).find((type) => type && type.id === dataTypeId); + if (!DataType) throw new TypeError(`Invalid Type for Attribute: ${res.id}`); - // Check if this record is expected to have a status field - if (withStatus) { - ZCLDataTypes.enum8Status.toBuffer(buf, configureReportingRecord.status, i); - i += ZCLDataTypes.enum8Status.length; - } + if (attributes[res.id]) { + res.name = attributes[res.id].name || `unknown_attr_${res.id}`; + } - ZCLConfigureReportingRecordStruct.fields.direction - .toBuffer(buf, configureReportingRecord.direction, i); - i += ZCLConfigureReportingRecordStruct.fields.direction.length; + const entry = DataType.fromBuffer(buf, i, true); + if (DataType.length > 0) { + i += DataType.length; + res.value = entry; + } else { + res.value = entry.result; + i += entry.length; + } + } + } + if (returnLength) { + return { result: res, length: i - startByte }; + } + return res; + }, + ); + +const ZCLConfigureReportingRecords = ({ withStatus } = {}) => + new ZCLDataType( + NaN, + 'configureReportingRecords', + -0, + (buf, value, i) => { + // toBuffer + const startByte = i; + + // Loop each configure reporting record + for (const configureReportingRecord of value) { + // Remove the minChange property (because its size will vary) + const { minChange, direction } = configureReportingRecord; + + // Check if this record is expected to have a status field + if (withStatus) { + ZCLDataTypes.enum8Status.toBuffer(buf, configureReportingRecord.status, i); + i += ZCLDataTypes.enum8Status.length; + } - ZCLConfigureReportingRecordStruct.fields.attributeId - .toBuffer(buf, configureReportingRecord.attributeId, i); - i += ZCLConfigureReportingRecordStruct.fields.attributeId.length; + ZCLConfigureReportingRecordStruct.fields.direction.toBuffer( + buf, + configureReportingRecord.direction, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.direction.length; + + ZCLConfigureReportingRecordStruct.fields.attributeId.toBuffer( + buf, + configureReportingRecord.attributeId, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.attributeId.length; + + // Nothing further to parse if it was not a success + if (withStatus && configureReportingRecord.status !== 'SUCCESS') { + continue; + } - // Nothing further to parse if it was not a success - if (withStatus && configureReportingRecord.status !== 'SUCCESS') { - continue; + if (direction === 'reported') { + // Determine the type of the minChange property (equal to attributeDataType of this record) + const minChangeDataType = Object.values(ZCLDataTypes).find( + (DataType) => DataType.id === configureReportingRecord.attributeDataType, + ); + + ZCLConfigureReportingRecordStruct.fields.attributeDataType.toBuffer( + buf, + configureReportingRecord.attributeDataType, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.attributeDataType.length; + + ZCLConfigureReportingRecordStruct.fields.minInterval.toBuffer( + buf, + configureReportingRecord.minInterval, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.minInterval.length; + + ZCLConfigureReportingRecordStruct.fields.maxInterval.toBuffer( + buf, + configureReportingRecord.maxInterval, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.maxInterval.length; + + if (minChangeDataType && typeof minChange === 'number') { + minChangeDataType.toBuffer(buf, minChange, i); + i += minChangeDataType.length; + } + } else { + ZCLConfigureReportingRecordStruct.fields.timeoutPeriod.toBuffer( + buf, + configureReportingRecord.timeoutPeriod, + i, + ); + i += ZCLConfigureReportingRecordStruct.fields.timeoutPeriod.length; + } } - if (direction === 'reported') { - // Determine the type of the minChange property (equal to attributeDataType of this record) - const minChangeDataType = Object.values(ZCLDataTypes) - .find(DataType => DataType.id === configureReportingRecord.attributeDataType); + return i - startByte; + }, + (buf, i, returnLength) => { + // fromBuffer + i = i || 0; + // Keep a reference to the current buffer index while reading it + const startByte = i; + const res = []; + + // Read the entire buffer + while (buf.length > i) { + // Parse status only if part of record struct (the read configuration response has a + // status field whereas the configure reporting command does not) + let status; + + // Check if this record is expected to have a status field + if (withStatus) { + status = ZCLDataTypes.enum8Status.fromBuffer(buf, i); + i += ZCLDataTypes.enum8Status.length; + } - ZCLConfigureReportingRecordStruct.fields.attributeDataType - .toBuffer(buf, configureReportingRecord.attributeDataType, i); - i += ZCLConfigureReportingRecordStruct.fields.attributeDataType.length; + // Parse direction + const direction = ZCLConfigureReportingRecordStruct.fields.direction.fromBuffer(buf, i); + i += 1; - ZCLConfigureReportingRecordStruct.fields.minInterval - .toBuffer(buf, configureReportingRecord.minInterval, i); - i += ZCLConfigureReportingRecordStruct.fields.minInterval.length; + // Parse attributeId + const attributeId = ZCLConfigureReportingRecordStruct.fields.attributeId.fromBuffer(buf, i); + i += 2; - ZCLConfigureReportingRecordStruct.fields.maxInterval - .toBuffer(buf, configureReportingRecord.maxInterval, i); - i += ZCLConfigureReportingRecordStruct.fields.maxInterval.length; + // Check if the status is success, only then we need to continue parsing for other values + if (withStatus && status !== 'SUCCESS') { + // Buffer parsing is complete + res.push({ status, direction, attributeId }); - if (minChangeDataType && typeof minChange === 'number') { - minChangeDataType.toBuffer(buf, minChange, i); - i += minChangeDataType.length; + // Continue reading the buffer in a next iteration + continue; } - } else { - ZCLConfigureReportingRecordStruct.fields.timeoutPeriod - .toBuffer(buf, configureReportingRecord.timeoutPeriod, i); - i += ZCLConfigureReportingRecordStruct.fields.timeoutPeriod.length; - } - } - - return i - startByte; - }), ((buf, i, returnLength) => { - // fromBuffer - i = i || 0; - // Keep a reference to the current buffer index while reading it - const startByte = i; - const res = []; - - // Read the entire buffer - while (buf.length > i) { - // Parse status only if part of record struct (the read configuration response has a - // status field whereas the configure reporting command does not) - let status; - - // Check if this record is expected to have a status field - if (withStatus) { - status = ZCLDataTypes.enum8Status.fromBuffer(buf, i); - i += ZCLDataTypes.enum8Status.length; - } - // Parse direction - const direction = ZCLConfigureReportingRecordStruct.fields.direction.fromBuffer(buf, i); - i += 1; + switch (direction) { + // Only if the direction is 'reported' are the minInterval/maxInterval and minChange + // properties available + case 'reported': { + // Parse attributeDataType, we need this to find the respective ZCLDataType in the + // next step + const attributeDataType = + ZCLConfigureReportingRecordStruct.fields.attributeDataType.fromBuffer(buf, i); + i += 1; + + // Find the respective ZCLDataType, we need it later on to determine if the data type + // is analog or discrete + const parsedAttributeDataType = Object.values(ZCLDataTypes).find( + (x) => x.id === attributeDataType, + ); + + // Parse minInterval + const minInterval = ZCLDataTypes.uint16.fromBuffer(buf, i); + i += 2; + + // Parse maxInterval + const maxInterval = ZCLDataTypes.uint16.fromBuffer(buf, i); + i += 2; + + // If the ZCLDataType is not analog there will be no minChange field + if (!parsedAttributeDataType.isAnalog) { + const resultRecord = { + direction, + attributeId, + attributeDataType, + minInterval, + maxInterval, + }; + if (withStatus) resultRecord.status = status; + res.push(resultRecord); + + // Continue reading the buffer in a next iteration + continue; + } + + // Apparently the ZCLDataType is analog so we should parse the minChange field + const minChange = ZCLDataTypes[parsedAttributeDataType.shortName].fromBuffer(buf, i); + i += parsedAttributeDataType.length; - // Parse attributeId - const attributeId = ZCLConfigureReportingRecordStruct.fields.attributeId.fromBuffer(buf, i); - i += 2; - - // Check if the status is success, only then we need to continue parsing for other values - if (withStatus && status !== 'SUCCESS') { - // Buffer parsing is complete - res.push({ status, direction, attributeId }); - - // Continue reading the buffer in a next iteration - continue; - } - - switch (direction) { - // Only if the direction is 'reported' are the minInterval/maxInterval and minChange - // properties available - case 'reported': { - // Parse attributeDataType, we need this to find the respective ZCLDataType in the - // next step - const attributeDataType = ZCLConfigureReportingRecordStruct.fields.attributeDataType - .fromBuffer(buf, i); - i += 1; - - // Find the respective ZCLDataType, we need it later on to determine if the data type - // is analog or discrete - const parsedAttributeDataType = Object.values(ZCLDataTypes) - .find(x => x.id === attributeDataType); - - // Parse minInterval - const minInterval = ZCLDataTypes.uint16.fromBuffer(buf, i); - i += 2; - - // Parse maxInterval - const maxInterval = ZCLDataTypes.uint16.fromBuffer(buf, i); - i += 2; - - // If the ZCLDataType is not analog there will be no minChange field - if (!parsedAttributeDataType.isAnalog) { const resultRecord = { - direction, attributeId, attributeDataType, minInterval, maxInterval, + direction, + attributeId, + attributeDataType, + minInterval, + maxInterval, + minChange, }; + if (withStatus) resultRecord.status = status; res.push(resultRecord); - // Continue reading the buffer in a next iteration - continue; + // Buffer parsing is complete + break; } - // Apparently the ZCLDataType is analog so we should parse the minChange field - const minChange = ZCLDataTypes[parsedAttributeDataType.shortName] - .fromBuffer(buf, i); - i += parsedAttributeDataType.length; - - const resultRecord = { - direction, - attributeId, - attributeDataType, - minInterval, - maxInterval, - minChange, - }; - - if (withStatus) resultRecord.status = status; - res.push(resultRecord); - - // Buffer parsing is complete - break; - } - - // If the direction is 'received' we only have to parse the - case 'received': { - // timeoutPeriod - // Parse timeout period - const timeoutPeriod = ZCLConfigureReportingRecordStruct.fields.timeoutPeriod - .fromBuffer(buf, i); - i += 2; + // If the direction is 'received' we only have to parse the + case 'received': { + // timeoutPeriod + // Parse timeout period + const timeoutPeriod = ZCLConfigureReportingRecordStruct.fields.timeoutPeriod.fromBuffer( + buf, + i, + ); + i += 2; - const resultRecord = { - direction, attributeId, timeoutPeriod, - }; + const resultRecord = { + direction, + attributeId, + timeoutPeriod, + }; - if (withStatus) resultRecord.status = status; - res.push(resultRecord); + if (withStatus) resultRecord.status = status; + res.push(resultRecord); - // Buffer parsing is complete - break; + // Buffer parsing is complete + break; + } } } - } - if (returnLength) { - return { result: res, length: i - startByte }; - } - return res; - }), -); + if (returnLength) { + return { result: res, length: i - startByte }; + } + return res; + }, + ); module.exports = { ZCLStandardHeader, diff --git a/lib/zclTypes.js b/lib/zclTypes.js index dee27bb1..e8706977 100644 --- a/lib/zclTypes.js +++ b/lib/zclTypes.js @@ -6,6 +6,7 @@ const ZCLDataType = DataType; const ZCLDataTypes = DataTypes; const ZCLStruct = Struct; +// prettier-ignore ZCLDataTypes.enum8Status = ZCLDataTypes.enum8({ SUCCESS : 0x00, FAILURE : 0x01, diff --git a/scripts/generate-types.mts b/scripts/generate-types.mts index b480d340..02188170 100644 --- a/scripts/generate-types.mts +++ b/scripts/generate-types.mts @@ -1,7 +1,7 @@ -import type {Cluster, types} from "zigbee-clusters"; -import path from "node:path"; -import * as fs from "node:fs/promises"; -import type {DataType, DataTypes} from "@athombv/data-types"; +import type { Cluster, types } from 'zigbee-clusters'; +import path from 'node:path'; +import * as fs from 'node:fs/promises'; +import type { DataType, DataTypes } from '@athombv/data-types'; const INDENTATION_SPACES = 2; @@ -15,13 +15,13 @@ class StringBuilder { } public startLine(): void { - this.strings.push(" ".repeat(this.indentation)); + this.strings.push(' '.repeat(this.indentation)); } public print(...line: string[]): void { this.strings.push(...line); } public endLine(): void { - this.strings.push("\n"); + this.strings.push('\n'); } public printLine(...line: string[]): void { @@ -30,7 +30,6 @@ class StringBuilder { this.endLine(); } - public increaseIndent(levels = 1): void { this.indentation += INDENTATION_SPACES * levels; } @@ -45,7 +44,7 @@ class StringBuilder { } public toString(): string { - return this.strings.join(""); + return this.strings.join(''); } } @@ -62,12 +61,12 @@ async function main(): Promise { const stringBuilder = new StringBuilder(); // Write preamble and open module declaration - stringBuilder.printLine('declare module "zigbee-clusters" {') + stringBuilder.printLine('declare module "zigbee-clusters" {'); stringBuilder.increaseIndent(); - stringBuilder.printLine('import {Bitmap} from "@athombv/data-types";') + stringBuilder.printLine('import {Bitmap} from "@athombv/data-types";'); // @ts-expect-error No type declarations - const clustersModule = await import("../lib/clusters/index.js"); + const clustersModule = await import('../lib/clusters/index.js'); for (const [name, value] of Object.entries(clustersModule.default)) { if (name !== 'Cluster' && name !== 'CLUSTER') { await formatCluster(stringBuilder, name, value as typeof Cluster); @@ -75,7 +74,7 @@ async function main(): Promise { } // Open CLUSTER - stringBuilder.printLine("const CLUSTER: {"); + stringBuilder.printLine('const CLUSTER: {'); stringBuilder.increaseIndent(); // Collect (NAME, ClassName) pairs to emit a typed lookup map after the @@ -84,7 +83,7 @@ async function main(): Promise { const clusterClassByName: Array<{ name: string; className: string }> = []; for (const [key, value] of Object.entries(clustersModule.default.CLUSTER)) { - const definition = value as {ID: number, NAME: string}; + const definition = value as { ID: number; NAME: string }; // `lib/clusters/index.js` no longer re-exports `Cluster` (PR #182), so // reach the static `Cluster.clusters` registry via the top-level entry. const clusterClass = topModule.Cluster.clusters[definition.ID]; @@ -93,13 +92,13 @@ async function main(): Promise { if (clusterName === 'TouchlinkCluster') { clusterName = 'TouchLinkCluster'; } - if (!clusterName.endsWith("Cluster")) { + if (!clusterName.endsWith('Cluster')) { clusterName = `${clusterName}Cluster`; } clusterClassByName.push({ name: definition.NAME, className: clusterName }); stringBuilder.printLine(`${key}: {`); stringBuilder.increaseIndent(); - stringBuilder.printLine(`ID: 0x${definition.ID.toString(16).padStart(4, "0")},`); + stringBuilder.printLine(`ID: 0x${definition.ID.toString(16).padStart(4, '0')},`); stringBuilder.printLine(`NAME: ${JSON.stringify(definition.NAME)},`); stringBuilder.printLine(`ATTRIBUTES: Readonly<${clusterName}Attributes>,`); stringBuilder.printLine(`COMMANDS: Readonly<${clusterName}Commands>,`); @@ -109,20 +108,20 @@ async function main(): Promise { // Close CLUSTER stringBuilder.decreaseIndent(); - stringBuilder.printLine("};"); + stringBuilder.printLine('};'); // Emit a name->class lookup map used by `ZCLNodeEndpoint.clusters` for // typed access to known cluster classes. Keys are optional because an // endpoint may not implement every cluster; an unknown cluster name // (e.g. manufacturer-specific) falls through the intersection with // `Record>` in the template. - stringBuilder.printLine("type ClustersByName = {"); + stringBuilder.printLine('type ClustersByName = {'); stringBuilder.increaseIndent(); for (const { name, className } of clusterClassByName) { stringBuilder.printLine(`${JSON.stringify(name)}?: ${className};`); } stringBuilder.decreaseIndent(); - stringBuilder.printLine("};"); + stringBuilder.printLine('};'); const template = await fs.readFile(templatePath, 'utf8'); const templateHeader = "declare module 'zigbee-clusters' {"; @@ -133,19 +132,22 @@ async function main(): Promise { // Write the result to index.d.ts await fs.writeFile(filePath, stringBuilder.toString()); - console.log("Done!"); + console.log('Done!'); } - -async function formatCluster(stringBuilder: StringBuilder, className: string, classDefinition: typeof Cluster): Promise { +async function formatCluster( + stringBuilder: StringBuilder, + className: string, + classDefinition: typeof Cluster, +): Promise { stringBuilder.startLine(); stringBuilder.print(`type ${className}Attributes = `); if (Object.keys(classDefinition.ATTRIBUTES).length === 0) { - stringBuilder.print("Record;"); + stringBuilder.print('Record;'); stringBuilder.endLine(); } else { // Open attributes - stringBuilder.print("{"); + stringBuilder.print('{'); stringBuilder.endLine(); stringBuilder.increaseIndent(); for (const attribute in classDefinition.ATTRIBUTES) { @@ -160,11 +162,11 @@ async function formatCluster(stringBuilder: StringBuilder, className: string, cl stringBuilder.startLine(); stringBuilder.print(`type ${className}Commands = `); if (Object.keys(classDefinition.COMMANDS).length === 0) { - stringBuilder.print("Record;"); + stringBuilder.print('Record;'); stringBuilder.endLine(); } else { // Open commands - stringBuilder.print("{"); + stringBuilder.print('{'); stringBuilder.endLine(); stringBuilder.increaseIndent(); for (const command in classDefinition.COMMANDS) { @@ -177,7 +179,9 @@ async function formatCluster(stringBuilder: StringBuilder, className: string, cl } // Open class - stringBuilder.printLine(`class ${className} extends Cluster {`); + stringBuilder.printLine( + `class ${className} extends Cluster {`, + ); stringBuilder.increaseIndent(); for (const command in classDefinition.COMMANDS) { @@ -190,46 +194,58 @@ async function formatCluster(stringBuilder: StringBuilder, className: string, cl stringBuilder.printLine('}'); } - -function formatAttribute(stringBuilder: StringBuilder, className: string, name: string, definition: types.AttributeDefinition): void { +function formatAttribute( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.AttributeDefinition, +): void { stringBuilder.startLine(); - stringBuilder.print(`${name}: { id: 0x${definition.id.toString(16).padStart(2, "0")}`); + stringBuilder.print(`${name}: { id: 0x${definition.id.toString(16).padStart(2, '0')}`); stringBuilder.print(', type: ZCLDataType<'); formatZCLDataTypeGeneric(stringBuilder, className, name, definition.type); - stringBuilder.print("> },"); + stringBuilder.print('> },'); stringBuilder.endLine(); } - -function formatCommand(stringBuilder: StringBuilder, className: string, name: string, definition: types.CommandDefinition): void { +function formatCommand( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.CommandDefinition, +): void { stringBuilder.startLine(); - stringBuilder.print(`${name}: { id: 0x${definition.id.toString(16).padStart(2, "0")}`); - stringBuilder.print(`, direction: ${JSON.stringify(definition.direction ?? "DIRECTION_SERVER_TO_CLIENT")}`); + stringBuilder.print(`${name}: { id: 0x${definition.id.toString(16).padStart(2, '0')}`); + stringBuilder.print( + `, direction: ${JSON.stringify(definition.direction ?? 'DIRECTION_SERVER_TO_CLIENT')}`, + ); if (definition.frameControl !== undefined) { stringBuilder.print(`, frameControl: ${JSON.stringify(definition.frameControl)}`); } if (definition.encodeMissingFieldsBehavior !== undefined) { - stringBuilder.print(`, encodeMissingFieldsBehavior: ${JSON.stringify(definition.encodeMissingFieldsBehavior)}`); + stringBuilder.print( + `, encodeMissingFieldsBehavior: ${JSON.stringify(definition.encodeMissingFieldsBehavior)}`, + ); } if (definition.global !== undefined) { stringBuilder.print(`, global: ${definition.global}`); } if (definition.args !== undefined) { - stringBuilder.print(", args: "); + stringBuilder.print(', args: '); if (Object.keys(definition.args).length === 0) { - throw new Error(`Empty args definition for ${className} command ${name}`) + throw new Error(`Empty args definition for ${className} command ${name}`); } else { // Open args - stringBuilder.print("{"); + stringBuilder.print('{'); stringBuilder.endLine(); stringBuilder.increaseIndent(2); for (const [key, arg] of Object.entries(definition.args)) { stringBuilder.startLine(); stringBuilder.print(`${key}: ZCLDataType<`); formatZCLDataTypeGeneric(stringBuilder, className, name, arg); - stringBuilder.print(">,"); + stringBuilder.print('>,'); stringBuilder.endLine(); } // Close args @@ -241,15 +257,19 @@ function formatCommand(stringBuilder: StringBuilder, className: string, name: st } } else { // Space before closing bracket of attribute - stringBuilder.print(" "); + stringBuilder.print(' '); } stringBuilder.print('},'); stringBuilder.endLine(); } - -function formatCommandMethod(stringBuilder: StringBuilder, className: string, name: string, definition: types.CommandDefinition): void { +function formatCommandMethod( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.CommandDefinition, +): void { // The runtime `Cluster.addCluster` installs a sendable method for every // command regardless of direction (see lib/Cluster.js around the // `Object.defineProperty(proto, cmdName, ...)` loop). Emit the sendable @@ -264,12 +284,17 @@ function formatCommandMethod(stringBuilder: StringBuilder, className: string, na } } -function emitSendableCommandMethod(stringBuilder: StringBuilder, className: string, name: string, definition: types.CommandDefinition): void { +function emitSendableCommandMethod( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.CommandDefinition, +): void { stringBuilder.startLine(); stringBuilder.print(name); // Open method - stringBuilder.print("("); + stringBuilder.print('('); stringBuilder.endLine(); stringBuilder.increaseIndent(); @@ -292,7 +317,6 @@ function emitSendableCommandMethod(stringBuilder: StringBuilder, className: stri stringBuilder.decreaseIndent(); stringBuilder.printLine('},'); - // Opts stringBuilder.printLine('opts?: {'); stringBuilder.increaseIndent(); @@ -305,17 +329,22 @@ function emitSendableCommandMethod(stringBuilder: StringBuilder, className: stri closeCommandMethod(stringBuilder, className, name, definition); } -function emitCommandHandlerMethod(stringBuilder: StringBuilder, className: string, name: string, definition: types.CommandDefinition): void { +function emitCommandHandlerMethod( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.CommandDefinition, +): void { stringBuilder.startLine(); - stringBuilder.print("on", name.charAt(0).toUpperCase(), name.slice(1)); + stringBuilder.print('on', name.charAt(0).toUpperCase(), name.slice(1)); // Open method - stringBuilder.print("("); + stringBuilder.print('('); stringBuilder.endLine(); stringBuilder.increaseIndent(); if (definition.args === undefined) { - stringBuilder.printLine('args: undefined,') + stringBuilder.printLine('args: undefined,'); } else { // Open args stringBuilder.printLine('args: {'); @@ -334,49 +363,58 @@ function emitCommandHandlerMethod(stringBuilder: StringBuilder, className: strin stringBuilder.printLine('},'); } - stringBuilder.printLine("meta: object,") - stringBuilder.printLine("frame: object,") - stringBuilder.printLine("rawFrame: Buffer,") + stringBuilder.printLine('meta: object,'); + stringBuilder.printLine('frame: object,'); + stringBuilder.printLine('rawFrame: Buffer,'); closeCommandMethod(stringBuilder, className, name, definition); } -function closeCommandMethod(stringBuilder: StringBuilder, className: string, name: string, definition: types.CommandDefinition): void { +function closeCommandMethod( + stringBuilder: StringBuilder, + className: string, + name: string, + definition: types.CommandDefinition, +): void { // Close method stringBuilder.decreaseIndent(); stringBuilder.startLine(); - stringBuilder.print("): Promise<"); + stringBuilder.print('): Promise<'); if (definition.response === undefined) { - stringBuilder.print("void"); + stringBuilder.print('void'); } else { // Open return object - stringBuilder.print("{") - stringBuilder.endLine() - stringBuilder.increaseIndent() + stringBuilder.print('{'); + stringBuilder.endLine(); + stringBuilder.increaseIndent(); for (let argName in definition.response.args) { const argType = definition.response.args[argName]; stringBuilder.startLine(); stringBuilder.print(argName); if (definition.response.encodeMissingFieldsBehavior === 'skip') { - stringBuilder.print("?") + stringBuilder.print('?'); } - stringBuilder.print(": ") + stringBuilder.print(': '); formatZCLDataTypeGeneric(stringBuilder, className, name, argType); - stringBuilder.print(",") + stringBuilder.print(','); stringBuilder.endLine(); } // Close return object stringBuilder.decreaseIndent(); stringBuilder.startLine(); - stringBuilder.print("}") + stringBuilder.print('}'); } - stringBuilder.print(">;") + stringBuilder.print('>;'); stringBuilder.endLine(); } - -function formatZCLDataTypeGeneric(stringBuilder: StringBuilder, className: string, command: string, definition: DataType): void { +function formatZCLDataTypeGeneric( + stringBuilder: StringBuilder, + className: string, + command: string, + definition: DataType, +): void { const typeName = definition.shortName as keyof typeof DataTypes; const typeArgs: Array = definition.args; @@ -385,21 +423,26 @@ function formatZCLDataTypeGeneric(stringBuilder: StringBuilder, className: strin stringBuilder.print('unknown'); } else if (typeName.startsWith('enum')) { if (definition === enum8Status) { - stringBuilder.print('types.ZCLEnum8Status') + stringBuilder.print('types.ZCLEnum8Status'); } else { formatEnumDataType(stringBuilder, className, command, typeName, typeArgs); } } else if (typeName.startsWith('map')) { formatMapDataType(stringBuilder, className, command, typeName, typeArgs); - } else if (typeName.startsWith("_Array")) { + } else if (typeName.startsWith('_Array')) { formatArrayDataType(stringBuilder, className, command, typeName, typeArgs); } else { stringBuilder.print(zclDataTypeToValueType(typeName)); } } - -function formatEnumDataType(stringBuilder: StringBuilder, className: string, field: string, typeName: string, typeArgs: Array): void { +function formatEnumDataType( + stringBuilder: StringBuilder, + className: string, + field: string, + typeName: string, + typeArgs: Array, +): void { const typeArg = typeArgs[0]; if (typeof typeArg !== 'object' || Array.isArray(typeArg) || typeArg === null) { throw new Error(`Invalid ${typeName} arg for ${className} ${field}`); @@ -409,12 +452,17 @@ function formatEnumDataType(stringBuilder: StringBuilder, className: string, fie if (entries.length === 0) { stringBuilder.print('unknown'); } else { - stringBuilder.print(entries.map(arg => JSON.stringify(arg)).join(" | ")); + stringBuilder.print(entries.map((arg) => JSON.stringify(arg)).join(' | ')); } } - -function formatMapDataType(stringBuilder: StringBuilder, className: string, field: string, typeName: string, typeArgs: Array): void { +function formatMapDataType( + stringBuilder: StringBuilder, + className: string, + field: string, + typeName: string, + typeArgs: Array, +): void { if (typeof typeArgs !== 'object' || !Array.isArray(typeArgs)) { throw new Error(`Invalid ${typeName} arg for ${className} ${field}`); } @@ -427,87 +475,92 @@ function formatMapDataType(stringBuilder: StringBuilder, className: string, fiel stringBuilder.print('Bitmap'); } else { stringBuilder.print('Bitmap<'); - stringBuilder.print(typeArgs.map(arg => JSON.stringify(arg)).join(" | ")); - stringBuilder.print(">"); + stringBuilder.print(typeArgs.map((arg) => JSON.stringify(arg)).join(' | ')); + stringBuilder.print('>'); } } -function formatArrayDataType(stringBuilder: StringBuilder, className: string, field: string, typeName: string, typeArgs: Array>): void { - stringBuilder.print('Array<') - formatZCLDataTypeGeneric(stringBuilder, className, field, typeArgs[0]) - stringBuilder.print('>') +function formatArrayDataType( + stringBuilder: StringBuilder, + className: string, + field: string, + typeName: string, + typeArgs: Array>, +): void { + stringBuilder.print('Array<'); + formatZCLDataTypeGeneric(stringBuilder, className, field, typeArgs[0]); + stringBuilder.print('>'); } - function zclDataTypeToValueType(type: keyof typeof DataTypes): string { switch (type) { - case "noData": - return "null"; - case "data8": - case "data16": - case "data24": - case "data32": - case "data40": - case "data48": - case "data56": - return "Buffer"; - case "bool": - return "boolean"; - case "map8": - case "map16": - case "map24": - case "map32": - case "map40": - case "map48": - case "map56": - case "map64": - throw new Error("Complex type"); - case "uint8": - case "uint16": - case "uint24": - case "uint32": - case "uint40": - case "uint48": - case "int8": - case "int16": - case "int24": - case "int32": - case "int40": - case "int48": - case "UTC": - return "number"; - case "enum8": - case "enum16": - case "enum32": - throw new Error("Complex type"); - case "single": - case "double": - return "number"; - case "octstr": - return "Buffer" - case "string": - case "EUI48": - case "EUI64": - case "key128": - return "string"; + case 'noData': + return 'null'; + case 'data8': + case 'data16': + case 'data24': + case 'data32': + case 'data40': + case 'data48': + case 'data56': + return 'Buffer'; + case 'bool': + return 'boolean'; + case 'map8': + case 'map16': + case 'map24': + case 'map32': + case 'map40': + case 'map48': + case 'map56': + case 'map64': + throw new Error('Complex type'); + case 'uint8': + case 'uint16': + case 'uint24': + case 'uint32': + case 'uint40': + case 'uint48': + case 'int8': + case 'int16': + case 'int24': + case 'int32': + case 'int40': + case 'int48': + case 'UTC': + return 'number'; + case 'enum8': + case 'enum16': + case 'enum32': + throw new Error('Complex type'); + case 'single': + case 'double': + return 'number'; + case 'octstr': + return 'Buffer'; + case 'string': + case 'EUI48': + case 'EUI64': + case 'key128': + return 'string'; } // Internal types switch (type as string) { - case "_map4": - throw new Error("Complex type"); - case "_uint4": - return "number"; - case "_enum4": - throw new Error("Complex type"); - case "_buffer": - case "_buffer8": - case "_buffer16": - return "Buffer"; - case "_Array0": - case "_Array8": - throw new Error("Complex type"); - case "_FixedString": - return "string"; + case '_map4': + throw new Error('Complex type'); + case '_uint4': + return 'number'; + case '_enum4': + throw new Error('Complex type'); + case '_buffer': + case '_buffer8': + case '_buffer16': + return 'Buffer'; + case '_Array0': + case '_Array8': + throw new Error('Complex type'); + case '_FixedString': + return 'string'; } throw new Error(`Unknown ZCL type ${type}`); } diff --git a/test/attributeReport.js b/test/attributeReport.js index 5a43456d..a595a89f 100644 --- a/test/attributeReport.js +++ b/test/attributeReport.js @@ -5,13 +5,15 @@ const OnOffCluster = require('../lib/clusters/onOff'); const { createMockNode } = require('./util'); const { ZCLStandardHeader } = require('../lib/zclFrames'); -describe('Attribute report', function() { - it('should emit the transaction sequence number with the attribute value', function(done) { +describe('Attribute report', function () { + it('should emit the transaction sequence number with the attribute value', function (done) { const node = createMockNode({ - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); node.endpoints[1].clusters.onOff.on('attr.onOff', (value, meta) => { @@ -22,7 +24,7 @@ describe('Attribute report', function() { }); const frame = new ZCLStandardHeader(); - frame.cmdId = 0x0A; // reportAttributes + frame.cmdId = 0x0a; // reportAttributes frame.trxSequenceNumber = 42; frame.frameControl.directionToClient = true; // attributeId 0x0000 (onOff), dataType 0x10 (bool), value true diff --git a/test/boundClusterAttributeStatus.js b/test/boundClusterAttributeStatus.js index b576c824..338f7ce2 100644 --- a/test/boundClusterAttributeStatus.js +++ b/test/boundClusterAttributeStatus.js @@ -21,10 +21,10 @@ function readStatuses(response) { return BasicCluster.attributeArrayStatusDataType.fromBuffer(response.attributes, 0); } -describe('BoundCluster attribute status codes', function() { - describe('readAttributes', function() { - it('reports UNSUPPORTED_ATTRIBUTE for an id the cluster does not define', async function() { - const bound = bind(new class extends BoundCluster {}()); +describe('BoundCluster attribute status codes', function () { + describe('readAttributes', function () { + it('reports UNSUPPORTED_ATTRIBUTE for an id the cluster does not define', async function () { + const bound = bind(new (class extends BoundCluster {})()); const records = readStatuses(await bound.readAttributes({ attributes: [UNREGISTERED_ID] })); assert.equal(records.length, 1); @@ -34,49 +34,49 @@ describe('BoundCluster attribute status codes', function() { assert.equal(records[0].status, 'UNSUPPORTED_ATTRIBUTE'); }); - it('reports UNSUPPORTED_ATTRIBUTE for a defined but unimplemented attribute', async function() { - const bound = bind(new class extends BoundCluster {}()); + it('reports UNSUPPORTED_ATTRIBUTE for a defined but unimplemented attribute', async function () { + const bound = bind(new (class extends BoundCluster {})()); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); assert.equal(records[0].status, 'UNSUPPORTED_ATTRIBUTE'); }); - it('still reports FAILURE when a getter throws', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - throw new Error('sensor offline'); - } - - }()); + it('still reports FAILURE when a getter throws', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + throw new Error('sensor offline'); + } + })(), + ); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); // The attribute exists, so this is a genuine failure and must not be masked as unsupported. assert.equal(records[0].status, 'FAILURE'); }); - it('reports FAILURE when the value cannot be serialized', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - // A string too long for the one-octet length prefix of a ZCL character string. - return 'x'.repeat(300); - } - - }()); + it('reports FAILURE when the value cannot be serialized', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + // A string too long for the one-octet length prefix of a ZCL character string. + return 'x'.repeat(300); + } + })(), + ); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); assert.equal(records[0].status, 'FAILURE'); }); - it('reports UNSUPPORTED_ATTRIBUTE for a write-only attribute, not 0x8f', async function() { - const bound = bind(new class extends BoundCluster { - - set modelId(value) { - this._modelId = value; - } - - }()); + it('reports UNSUPPORTED_ATTRIBUTE for a write-only attribute, not 0x8f', async function () { + const bound = bind( + new (class extends BoundCluster { + set modelId(value) { + this._modelId = value; + } + })(), + ); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); // ZCL R8 deprecates WRITE_ONLY and reassigns 0x8f to NOT_AUTHORIZED ("a request has been @@ -86,14 +86,14 @@ describe('BoundCluster attribute status codes', function() { assert.equal(records[0].status, 'UNSUPPORTED_ATTRIBUTE'); }); - it('honours a ZCLError thrown by a getter instead of flattening it to FAILURE', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - throw new ZCLError('NOT_AUTHORIZED'); - } - - }()); + it('honours a ZCLError thrown by a getter instead of flattening it to FAILURE', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + throw new ZCLError('NOT_AUTHORIZED'); + } + })(), + ); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); // The per-attribute status is data on the error, so an implementation can pick its own rather @@ -101,28 +101,28 @@ describe('BoundCluster attribute status codes', function() { assert.equal(records[0].status, 'NOT_AUTHORIZED'); }); - it('still reports SUCCESS with a value for an implemented attribute', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - return 'Homey'; - } - - }()); + it('still reports SUCCESS with a value for an implemented attribute', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + return 'Homey'; + } + })(), + ); const records = readStatuses(await bound.readAttributes({ attributes: [MODEL_ID] })); assert.equal(records[0].status, 'SUCCESS'); assert.equal(records[0].value, 'Homey'); }); - it('reports a per-attribute status, not one status for the whole request', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - return 'Homey'; - } - - }()); + it('reports a per-attribute status, not one status for the whole request', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + return 'Homey'; + } + })(), + ); const records = readStatuses( await bound.readAttributes({ attributes: [MODEL_ID, UNREGISTERED_ID] }), ); @@ -133,7 +133,7 @@ describe('BoundCluster attribute status codes', function() { }); }); - describe('writeAttributes', function() { + describe('writeAttributes', function () { /** Build the wire payload writeAttributes expects. */ function writeBuffer(records) { const buf = Buffer.alloc(255); @@ -141,8 +141,8 @@ describe('BoundCluster attribute status codes', function() { return buf.slice(0, len); } - it('reports UNSUPPORTED_ATTRIBUTE for an attribute this device does not implement', async function() { - const bound = bind(new class extends BoundCluster {}()); + it('reports UNSUPPORTED_ATTRIBUTE for an attribute this device does not implement', async function () { + const bound = bind(new (class extends BoundCluster {})()); const { attributes } = await bound.writeAttributes({ attributes: writeBuffer([{ id: MODEL_ID, value: 'Homey' }]), }); @@ -150,14 +150,14 @@ describe('BoundCluster attribute status codes', function() { assert.equal(attributes[0].status, 'UNSUPPORTED_ATTRIBUTE'); }); - it('reports READ_ONLY for an attribute exposed with a getter but no setter', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - return 'Homey'; - } - - }()); + it('reports READ_ONLY for an attribute exposed with a getter but no setter', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + return 'Homey'; + } + })(), + ); const { attributes } = await bound.writeAttributes({ attributes: writeBuffer([{ id: MODEL_ID, value: 'Other' }]), }); @@ -166,18 +166,18 @@ describe('BoundCluster attribute status codes', function() { assert.equal(attributes[0].status, 'READ_ONLY'); }); - it('honours a ZCLError thrown by a setter', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - return this._modelId; - } - - set modelId(value) { - throw new ZCLError('INVALID_VALUE'); - } - - }()); + it('honours a ZCLError thrown by a setter', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + return this._modelId; + } + + set modelId(value) { + throw new ZCLError('INVALID_VALUE'); + } + })(), + ); const { attributes } = await bound.writeAttributes({ attributes: writeBuffer([{ id: MODEL_ID, value: 'Other' }]), }); @@ -185,18 +185,18 @@ describe('BoundCluster attribute status codes', function() { assert.equal(attributes[0].status, 'INVALID_VALUE'); }); - it('still reports SUCCESS for a writable attribute', async function() { - const bound = bind(new class extends BoundCluster { - - get modelId() { - return this._modelId; - } - - set modelId(value) { - this._modelId = value; - } - - }()); + it('still reports SUCCESS for a writable attribute', async function () { + const bound = bind( + new (class extends BoundCluster { + get modelId() { + return this._modelId; + } + + set modelId(value) { + this._modelId = value; + } + })(), + ); const { attributes } = await bound.writeAttributes({ attributes: writeBuffer([{ id: MODEL_ID, value: 'Other' }]), }); diff --git a/test/colorControl.js b/test/colorControl.js index 5aef9d33..967b46d1 100644 --- a/test/colorControl.js +++ b/test/colorControl.js @@ -15,19 +15,22 @@ const sandbox = sinon.createSandbox(); async function sendColorLoopSet(args) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [ColorControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }, + ], }); - node.endpoints[1].bind('colorControl', new (class extends BoundCluster { - - async colorLoopSet() { - // Accept the command so no error response is generated - } - - })()); + node.endpoints[1].bind( + 'colorControl', + new (class extends BoundCluster { + async colorLoopSet() { + // Accept the command so no error response is generated + } + })(), + ); const sendFrameSpy = sandbox.spy(node, 'sendFrame'); await node.endpoints[1].clusters.colorControl.colorLoopSet(args); @@ -35,28 +38,31 @@ async function sendColorLoopSet(args) { return Endpoint.parseFrame(sendFrameSpy.getCall(0).args[2]); } -describe('Color Control', function() { - afterEach(function() { +describe('Color Control', function () { + afterEach(function () { sandbox.restore(); }); - it('should receive moveToColor', async function() { + it('should receive moveToColor', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [ColorControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('colorControl', new (class extends BoundCluster { - - async moveToColor(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'colorControl', + new (class extends BoundCluster { + async moveToColor(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.colorControl.moveToColor({ colorX: 0x5000, @@ -69,23 +75,26 @@ describe('Color Control', function() { assert.strictEqual(receivedData.transitionTime, 20); }); - it('should receive moveToColorTemperature', async function() { + it('should receive moveToColorTemperature', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [ColorControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('colorControl', new (class extends BoundCluster { - - async moveToColorTemperature(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'colorControl', + new (class extends BoundCluster { + async moveToColorTemperature(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.colorControl.moveToColorTemperature({ colorTemperature: 370, @@ -96,23 +105,26 @@ describe('Color Control', function() { assert.strictEqual(receivedData.transitionTime, 15); }); - it('should receive moveToHueAndSaturation', async function() { + it('should receive moveToHueAndSaturation', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [ColorControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('colorControl', new (class extends BoundCluster { - - async moveToHueAndSaturation(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'colorControl', + new (class extends BoundCluster { + async moveToHueAndSaturation(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.colorControl.moveToHueAndSaturation({ hue: 180, @@ -125,23 +137,26 @@ describe('Color Control', function() { assert.strictEqual(receivedData.transitionTime, 10); }); - it('should receive colorLoopSet', async function() { + it('should receive colorLoopSet', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [ColorControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [ColorControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('colorControl', new (class extends BoundCluster { - - async colorLoopSet(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'colorControl', + new (class extends BoundCluster { + async colorLoopSet(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.colorControl.colorLoopSet({ updateFlags: ['updateAction', 'updateDirection', 'updateTime', 'updateStartHue'], @@ -152,7 +167,10 @@ describe('Color Control', function() { }); assert.deepStrictEqual(receivedData.updateFlags.getBits(), [ - 'updateAction', 'updateDirection', 'updateTime', 'updateStartHue', + 'updateAction', + 'updateDirection', + 'updateTime', + 'updateStartHue', ]); assert.strictEqual(receivedData.action, 'activateFromColorLoopStartEnhancedHue'); assert.strictEqual(receivedData.direction, 'incrementHue'); @@ -164,7 +182,7 @@ describe('Color Control', function() { // that definition is wrong on the wire. These bytes come from the spec, not from our types: // ZCL r8 Figure 5-17 (payload layout), Figure 5-18 (updateFlags bits), Table 5.25 (action // values), Table 5.26 (direction values), Table 5.13 (command id). - it('should encode colorLoopSet as specified by the ZCL', async function() { + it('should encode colorLoopSet as specified by the ZCL', async function () { const frame = await sendColorLoopSet({ updateFlags: ['updateAction', 'updateStartHue'], // Bits 0 and 3, so 0x09 action: 'activateFromEnhancedCurrentHue', // 0x02 @@ -178,7 +196,7 @@ describe('Color Control', function() { }); // Setting every flag at once cannot tell the bit positions apart, so pin each one separately - it('should encode each colorLoopSet update flag on its own bit', async function() { + it('should encode each colorLoopSet update flag on its own bit', async function () { const flagBits = [ ['updateAction', 0x01], ['updateDirection', 0x02], diff --git a/test/defaultResponse.js b/test/defaultResponse.js index 43282a05..91cf82da 100644 --- a/test/defaultResponse.js +++ b/test/defaultResponse.js @@ -7,7 +7,7 @@ const DoorLockCluster = require('../lib/clusters/doorLock'); // Door Lock setWeekDaySchedule is cluster-specific command 0x0B, same id as global Default Response const SET_WEEK_DAY_SCHEDULE_REQUEST = Buffer.from([ - 0x01, 0x05, 0x0B, 0x01, 0x02, 0x00, 0x02, 0x08, 0x00, 0x17, 0x00, + 0x01, 0x05, 0x0b, 0x01, 0x02, 0x00, 0x02, 0x08, 0x00, 0x17, 0x00, ]); function createEndpoint() { @@ -23,23 +23,24 @@ function createEndpoint() { return { endpoint, sentFrames }; } -describe('Default Response', function() { - it('sends the cluster-specific response to a cluster-specific command 0x0B', async function() { +describe('Default Response', function () { + it('sends the cluster-specific response to a cluster-specific command 0x0B', async function () { const { endpoint, sentFrames } = createEndpoint(); - endpoint.bind('doorLock', new (class extends BoundCluster { - - async setWeekDaySchedule() { - return { status: 0 }; - } - - })()); + endpoint.bind( + 'doorLock', + new (class extends BoundCluster { + async setWeekDaySchedule() { + return { status: 0 }; + } + })(), + ); await endpoint.handleFrame(DoorLockCluster.ID, SET_WEEK_DAY_SCHEDULE_REQUEST); assert.deepEqual(sentFrames, ['19050b00']); }); - it('sends an error Default Response to an unhandled cluster-specific command 0x0B', async function() { + it('sends an error Default Response to an unhandled cluster-specific command 0x0B', async function () { const { endpoint, sentFrames } = createEndpoint(); endpoint.bind('doorLock', new BoundCluster()); @@ -53,10 +54,10 @@ describe('Default Response', function() { assert.match(sentFrames[0], /^18050b0b(?!00)[0-9a-f]{2}$/); }); - it('does not respond to a global Default Response', async function() { + it('does not respond to a global Default Response', async function () { const { endpoint, sentFrames } = createEndpoint(); - await endpoint.handleFrame(DoorLockCluster.ID, Buffer.from([0x08, 0x05, 0x0B, 0x00, 0x00])); + await endpoint.handleFrame(DoorLockCluster.ID, Buffer.from([0x08, 0x05, 0x0b, 0x00, 0x00])); await new Promise(setImmediate); assert.deepEqual(sentFrames, []); diff --git a/test/doorLock.js b/test/doorLock.js index 4c922c08..a3c5b65b 100644 --- a/test/doorLock.js +++ b/test/doorLock.js @@ -6,24 +6,27 @@ const DoorLockCluster = require('../lib/clusters/doorLock'); const { createMockNode } = require('./util'); const { ZCLStandardHeader } = require('../lib/zclFrames'); -describe('Door Lock', function() { - it('should receive lockDoor', async function() { +describe('Door Lock', function () { + it('should receive lockDoor', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('doorLock', new (class extends BoundCluster { - - async lockDoor(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'doorLock', + new (class extends BoundCluster { + async lockDoor(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.doorLock.lockDoor({ pinCode: Buffer.from([0x31, 0x32, 0x33, 0x34]), @@ -32,48 +35,54 @@ describe('Door Lock', function() { assert.deepStrictEqual(receivedData.pinCode, Buffer.from([0x31, 0x32, 0x33, 0x34])); }); - it('should receive lockDoor without arguments', async function() { + it('should receive lockDoor without arguments', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); let called = false; - node.endpoints[1].bind('doorLock', new (class extends BoundCluster { - - async lockDoor(data) { - called = true; - // pinCode should be empty buffer when not provided - assert.deepStrictEqual(data.pinCode, Buffer.from([])); - } - - })()); + node.endpoints[1].bind( + 'doorLock', + new (class extends BoundCluster { + async lockDoor(data) { + called = true; + // pinCode should be empty buffer when not provided + assert.deepStrictEqual(data.pinCode, Buffer.from([])); + } + })(), + ); // Call without any arguments - should not throw await node.endpoints[1].clusters.doorLock.lockDoor(); assert.strictEqual(called, true); }); - it('should receive unlockDoor', async function() { + it('should receive unlockDoor', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('doorLock', new (class extends BoundCluster { - - async unlockDoor(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'doorLock', + new (class extends BoundCluster { + async unlockDoor(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.doorLock.unlockDoor({ pinCode: Buffer.from([0x30, 0x30, 0x30, 0x30]), @@ -82,23 +91,26 @@ describe('Door Lock', function() { assert.deepStrictEqual(receivedData.pinCode, Buffer.from([0x30, 0x30, 0x30, 0x30])); }); - it('should receive setPINCode', async function() { + it('should receive setPINCode', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('doorLock', new (class extends BoundCluster { - - async setPINCode(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'doorLock', + new (class extends BoundCluster { + async setPINCode(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.doorLock.setPINCode({ userId: 1, @@ -115,16 +127,18 @@ describe('Door Lock', function() { // Server-to-client notifications require manual frame construction // since they are sent BY the device TO the controller - it('should receive operationEventNotification', function(done) { + it('should receive operationEventNotification', function (done) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); - node.endpoints[1].clusters.doorLock.onOperationEventNotification = data => { + node.endpoints[1].clusters.doorLock.onOperationEventNotification = (data) => { assert.strictEqual(data.operationEventSource, 1); // Keypad assert.strictEqual(data.operationEventCode, 2); // Unlock assert.strictEqual(data.userId, 3); @@ -139,32 +153,31 @@ describe('Door Lock', function() { frame.frameControl.directionToClient = true; frame.frameControl.clusterSpecific = true; frame.data = Buffer.from([ - 0x01, 0x02, 0x03, 0x00, - 0x04, 0x31, 0x32, 0x33, 0x34, - 0x78, 0x56, 0x34, 0x12, - 0x00, + 0x01, 0x02, 0x03, 0x00, 0x04, 0x31, 0x32, 0x33, 0x34, 0x78, 0x56, 0x34, 0x12, 0x00, ]); node.handleFrame(1, DoorLockCluster.ID, frame.toBuffer(), {}); }); - it('should receive programmingEventNotification', function(done) { + it('should receive programmingEventNotification', function (done) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [DoorLockCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [DoorLockCluster.ID], + }, + ], }); - node.endpoints[1].clusters.doorLock.onProgrammingEventNotification = data => { + node.endpoints[1].clusters.doorLock.onProgrammingEventNotification = (data) => { assert.strictEqual(data.programEventSource, 2); // RF assert.strictEqual(data.programEventCode, 3); // PIN added assert.strictEqual(data.userId, 7); assert.deepStrictEqual(data.pin, Buffer.from([0x35, 0x36, 0x37, 0x38])); assert.strictEqual(data.userType, 'unrestricted'); assert.strictEqual(data.userStatus, 'occupiedEnabled'); - assert.strictEqual(data.zigBeeLocalTime, 0xAABBCCDD); + assert.strictEqual(data.zigBeeLocalTime, 0xaabbccdd); assert.deepStrictEqual(data.data, Buffer.from([])); done(); }; @@ -174,10 +187,7 @@ describe('Door Lock', function() { frame.frameControl.directionToClient = true; frame.frameControl.clusterSpecific = true; frame.data = Buffer.from([ - 0x02, 0x03, 0x07, 0x00, - 0x04, 0x35, 0x36, 0x37, 0x38, - 0x00, 0x01, - 0xDD, 0xCC, 0xBB, 0xAA, + 0x02, 0x03, 0x07, 0x00, 0x04, 0x35, 0x36, 0x37, 0x38, 0x00, 0x01, 0xdd, 0xcc, 0xbb, 0xaa, 0x00, ]); diff --git a/test/endpointStandalone.js b/test/endpointStandalone.js index 51bb88f7..0593f3f6 100644 --- a/test/endpointStandalone.js +++ b/test/endpointStandalone.js @@ -8,15 +8,13 @@ const BASIC_CLUSTER_ID = 0x0000; const MODEL_ID = 0x0005; class Basic extends BoundCluster { - get modelId() { return 'Homey Bridge'; } - } -describe('Endpoint standalone', function() { - it('serves bound clusters with nothing but a sendFrame owner', async function() { +describe('Endpoint standalone', function () { + it('serves bound clusters with nothing but a sendFrame owner', async function () { // The whole point of the export: a device answering incoming commands is not a peer, so it // should not have to construct a ZCLNode - which models one remote node and bakes that node's // address into its own sendFrame. Endpoint only needs sendFrame from its owner (nextSeqNr is @@ -38,7 +36,7 @@ describe('Endpoint standalone', function() { assert.equal(sent.toString('hex'), '180701050000420c486f6d657920427269646765'); }); - it('allocates transaction sequence numbers without a node counter', function() { + it('allocates transaction sequence numbers without a node counter', function () { const endpoint = new Endpoint( { sendFrame: async () => {} }, { endpointId: 1, inputClusters: [BASIC_CLUSTER_ID], outputClusters: [] }, diff --git a/test/iasZone.js b/test/iasZone.js index a551210a..514e5d95 100644 --- a/test/iasZone.js +++ b/test/iasZone.js @@ -6,18 +6,20 @@ const IASZoneCluster = require('../lib/clusters/iasZone'); const { createMockNode, MOCK_DEVICES } = require('./util'); const { ZCLStandardHeader } = require('../lib/zclFrames'); -describe('IAS Zone', function() { +describe('IAS Zone', function () { // Server-to-client notification - requires manual frame - it('should receive onZoneEnrollRequest', function(done) { + it('should receive onZoneEnrollRequest', function (done) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [IASZoneCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [IASZoneCluster.ID], + }, + ], }); - node.endpoints[1].clusters.iasZone.onZoneEnrollRequest = data => { + node.endpoints[1].clusters.iasZone.onZoneEnrollRequest = (data) => { assert.strictEqual(data.zoneType, 'keypad'); assert.strictEqual(data.manufacturerCode, 4117); done(); @@ -33,23 +35,26 @@ describe('IAS Zone', function() { }); // Client-to-server command - can use loopback - it('should send zoneEnrollResponse', async function() { + it('should send zoneEnrollResponse', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [IASZoneCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [IASZoneCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('iasZone', new (class extends BoundCluster { - - async zoneEnrollResponse(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'iasZone', + new (class extends BoundCluster { + async zoneEnrollResponse(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.iasZone.zoneEnrollResponse({ enrollResponseCode: 'success', @@ -61,16 +66,18 @@ describe('IAS Zone', function() { }); // Server-to-client notification - requires manual frame - it('should receive zoneStatusChangeNotification', function(done) { + it('should receive zoneStatusChangeNotification', function (done) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [IASZoneCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [IASZoneCluster.ID], + }, + ], }); - node.endpoints[1].clusters.iasZone.onZoneStatusChangeNotification = data => { + node.endpoints[1].clusters.iasZone.onZoneStatusChangeNotification = (data) => { assert.strictEqual(data.zoneStatus.supervisionReports, true); assert.strictEqual(data.extendedStatus, 0); assert.strictEqual(data.zoneId, 10); @@ -82,27 +89,27 @@ describe('IAS Zone', function() { frame.cmdId = IASZoneCluster.COMMANDS.zoneStatusChangeNotification.id; frame.frameControl.directionToClient = true; frame.frameControl.clusterSpecific = true; - frame.data = Buffer.from([0x10, 0x00, 0x00, 0x0A, 0x6C, 0x00]); + frame.data = Buffer.from([0x10, 0x00, 0x00, 0x0a, 0x6c, 0x00]); node.handleFrame(1, IASZoneCluster.ID, frame.toBuffer(), {}); }); - describe('Mock Device Factory', function() { - it('should create a motion sensor with correct zone type', function() { + describe('Mock Device Factory', function () { + it('should create a motion sensor with correct zone type', function () { const sensor = MOCK_DEVICES.motionSensor(); const boundCluster = sensor.endpoints[1].bindings.iasZone; - assert.strictEqual(boundCluster.zoneType, 0x000D, 'Should be motion sensor type'); + assert.strictEqual(boundCluster.zoneType, 0x000d, 'Should be motion sensor type'); assert.strictEqual(boundCluster.zoneState, 1, 'Should be enrolled'); }); - it('should create a contact sensor with correct zone type', function() { + it('should create a contact sensor with correct zone type', function () { const sensor = MOCK_DEVICES.contactSensor(); const boundCluster = sensor.endpoints[1].bindings.iasZone; assert.strictEqual(boundCluster.zoneType, 0x0015, 'Should be contact switch type'); assert.strictEqual(boundCluster.zoneState, 1, 'Should be enrolled'); }); - it('should allow attribute overrides', function() { + it('should allow attribute overrides', function () { const sensor = MOCK_DEVICES.motionSensor({ iasZone: { zoneStatus: 0x0001 }, // Alarm1 active }); @@ -110,7 +117,7 @@ describe('IAS Zone', function() { assert.strictEqual(boundCluster.zoneStatus, 0x0001, 'Should have alarm1 bit set'); }); - it('should create temp/humidity sensor with measurement values', function() { + it('should create temp/humidity sensor with measurement values', function () { const sensor = MOCK_DEVICES.tempHumiditySensor(); const tempCluster = sensor.endpoints[1].bindings.temperatureMeasurement; const humCluster = sensor.endpoints[1].bindings.relativeHumidity; diff --git a/test/levelControl.js b/test/levelControl.js index 98daf598..cc557629 100644 --- a/test/levelControl.js +++ b/test/levelControl.js @@ -5,24 +5,27 @@ const BoundCluster = require('../lib/BoundCluster'); const LevelControlCluster = require('../lib/clusters/levelControl'); const { createMockNode } = require('./util'); -describe('Level Control', function() { - it('should receive moveToLevel', async function() { +describe('Level Control', function () { + it('should receive moveToLevel', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [LevelControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('levelControl', new (class extends BoundCluster { - - async moveToLevel(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'levelControl', + new (class extends BoundCluster { + async moveToLevel(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.levelControl.moveToLevel({ level: 128, @@ -33,23 +36,26 @@ describe('Level Control', function() { assert.strictEqual(receivedData.transitionTime, 10); }); - it('should receive step', async function() { + it('should receive step', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [LevelControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('levelControl', new (class extends BoundCluster { - - async step(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'levelControl', + new (class extends BoundCluster { + async step(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.levelControl.step({ mode: 'up', @@ -62,23 +68,26 @@ describe('Level Control', function() { assert.strictEqual(receivedData.transitionTime, 5); }); - it('should receive stop', async function() { + it('should receive stop', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [LevelControlCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [LevelControlCluster.ID], + }, + ], }); let called = false; - node.endpoints[1].bind('levelControl', new (class extends BoundCluster { - - async stop() { - called = true; - } - - })()); + node.endpoints[1].bind( + 'levelControl', + new (class extends BoundCluster { + async stop() { + called = true; + } + })(), + ); await node.endpoints[1].clusters.levelControl.stop(); assert.strictEqual(called, true); diff --git a/test/manual/xiaomiHacks.js b/test/manual/xiaomiHacks.js index d08177c5..90ef1a82 100644 --- a/test/manual/xiaomiHacks.js +++ b/test/manual/xiaomiHacks.js @@ -24,59 +24,70 @@ const attributes = { }; // custom data type parser with dynamic length with minimal 3 bytes, and no declared ZCL id. -const XiaomiLifelineDataRecord = new ZCLDataType(NaN, 'XiaomiLifelineDataRecord', -3, function(_buf, _v, _i) { - throw new Error('generating xiaomi structs is not implemented'); -}, function(buf, i, returnLength) { - i = i || 0; - const startByte = i; - const res = {}; - res.id = ZCLDataTypes.uint8.fromBuffer(buf, i); - i += ZCLDataTypes.uint8.length; - - const dataTypeId = ZCLDataTypes.uint8.fromBuffer(buf, i); - i += ZCLDataTypes.uint8.length; - - const DataType = attributes[res.id] && attributes[res.id].type - || Object.values(ZCLDataTypes).find(type => type && type.id === dataTypeId); - if (!DataType) throw new TypeError(`Invalid Type for Attribute: ${res.id}`); - - res.name = attributes[res.id] && attributes[res.id].name || `unknown_attr_${res.id}`; - - const entry = DataType.fromBuffer(buf, i, true); - if (DataType.length > 0) { - i += DataType.length; - res.value = entry; - } else { - res.value = entry.result; - i += entry.length; - } - if (returnLength) { - return { result: res, length: i - startByte }; - } - return res; -}); - -const XiaomiLifelineDataRecordArray = new ZCLDataType(NaN, 'XiaomiLifelineDataRecordArray', -1, function(_buf, _v, _i) { - throw new Error('not_supported'); -}, function(buf, i, returnLength) { - let { result, length } = ZCLDataTypes.buffer8.fromBuffer(buf, i, true); - result = ZCLDataTypes.Array0(XiaomiLifelineDataRecord).fromBuffer(result, 0); - result = result.reduce((r, { name, value }) => Object.assign(r, { [name]: value }), {}); - if (returnLength) { - return { result, length }; - } - return result; -}); +const XiaomiLifelineDataRecord = new ZCLDataType( + NaN, + 'XiaomiLifelineDataRecord', + -3, + function (_buf, _v, _i) { + throw new Error('generating xiaomi structs is not implemented'); + }, + function (buf, i, returnLength) { + i = i || 0; + const startByte = i; + const res = {}; + res.id = ZCLDataTypes.uint8.fromBuffer(buf, i); + i += ZCLDataTypes.uint8.length; + + const dataTypeId = ZCLDataTypes.uint8.fromBuffer(buf, i); + i += ZCLDataTypes.uint8.length; + + const DataType = + (attributes[res.id] && attributes[res.id].type) || + Object.values(ZCLDataTypes).find((type) => type && type.id === dataTypeId); + if (!DataType) throw new TypeError(`Invalid Type for Attribute: ${res.id}`); + + res.name = (attributes[res.id] && attributes[res.id].name) || `unknown_attr_${res.id}`; + + const entry = DataType.fromBuffer(buf, i, true); + if (DataType.length > 0) { + i += DataType.length; + res.value = entry; + } else { + res.value = entry.result; + i += entry.length; + } + if (returnLength) { + return { result: res, length: i - startByte }; + } + return res; + }, +); + +const XiaomiLifelineDataRecordArray = new ZCLDataType( + NaN, + 'XiaomiLifelineDataRecordArray', + -1, + function (_buf, _v, _i) { + throw new Error('not_supported'); + }, + function (buf, i, returnLength) { + let { result, length } = ZCLDataTypes.buffer8.fromBuffer(buf, i, true); + result = ZCLDataTypes.Array0(XiaomiLifelineDataRecord).fromBuffer(result, 0); + result = result.reduce((r, { name, value }) => Object.assign(r, { [name]: value }), {}); + if (returnLength) { + return { result, length }; + } + return result; + }, +); class XiaomiHackedBasicCluster extends BasicCluster { - static get ATTRIBUTES() { return { ...super.ATTRIBUTES, - xiaomiLifeline: { id: 0xFF01, type: XiaomiLifelineDataRecordArray }, + xiaomiLifeline: { id: 0xff01, type: XiaomiLifelineDataRecordArray }, }; } - } // Replace basic cluster with our patched version diff --git a/test/onOff.js b/test/onOff.js index 5306f556..803d15e1 100644 --- a/test/onOff.js +++ b/test/onOff.js @@ -5,90 +5,102 @@ const BoundCluster = require('../lib/BoundCluster'); const OnOffCluster = require('../lib/clusters/onOff'); const { createMockNode } = require('./util'); -describe('On/Off', function() { - it('should receive setOn', async function() { +describe('On/Off', function () { + it('should receive setOn', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); let called = false; - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - called = true; - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + called = true; + } + })(), + ); await node.endpoints[1].clusters.onOff.setOn(); assert.strictEqual(called, true); }); - it('should receive setOff', async function() { + it('should receive setOff', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); let called = false; - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOff() { - called = true; - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOff() { + called = true; + } + })(), + ); await node.endpoints[1].clusters.onOff.setOff(); assert.strictEqual(called, true); }); - it('should receive toggle', async function() { + it('should receive toggle', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); let called = false; - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async toggle() { - called = true; - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async toggle() { + called = true; + } + })(), + ); await node.endpoints[1].clusters.onOff.toggle(); assert.strictEqual(called, true); }); - it('should receive onWithTimedOff', async function() { + it('should receive onWithTimedOff', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); let receivedData = null; - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async onWithTimedOff(data) { - receivedData = data; - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async onWithTimedOff(data) { + receivedData = data; + } + })(), + ); await node.endpoints[1].clusters.onOff.onWithTimedOff({ onOffControl: 0x01, diff --git a/test/ota.js b/test/ota.js index fd1e697b..74eafa38 100644 --- a/test/ota.js +++ b/test/ota.js @@ -25,32 +25,35 @@ function createCapturingNode() { capturedFrames.push(buffer); return node.handleFrame(endpointId, clusterId, buffer, {}); }, - endpointDescriptors: [{ - endpointId: 1, - inputClusters: [OTACluster.ID], - outputClusters: [], - }], + endpointDescriptors: [ + { + endpointId: 1, + inputClusters: [OTACluster.ID], + outputClusters: [], + }, + ], }; node = new Node(mockNode); return { node, capturedFrames }; } -describe('OTA', function() { +describe('OTA', function () { // --- queryNextImageRequest: encodeMissingFieldsBehavior on the request --- - it('should NOT encode hardwareVersion when omitted from queryNextImageRequest', async function() { + it('should NOT encode hardwareVersion when omitted from queryNextImageRequest', async function () { const { node, capturedFrames } = createCapturingNode(); let receivedData = null; - node.endpoints[1].bind('ota', new (class extends BoundCluster { - - async queryNextImageRequest(data) { - receivedData = data; - return { status: 'NO_IMAGE_AVAILABLE' }; - } - - })()); + node.endpoints[1].bind( + 'ota', + new (class extends BoundCluster { + async queryNextImageRequest(data) { + receivedData = data; + return { status: 'NO_IMAGE_AVAILABLE' }; + } + })(), + ); await node.endpoints[1].clusters.ota.queryNextImageRequest({ fieldControl: 0, @@ -60,7 +63,7 @@ describe('OTA', function() { // hardwareVersion intentionally omitted }); - const requestFrame = capturedFrames.find(buf => buf[2] === CMD_QUERY_NEXT_IMAGE_REQUEST); + const requestFrame = capturedFrames.find((buf) => buf[2] === CMD_QUERY_NEXT_IMAGE_REQUEST); assert.ok(requestFrame, 'request frame not captured'); // fieldControl(1) + manufacturerCode(2) + imageType(2) + fileVersion(4) = 9 bytes @@ -79,16 +82,17 @@ describe('OTA', function() { assert.strictEqual(receivedData.hardwareVersion, 0); }); - it('should encode hardwareVersion when provided in queryNextImageRequest', async function() { + it('should encode hardwareVersion when provided in queryNextImageRequest', async function () { const { node, capturedFrames } = createCapturingNode(); - node.endpoints[1].bind('ota', new (class extends BoundCluster { - - async queryNextImageRequest() { - return { status: 'NO_IMAGE_AVAILABLE' }; - } - - })()); + node.endpoints[1].bind( + 'ota', + new (class extends BoundCluster { + async queryNextImageRequest() { + return { status: 'NO_IMAGE_AVAILABLE' }; + } + })(), + ); await node.endpoints[1].clusters.ota.queryNextImageRequest({ fieldControl: ['hardwareVersionPresent'], @@ -98,7 +102,7 @@ describe('OTA', function() { hardwareVersion: 0x0002, }); - const requestFrame = capturedFrames.find(buf => buf[2] === CMD_QUERY_NEXT_IMAGE_REQUEST); + const requestFrame = capturedFrames.find((buf) => buf[2] === CMD_QUERY_NEXT_IMAGE_REQUEST); assert.ok(requestFrame, 'request frame not captured'); // fieldControl(1) + manufacturerCode(2) + imageType(2) + fileVersion(4) @@ -116,22 +120,23 @@ describe('OTA', function() { // --- queryNextImageResponse: encodeMissingFieldsBehavior on the response --- - it('should encode all fields in queryNextImageResponse when status is SUCCESS', async function() { + it('should encode all fields in queryNextImageResponse when status is SUCCESS', async function () { const { node, capturedFrames } = createCapturingNode(); - node.endpoints[1].bind('ota', new (class extends BoundCluster { - - async queryNextImageRequest() { - return { - status: 'SUCCESS', - manufacturerCode: 0x1234, - imageType: 0x0001, - fileVersion: 0x00000020, - imageSize: 0x00010000, - }; - } - - })()); + node.endpoints[1].bind( + 'ota', + new (class extends BoundCluster { + async queryNextImageRequest() { + return { + status: 'SUCCESS', + manufacturerCode: 0x1234, + imageType: 0x0001, + fileVersion: 0x00000020, + imageSize: 0x00010000, + }; + } + })(), + ); const response = await node.endpoints[1].clusters.ota.queryNextImageRequest({ fieldControl: 0, @@ -140,7 +145,7 @@ describe('OTA', function() { fileVersion: 0x00000010, }); - const responseFrame = capturedFrames.find(buf => buf[2] === CMD_QUERY_NEXT_IMAGE_RESPONSE); + const responseFrame = capturedFrames.find((buf) => buf[2] === CMD_QUERY_NEXT_IMAGE_RESPONSE); assert.ok(responseFrame, 'response frame not captured'); // status(1) + manufacturerCode(2) + imageType(2) + fileVersion(4) + imageSize(4) = 13 bytes @@ -158,42 +163,42 @@ describe('OTA', function() { assert.strictEqual(response.imageSize, 0x00010000); }); - it('should encode only the status byte in queryNextImageResponse when status is NO_IMAGE_AVAILABLE', - async function() { - const { node, capturedFrames } = createCapturingNode(); - - node.endpoints[1].bind('ota', new (class extends BoundCluster { + it('should encode only the status byte in queryNextImageResponse when status is NO_IMAGE_AVAILABLE', async function () { + const { node, capturedFrames } = createCapturingNode(); + node.endpoints[1].bind( + 'ota', + new (class extends BoundCluster { async queryNextImageRequest() { return { status: 'NO_IMAGE_AVAILABLE' }; } + })(), + ); - })()); - - const response = await node.endpoints[1].clusters.ota.queryNextImageRequest({ - fieldControl: 0, - manufacturerCode: 0x1234, - imageType: 0x0001, - fileVersion: 0x00000010, - }); - - const responseFrame = capturedFrames.find(buf => buf[2] === CMD_QUERY_NEXT_IMAGE_RESPONSE); - assert.ok(responseFrame, 'response frame not captured'); - - // Only status(1) — all other fields must be absent - const payload = responseFrame.slice(ZCL_HEADER_SIZE); - assert.strictEqual( - payload.length, - 1, - `payload must be 1 byte (status only), got ${payload.length}`, - ); - assert.strictEqual(payload[0], 0x98, 'status byte must be NO_IMAGE_AVAILABLE (0x98)'); - - assert.strictEqual(response.status, 'NO_IMAGE_AVAILABLE'); - // Absent fields decode as 0 (decoder reads from exhausted buffer) - assert.strictEqual(response.manufacturerCode, 0); - assert.strictEqual(response.imageType, 0); - assert.strictEqual(response.fileVersion, 0); - assert.strictEqual(response.imageSize, 0); + const response = await node.endpoints[1].clusters.ota.queryNextImageRequest({ + fieldControl: 0, + manufacturerCode: 0x1234, + imageType: 0x0001, + fileVersion: 0x00000010, }); + + const responseFrame = capturedFrames.find((buf) => buf[2] === CMD_QUERY_NEXT_IMAGE_RESPONSE); + assert.ok(responseFrame, 'response frame not captured'); + + // Only status(1) — all other fields must be absent + const payload = responseFrame.slice(ZCL_HEADER_SIZE); + assert.strictEqual( + payload.length, + 1, + `payload must be 1 byte (status only), got ${payload.length}`, + ); + assert.strictEqual(payload[0], 0x98, 'status byte must be NO_IMAGE_AVAILABLE (0x98)'); + + assert.strictEqual(response.status, 'NO_IMAGE_AVAILABLE'); + // Absent fields decode as 0 (decoder reads from exhausted buffer) + assert.strictEqual(response.manufacturerCode, 0); + assert.strictEqual(response.imageType, 0); + assert.strictEqual(response.fileVersion, 0); + assert.strictEqual(response.imageSize, 0); + }); }); diff --git a/test/testClusterResponseTimeout.js b/test/testClusterResponseTimeout.js index 4ad7d97a..2e20ac70 100644 --- a/test/testClusterResponseTimeout.js +++ b/test/testClusterResponseTimeout.js @@ -13,11 +13,11 @@ const IasZoneCluster = require('../lib/clusters/iasZone'); const sandbox = sinon.createSandbox(); -describe('Cluster Response Timeout', function() { +describe('Cluster Response Timeout', function () { let receivingNode; let sendingNode; - beforeEach(function() { + beforeEach(function () { sendingNode = new Node({ // Forward frames to receiving node sendFrame: (...args) => { @@ -45,42 +45,44 @@ describe('Cluster Response Timeout', function() { }); }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); - it('should only start timeout after frame is sent', async function() { + it('should only start timeout after frame is sent', async function () { const originalSendFrame = sendingNode.endpoints[1].sendFrame.bind(sendingNode.endpoints[1]); sandbox.stub(sendingNode.endpoints[1], 'sendFrame').callsFake(async (...args) => { // Delay sendFrame by 150ms (longer than timeout) - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise((resolve) => setTimeout(resolve, 150)); return originalSendFrame(...args); }); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() { - // Respond immediately - } - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() { + // Respond immediately + } + })(), + ); // Start the command with 100ms timeout, if timeout starts immediately it will fire before // sendFrame completes and cause the command to fail await sendingNode.endpoints[1].clusters.onOff.toggle(undefined, { timeout: 100 }); }); - it('should clear timeout when response is received', async function() { + it('should clear timeout when response is received', async function () { const clearTimeoutSpy = sandbox.spy(global, 'clearTimeout'); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - async toggle() { - // Respond after sendFrame resolved - return new Promise(resolve => setImmediate(resolve)); - } - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async toggle() { + // Respond after sendFrame resolved + return new Promise((resolve) => setImmediate(resolve)); + } + })(), + ); await sendingNode.endpoints[1].clusters.onOff.toggle(undefined, { timeout: 5000 }); @@ -88,17 +90,17 @@ describe('Cluster Response Timeout', function() { assert(clearTimeoutSpy.called, 'clearTimeout should be called when response received'); }); - it('should not wait for response when waitForResponse is false', async function() { + it('should not wait for response when waitForResponse is false', async function () { const sendingNodeSendFrameSpy = sandbox.spy(sendingNode, 'sendFrame'); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() { - - // Respond with default response - } - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() { + // Respond with default response + } + })(), + ); // Send command without waiting for response const result = await sendingNode.endpoints[1].clusters.onOff.toggle({ waitForResponse: false }); @@ -110,20 +112,23 @@ describe('Cluster Response Timeout', function() { assert.strictEqual(sendingNodeSendFrameSpy.callCount, 1); }); - it('should not resolve with a device-initiated frame that reuses the pending TSN', async function() { + it('should not resolve with a device-initiated frame that reuses the pending TSN', async function () { const reportedValues = []; - sendingNode.endpoints[1].clusters.onOff.on('attr.onOff', value => reportedValues.push(value)); - - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - async toggle(args, meta, frame) { - // Device sends an attribute report numbered from its own counter, colliding with the TSN - await sendingNode.handleFrame(1, OnOffCluster.ID, Buffer.from([ - 0x18, frame.trxSequenceNumber, 0x0A, 0x00, 0x00, 0x10, 0x01, - ])); - } - - }()); + sendingNode.endpoints[1].clusters.onOff.on('attr.onOff', (value) => reportedValues.push(value)); + + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async toggle(args, meta, frame) { + // Device sends an attribute report numbered from its own counter, colliding with the TSN + await sendingNode.handleFrame( + 1, + OnOffCluster.ID, + Buffer.from([0x18, frame.trxSequenceNumber, 0x0a, 0x00, 0x00, 0x10, 0x01]), + ); + } + })(), + ); const result = await sendingNode.endpoints[1].clusters.onOff.toggle(); @@ -131,7 +136,7 @@ describe('Cluster Response Timeout', function() { assert.strictEqual(result, undefined); }); - it('should resolve getAlarm with a Get Alarm Response alone', async function() { + it('should resolve getAlarm with a Get Alarm Response alone', async function () { const pair = [AlarmsCluster.ID]; const sender = new Node({ sendFrame: (...args) => receiver.handleFrame(...args), @@ -141,15 +146,19 @@ describe('Cluster Response Timeout', function() { sendFrame: (...args) => sender.handleFrame(...args), endpointDescriptors: [{ endpointId: 1, inputClusters: pair }], }); - receiver.endpoints[1].bind('alarms', new class extends BoundCluster { - - getAlarm() { - return { - status: 'SUCCESS', alarmCode: 2, clusterId: 6, timeStamp: 1234, - }; - } - - }()); + receiver.endpoints[1].bind( + 'alarms', + new (class extends BoundCluster { + getAlarm() { + return { + status: 'SUCCESS', + alarmCode: 2, + clusterId: 6, + timeStamp: 1234, + }; + } + })(), + ); const { alarms } = sender.endpoints[1].clusters; const response = await alarms.getAlarm(undefined, { timeout: 1000 }); @@ -158,11 +167,10 @@ describe('Cluster Response Timeout', function() { assert.strictEqual(response.timeStamp, 1234); }); - it('should resolve a command without declared response on a cluster-specific reply', async function() { + it('should resolve a command without declared response on a cluster-specific reply', async function () { class TuyaLikeCluster extends Cluster { - static get ID() { - return 0xEF00; + return 0xef00; } static get NAME() { @@ -179,7 +187,6 @@ describe('Cluster Response Timeout', function() { }, }; } - } Cluster.addCluster(TuyaLikeCluster); @@ -193,20 +200,24 @@ describe('Cluster Response Timeout', function() { sendFrame: (...args) => sender.handleFrame(...args), endpointDescriptors: [{ endpointId: 1, inputClusters: pair }], }); - receiver.endpoints[1].bind('tuyaLike', new class extends BoundCluster { - - // Reply like a Tuya MCU: its own command with the request's TSN, no Default Response - async datapoint({ value }, meta, frame) { - await sender.handleFrame(1, TuyaLikeCluster.ID, Buffer.from([ - 0x19, frame.trxSequenceNumber, 0x01, value, - ])); - } - - }()); + receiver.endpoints[1].bind( + 'tuyaLike', + new (class extends BoundCluster { + // Reply like a Tuya MCU: its own command with the request's TSN, no Default Response + async datapoint({ value }, meta, frame) { + await sender.handleFrame( + 1, + TuyaLikeCluster.ID, + Buffer.from([0x19, frame.trxSequenceNumber, 0x01, value]), + ); + } + })(), + ); const { tuyaLike } = sender.endpoints[1].clusters; const response = await tuyaLike.datapoint( - { value: 7 }, { disableDefaultResponse: true, timeout: 1000 }, + { value: 7 }, + { disableDefaultResponse: true, timeout: 1000 }, ); assert.strictEqual(response.value, 7); @@ -215,11 +226,10 @@ describe('Cluster Response Timeout', function() { } }); - it('should treat a declared response without id as the command id', async function() { + it('should treat a declared response without id as the command id', async function () { class EchoCluster extends Cluster { - static get ID() { - return 0xFC01; + return 0xfc01; } static get NAME() { @@ -240,7 +250,6 @@ describe('Cluster Response Timeout', function() { }, }; } - } Cluster.addCluster(EchoCluster); @@ -254,17 +263,20 @@ describe('Cluster Response Timeout', function() { sendFrame: (...args) => sender.handleFrame(...args), endpointDescriptors: [{ endpointId: 1, inputClusters: pair }], }); - receiver.endpoints[1].bind('echo', new class extends BoundCluster { - - // An unrelated device command reuses the TSN first, then the real response (id 0x05) - async ping({ value }, meta, frame) { - await sender.handleFrame(1, EchoCluster.ID, Buffer.from([ - 0x19, frame.trxSequenceNumber, 0x06, 99, - ])); - return { value }; - } - - }()); + receiver.endpoints[1].bind( + 'echo', + new (class extends BoundCluster { + // An unrelated device command reuses the TSN first, then the real response (id 0x05) + async ping({ value }, meta, frame) { + await sender.handleFrame( + 1, + EchoCluster.ID, + Buffer.from([0x19, frame.trxSequenceNumber, 0x06, 99]), + ); + return { value }; + } + })(), + ); const { echo } = sender.endpoints[1].clusters; const response = await echo.ping({ value: 7 }, { timeout: 1000 }); @@ -275,7 +287,7 @@ describe('Cluster Response Timeout', function() { } }); - describe('pending response matching', function() { + describe('pending response matching', function () { // Node pair where the receiving side first sends `colliding(tsn)` with the request's TSN, // then its real reply function createCollidingPair(clusterId, colliding) { @@ -294,18 +306,19 @@ describe('Cluster Response Timeout', function() { return { sender, receiver }; } - it('should not resolve a global command on a cluster-specific frame with its response id', async function() { + it('should not resolve a global command on a cluster-specific frame with its response id', async function () { // IAS Zone Enroll Request is cluster-specific 0x01, the same id as Read Attributes Response - const { sender, receiver } = createCollidingPair(IasZoneCluster.ID, tsn => Buffer.from([ - 0x19, tsn, 0x01, 0x0D, 0x00, 0x00, 0x00, - ])); - receiver.endpoints[1].bind('iasZone', new class extends BoundCluster { - - get zoneState() { - return 'enrolled'; - } - - }()); + const { sender, receiver } = createCollidingPair(IasZoneCluster.ID, (tsn) => + Buffer.from([0x19, tsn, 0x01, 0x0d, 0x00, 0x00, 0x00]), + ); + receiver.endpoints[1].bind( + 'iasZone', + new (class extends BoundCluster { + get zoneState() { + return 'enrolled'; + } + })(), + ); const { iasZone } = sender.endpoints[1].clusters; const result = await iasZone.readAttributes(['zoneState'], { timeout: 1000 }); @@ -313,27 +326,29 @@ describe('Cluster Response Timeout', function() { assert.deepStrictEqual(result, { zoneState: 'enrolled' }); }); - it('should not resolve on a Default Response for another command', async function() { + it('should not resolve on a Default Response for another command', async function () { // Default Response with FAILURE for cmdId 0x01 (on), while toggle (0x02) is pending - const { sender, receiver } = createCollidingPair(OnOffCluster.ID, tsn => Buffer.from([ - 0x18, tsn, 0x0B, 0x01, 0x01, - ])); - receiver.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() {} - - }()); + const { sender, receiver } = createCollidingPair(OnOffCluster.ID, (tsn) => + Buffer.from([0x18, tsn, 0x0b, 0x01, 0x01]), + ); + receiver.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() {} + })(), + ); await sender.endpoints[1].clusters.onOff.toggle(undefined, { timeout: 1000 }); }); }); - it('should resolve when the response arrives before sendFrame rejects', async function() { - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() {} - - }()); + it('should resolve when the response arrives before sendFrame rejects', async function () { + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() {} + })(), + ); sendingNode.sendFrame = async (endpointId, clusterId, frame) => { await receivingNode.handleFrame(endpointId, clusterId, frame); throw new Error('device not responding'); @@ -342,7 +357,7 @@ describe('Cluster Response Timeout', function() { await sendingNode.endpoints[1].clusters.onOff.toggle(); }); - it('should reject when sendFrame rejects without a response', async function() { + it('should reject when sendFrame rejects without a response', async function () { sendingNode.sendFrame = async () => { throw new Error('device not responding'); }; diff --git a/test/testConfigureReportingRecordsStruct.js b/test/testConfigureReportingRecordsStruct.js index 016110b1..588c64c9 100644 --- a/test/testConfigureReportingRecordsStruct.js +++ b/test/testConfigureReportingRecordsStruct.js @@ -2,9 +2,7 @@ const assert = require('assert'); -const { - ZCLConfigureReportingRecords, -} = require('../lib/zclFrames'); +const { ZCLConfigureReportingRecords } = require('../lib/zclFrames'); const CONFIGURE_REPORTING_RECORDS_MOCK = [ { @@ -80,8 +78,8 @@ const CONFIGURE_REPORTING_RECORDS_WITH_STATUS_BUFFER_MOCK = Buffer.from( 'hex', ); -describe('command configure reporting', function() { - it('should parse multiple records to buffer', function() { +describe('command configure reporting', function () { + it('should parse multiple records to buffer', function () { // Create type instance const ZCLConfigureReportingRecordsType = ZCLConfigureReportingRecords(); @@ -95,13 +93,15 @@ describe('command configure reporting', function() { assert(buf.equals(CONFIGURE_REPORTING_RECORDS_BUFFER_MOCK)); }); - it('should parse buffer to multiple records', function() { + it('should parse buffer to multiple records', function () { // Create type instance const ZCLConfigureReportingRecordsType = ZCLConfigureReportingRecords(); // Allocate buffer to be filled with configure reporting records - const parsedRecords = ZCLConfigureReportingRecordsType - .fromBuffer(CONFIGURE_REPORTING_RECORDS_BUFFER_MOCK, 0); + const parsedRecords = ZCLConfigureReportingRecordsType.fromBuffer( + CONFIGURE_REPORTING_RECORDS_BUFFER_MOCK, + 0, + ); // Compare the parsed records against the initial records `configureReportingRecordsBuffer` for (let i = 0; i < CONFIGURE_REPORTING_RECORDS_MOCK.length; i++) { @@ -110,8 +110,8 @@ describe('command configure reporting', function() { }); }); -describe('command read configure reporting', function() { - it('should parse multiple records to buffer', function() { +describe('command read configure reporting', function () { + it('should parse multiple records to buffer', function () { // Create type instance const ZCLConfigureReportingRecordsWithStatusType = ZCLConfigureReportingRecords({ withStatus: true, @@ -129,15 +129,17 @@ describe('command read configure reporting', function() { assert(buf.equals(CONFIGURE_REPORTING_RECORDS_WITH_STATUS_BUFFER_MOCK)); }); - it('should parse buffer to multiple records', function() { + it('should parse buffer to multiple records', function () { // Create type instance const ZCLConfigureReportingRecordsWithStatusType = ZCLConfigureReportingRecords({ withStatus: true, }); // Allocate buffer to be filled with configure reporting records - const parsedRecords = ZCLConfigureReportingRecordsWithStatusType - .fromBuffer(CONFIGURE_REPORTING_RECORDS_WITH_STATUS_BUFFER_MOCK, 0); + const parsedRecords = ZCLConfigureReportingRecordsWithStatusType.fromBuffer( + CONFIGURE_REPORTING_RECORDS_WITH_STATUS_BUFFER_MOCK, + 0, + ); // Compare the parsed records against the initial records `configureReportingRecordsBuffer` for (let i = 0; i < CONFIGURE_REPORTING_RECORDS_WITH_STATUS_MOCK.length; i++) { diff --git a/test/testMfgSpecificCommands.js b/test/testMfgSpecificCommands.js index d29ea2bb..35a3830a 100644 --- a/test/testMfgSpecificCommands.js +++ b/test/testMfgSpecificCommands.js @@ -10,7 +10,6 @@ const MANUFACTURER_ID_1 = 0x1234; const MANUFACTURER_ID_2 = 0x4321; class IkeaSceneCluster extends SceneCluster { - static get ATTRIBUTES() { return { sceneCount: { @@ -40,7 +39,7 @@ class IkeaSceneCluster extends SceneCluster { ...super.COMMANDS, ikeaSceneStep: { id: 0x07, - manufacturerId: 0x117C, + manufacturerId: 0x117c, args: { mode: ZCLDataTypes.enum8({ up: 0, @@ -52,7 +51,7 @@ class IkeaSceneCluster extends SceneCluster { }, ikeaSceneMove: { id: 0x08, - manufacturerId: 0x117C, + manufacturerId: 0x117c, args: { mode: ZCLDataTypes.enum8({ up: 0, @@ -63,14 +62,13 @@ class IkeaSceneCluster extends SceneCluster { }, ikeaSceneMoveStop: { id: 0x09, - manufacturerId: 0x117C, + manufacturerId: 0x117c, args: { duration: ZCLDataTypes.uint16, }, }, }; } - } Cluster.addCluster(IkeaSceneCluster); @@ -78,14 +76,15 @@ const { createMockNode } = require('./util'); const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [SceneCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [SceneCluster.ID], + }, + ], }); class IkeaBoundCluster extends BoundCluster { - constructor({ onReadAttributes, onWriteAttributes, @@ -106,11 +105,10 @@ class IkeaBoundCluster extends BoundCluster { if (onIkeaSceneMove) this.ikeaSceneMove = onIkeaSceneMove; if (onIkeaSceneMoveStop) this.ikeaSceneMoveStop = onIkeaSceneMoveStop; } - } -describe('manufacturer specific commands', function() { - it('should execute custom commands', function(done) { +describe('manufacturer specific commands', function () { + it('should execute custom commands', function (done) { let commandsExecuted = 0; const ikeaSceneStepCommand = { mode: 'up', @@ -124,147 +122,182 @@ describe('manufacturer specific commands', function() { const ikeaSceneMoveStop = { duration: 1000, }; - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onIkeaSceneStep: payload => { - commandsExecuted++; - assert.deepStrictEqual({ ...payload }, ikeaSceneStepCommand); - if (commandsExecuted === 3) done(); - }, - onIkeaSceneMove: payload => { - commandsExecuted++; - assert.deepStrictEqual({ ...payload }, ikeaSceneMoveCommand); - if (commandsExecuted === 3) done(); - }, - onIkeaSceneMoveStop: payload => { - commandsExecuted++; - assert.deepStrictEqual({ ...payload }, ikeaSceneMoveStop); - if (commandsExecuted === 3) done(); - }, - })); + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onIkeaSceneStep: (payload) => { + commandsExecuted++; + assert.deepStrictEqual({ ...payload }, ikeaSceneStepCommand); + if (commandsExecuted === 3) done(); + }, + onIkeaSceneMove: (payload) => { + commandsExecuted++; + assert.deepStrictEqual({ ...payload }, ikeaSceneMoveCommand); + if (commandsExecuted === 3) done(); + }, + onIkeaSceneMoveStop: (payload) => { + commandsExecuted++; + assert.deepStrictEqual({ ...payload }, ikeaSceneMoveStop); + if (commandsExecuted === 3) done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].ikeaSceneStep(ikeaSceneStepCommand); node.endpoints[1].clusters['scenes'].ikeaSceneMove(ikeaSceneMoveCommand); node.endpoints[1].clusters['scenes'].ikeaSceneMoveStop(ikeaSceneMoveStop); }); - describe('global commands', function() { - describe('readAttributes', function() { - it('should set correct manufacturerId', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); - done(); - }, - })); + describe('global commands', function () { + describe('readAttributes', function () { + it('should set correct manufacturerId', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].readAttributes(['sceneCount']); }); - it('should throw when different manufacturerIds are found', function(done) { - node.endpoints[1].clusters['scenes'].readAttributes(['sceneCount', 'currentScene']) - .catch(err => { + it('should throw when different manufacturerIds are found', function (done) { + node.endpoints[1].clusters['scenes'] + .readAttributes(['sceneCount', 'currentScene']) + .catch((err) => { assert(err instanceof Error); done(); }); }); - it('should set correct manufacturerId for multiple attributes', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); - done(); - }, - })); + it('should set correct manufacturerId for multiple attributes', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].readAttributes(['currentScene', 'currentGroup']); }); - it('should not set manufacturerId when zcl attribute is present', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(undefined, rawPayload.manufacturerId); - done(); - }, - })); + it('should not set manufacturerId when zcl attribute is present', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(undefined, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].readAttributes(['sceneValid']); }); }); - describe('writeAttributes', function() { - it('should set correct manufacturerId', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onWriteAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); - done(); - }, - })); + describe('writeAttributes', function () { + it('should set correct manufacturerId', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onWriteAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].writeAttributes({ sceneCount: 1 }); }); - it('should throw when different manufacturerIds are found', function(done) { - node.endpoints[1].clusters['scenes'].writeAttributes({ sceneCount: 1, currentScene: 2 }) - .catch(err => { + it('should throw when different manufacturerIds are found', function (done) { + node.endpoints[1].clusters['scenes'] + .writeAttributes({ sceneCount: 1, currentScene: 2 }) + .catch((err) => { assert(err instanceof Error); done(); }); }); - it('should set correct manufacturerId for multiple attributes', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onWriteAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); - done(); - }, - })); + it('should set correct manufacturerId for multiple attributes', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onWriteAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].writeAttributes({ currentScene: 1, currentGroup: 2 }); }); - it('should not set manufacturerId when zcl attribute is present', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onWriteAttributes: (payload, meta, rawPayload) => { - assert.strictEqual(undefined, rawPayload.manufacturerId); - done(); - }, - })); + it('should not set manufacturerId when zcl attribute is present', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onWriteAttributes: (payload, meta, rawPayload) => { + assert.strictEqual(undefined, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].writeAttributes({ sceneValid: true }); }); }); - describe('configureReporting', function() { - it('should set correct manufacturerId', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onConfigureReporting: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); - done(); - }, - })); - node.endpoints[1].clusters['scenes'].configureReporting({ sceneCount: { minInterval: 0, maxInterval: 300, minChange: 1 } }); - }); - it('should throw when different manufacturerIds are found', function(done) { + describe('configureReporting', function () { + it('should set correct manufacturerId', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onConfigureReporting: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].configureReporting({ sceneCount: { minInterval: 0, maxInterval: 300, minChange: 1 }, - currentScene: { minInterval: 0, maxInterval: 300, minChange: 1 }, - }) - .catch(err => { + }); + }); + it('should throw when different manufacturerIds are found', function (done) { + node.endpoints[1].clusters['scenes'] + .configureReporting({ + sceneCount: { minInterval: 0, maxInterval: 300, minChange: 1 }, + currentScene: { minInterval: 0, maxInterval: 300, minChange: 1 }, + }) + .catch((err) => { assert(err instanceof Error); done(); }); }); - it('should set correct manufacturerId for multiple attributes', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onConfigureReporting: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); - done(); - }, - })); + it('should set correct manufacturerId for multiple attributes', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onConfigureReporting: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].configureReporting({ currentScene: { minInterval: 0, maxInterval: 300, minChange: 1 }, currentGroup: { minInterval: 0, maxInterval: 300, minChange: 1 }, }); }); - it('should not set manufacturerId when zcl attribute is present', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onConfigureReporting: (payload, meta, rawPayload) => { - assert.strictEqual(undefined, rawPayload.manufacturerId); - done(); - }, - })); + it('should not set manufacturerId when zcl attribute is present', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onConfigureReporting: (payload, meta, rawPayload) => { + assert.strictEqual(undefined, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].configureReporting({ sceneValid: { minInterval: 0, maxInterval: 300, minChange: 1 }, @@ -273,41 +306,57 @@ describe('manufacturer specific commands', function() { }); }); - describe('readReportingConfiguration', function() { - it('should set correct manufacturerId', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadReportingConfiguration: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); - done(); - }, - })); + describe('readReportingConfiguration', function () { + it('should set correct manufacturerId', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadReportingConfiguration: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_1, rawPayload.manufacturerId); + done(); + }, + }), + ); node.endpoints[1].clusters['scenes'].readReportingConfiguration(['sceneCount']); }); - it('should throw when different manufacturerIds are found', function(done) { - node.endpoints[1].clusters['scenes'].readReportingConfiguration(['sceneCount', 'currentScene']) - .catch(err => { + it('should throw when different manufacturerIds are found', function (done) { + node.endpoints[1].clusters['scenes'] + .readReportingConfiguration(['sceneCount', 'currentScene']) + .catch((err) => { assert(err instanceof Error); done(); }); }); - it('should set correct manufacturerId for multiple attributes', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadReportingConfiguration: (payload, meta, rawPayload) => { - assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); - done(); - }, - })); - node.endpoints[1].clusters['scenes'].readReportingConfiguration(['currentGroup', 'currentScene']); + it('should set correct manufacturerId for multiple attributes', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadReportingConfiguration: (payload, meta, rawPayload) => { + assert.strictEqual(MANUFACTURER_ID_2, rawPayload.manufacturerId); + done(); + }, + }), + ); + node.endpoints[1].clusters['scenes'].readReportingConfiguration([ + 'currentGroup', + 'currentScene', + ]); }); - it('should not set manufacturerId when zcl attribute is present', function(done) { - node.endpoints[1].bind('scenes', new IkeaBoundCluster({ - onReadReportingConfiguration: (payload, meta, rawPayload) => { - assert.strictEqual(undefined, rawPayload.manufacturerId); - done(); - }, - })); + it('should not set manufacturerId when zcl attribute is present', function (done) { + node.endpoints[1].bind( + 'scenes', + new IkeaBoundCluster({ + onReadReportingConfiguration: (payload, meta, rawPayload) => { + assert.strictEqual(undefined, rawPayload.manufacturerId); + done(); + }, + }), + ); - node.endpoints[1].clusters['scenes'].readReportingConfiguration(['currentGroup', 'sceneValid']); + node.endpoints[1].clusters['scenes'].readReportingConfiguration([ + 'currentGroup', + 'sceneValid', + ]); }); }); }); diff --git a/test/testNode.js b/test/testNode.js index 1826cca7..f1ad27c2 100644 --- a/test/testNode.js +++ b/test/testNode.js @@ -16,31 +16,37 @@ debug = debug.extend('test-node'); const sandbox = sinon.createSandbox(); -describe('Node', function() { +describe('Node', function () { let mockNode; let receivingNode; let sendingNode; - before(function() { + before(function () { mockNode = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [BasicCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [BasicCluster.ID], + }, + ], }); [sendingNode, receivingNode] = createConnectedNodePair( { - endpoints: [{ - endpointId: 1, - inputClusters: [BasicCluster.ID, OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [BasicCluster.ID, OnOffCluster.ID], + }, + ], }, { - endpoints: [{ - endpointId: 1, - inputClusters: [BasicCluster.ID, OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [BasicCluster.ID, OnOffCluster.ID], + }, + ], }, ); // Override log id for sending node @@ -52,10 +58,10 @@ describe('Node', function() { receivingNode.getLogId = receivingNodeLog; receivingNode.endpoints[1].getLogId = receivingNodeLog; }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); - it('should fail for unbound cluster', async function() { + it('should fail for unbound cluster', async function () { try { await mockNode.endpoints[1].clusters['basic'].configureReporting({ zclVersion: { @@ -66,10 +72,10 @@ describe('Node', function() { } catch { return; } - throw new Error('didn\'t throw'); + throw new Error("didn't throw"); }); - it('should fail for unimplemented command', async function() { + it('should fail for unimplemented command', async function () { mockNode.endpoints[1].bind('basic', new BoundCluster()); try { @@ -77,29 +83,31 @@ describe('Node', function() { } catch { return; } - throw new Error('didn\'t throw'); + throw new Error("didn't throw"); }); - it('should invoke command', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - async factoryReset() { - debug('factory reset'); - } - - }()); + it('should invoke command', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + async factoryReset() { + debug('factory reset'); + } + })(), + ); await mockNode.endpoints[1].clusters['basic'].factoryReset(); }); - it('should configure reporting', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - async configureReporting({ reports }) { - assert.equal(reports.length, 1, 'exactly 1 report'); - } - - }()); + it('should configure reporting', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + async configureReporting({ reports }) { + assert.equal(reports.length, 1, 'exactly 1 report'); + } + })(), + ); await mockNode.endpoints[1].clusters['basic'].configureReporting({ zclVersion: { @@ -109,25 +117,32 @@ describe('Node', function() { }); }); - it('should read attributes', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - constructor() { - super(); - this.dateCode = '1234'; - } - - get modelId() { - return 'test'; - } - - get manufacturerName() { - return 'Athom'; - } - - }()); + it('should read attributes', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + constructor() { + super(); + this.dateCode = '1234'; + } + + get modelId() { + return 'test'; + } + + get manufacturerName() { + return 'Athom'; + } + })(), + ); - const res = await mockNode.endpoints[1].clusters['basic'].readAttributes(['modelId', 'manufacturerName', 'clusterRevision', 'zclVersion', 'dateCode']); + const res = await mockNode.endpoints[1].clusters['basic'].readAttributes([ + 'modelId', + 'manufacturerName', + 'clusterRevision', + 'zclVersion', + 'dateCode', + ]); assert.equal(res.modelId, 'test', 'modelId should be test'); assert.equal(res.manufacturerName, 'Athom', 'manufacturerName should be test'); assert.equal(res.clusterRevision, 1, 'clusterRevision should be test'); @@ -135,108 +150,117 @@ describe('Node', function() { assert.equal(res.dateCode, '1234', 'dateCode should not be present'); }); - it('should read attributes by numeric ID', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'numericTest'; - } - - }()); + it('should read attributes by numeric ID', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'numericTest'; + } + })(), + ); // modelId has attribute ID 5 const res = await mockNode.endpoints[1].clusters['basic'].readAttributes([5]); assert.equal(res[5], 'numericTest', 'numeric ID 5 should return modelId value'); }); - it('should read attributes with mixed strings and numeric IDs', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'mixedTest'; - } - - get manufacturerName() { - return 'Athom'; - } - - }()); + it('should read attributes with mixed strings and numeric IDs', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'mixedTest'; + } + + get manufacturerName() { + return 'Athom'; + } + })(), + ); // manufacturerName by name, modelId (ID 5) by numeric ID - const res = await mockNode.endpoints[1].clusters['basic'].readAttributes(['manufacturerName', 5]); + const res = await mockNode.endpoints[1].clusters['basic'].readAttributes([ + 'manufacturerName', + 5, + ]); assert.equal(res.manufacturerName, 'Athom', 'manufacturerName should resolve by name'); assert.equal(res[5], 'mixedTest', 'numeric ID 5 should return modelId value'); }); - it('should return both name and numeric key when same attribute requested both ways', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'both'; - } - - }()); + it('should return both name and numeric key when same attribute requested both ways', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'both'; + } + })(), + ); const res = await mockNode.endpoints[1].clusters['basic'].readAttributes(['modelId', 5]); assert.equal(res.modelId, 'both', 'modelId key should exist'); assert.equal(res[5], 'both', 'numeric key 5 should also exist'); }); - it('should read an unregistered attribute by numeric ID', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster {}()); + it('should read an unregistered attribute by numeric ID', async function () { + mockNode.endpoints[1].bind('basic', new (class extends BoundCluster {})()); // 0x1234 is not registered in the basic cluster, expect no error and empty/missing result const res = await mockNode.endpoints[1].clusters['basic'].readAttributes([0x1234]); assert.equal(res[0x1234], undefined, 'unregistered attribute should not have a value'); }); - it('should throw for invalid numeric attribute IDs', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster {}()); + it('should throw for invalid numeric attribute IDs', async function () { + mockNode.endpoints[1].bind('basic', new (class extends BoundCluster {})()); - await assert.rejects( - () => mockNode.endpoints[1].clusters['basic'].readAttributes([-1]), - { name: 'TypeError' }, - ); - await assert.rejects( - () => mockNode.endpoints[1].clusters['basic'].readAttributes([0x10000]), - { name: 'TypeError' }, - ); - await assert.rejects( - () => mockNode.endpoints[1].clusters['basic'].readAttributes([1.5]), - { name: 'TypeError' }, - ); - await assert.rejects( - () => mockNode.endpoints[1].clusters['basic'].readAttributes([NaN]), - { name: 'TypeError' }, - ); + await assert.rejects(() => mockNode.endpoints[1].clusters['basic'].readAttributes([-1]), { + name: 'TypeError', + }); + await assert.rejects(() => mockNode.endpoints[1].clusters['basic'].readAttributes([0x10000]), { + name: 'TypeError', + }); + await assert.rejects(() => mockNode.endpoints[1].clusters['basic'].readAttributes([1.5]), { + name: 'TypeError', + }); + await assert.rejects(() => mockNode.endpoints[1].clusters['basic'].readAttributes([NaN]), { + name: 'TypeError', + }); }); - it('should respond with read attribute response when disableDefaultResponse is true', async function() { - receivingNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'test'; - } - - }()); + it('should respond with read attribute response when disableDefaultResponse is true', async function () { + receivingNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'test'; + } + })(), + ); - const readAttributesResponse = await sendingNode.endpoints[1].clusters['basic'].readAttributes(['modelId'], { disableDefaultResponse: true }); + const readAttributesResponse = await sendingNode.endpoints[1].clusters['basic'].readAttributes( + ['modelId'], + { disableDefaultResponse: true }, + ); assert.equal(readAttributesResponse.modelId, 'test'); }); - it('should not respond with a default response when disableDefaultResponse is true', async function() { - receivingNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'test'; - } - - }()); + it('should not respond with a default response when disableDefaultResponse is true', async function () { + receivingNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'test'; + } + })(), + ); const sendingNodeSendFrameSpy = sandbox.spy(sendingNode, 'sendFrame'); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); - await sendingNode.endpoints[1].clusters['basic'].readAttributes(['modelId'], { disableDefaultResponse: true }); + await sendingNode.endpoints[1].clusters['basic'].readAttributes(['modelId'], { + disableDefaultResponse: true, + }); // 1 frame sent by sending node, the read attributes command assert.equal(sendingNodeSendFrameSpy.callCount, 1); @@ -250,14 +274,15 @@ describe('Node', function() { assert.equal(frame.cmdId, 1); }); - it('should not respond to a default response frame', async function() { - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() { - debug('toggled'); - } - - }()); + it('should not respond to a default response frame', async function () { + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() { + debug('toggled'); + } + })(), + ); const sendingNodeSendFrameSpy = sandbox.spy(sendingNode, 'sendFrame'); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); @@ -275,32 +300,35 @@ describe('Node', function() { assert.equal(frame.cmdId, 11); }); - it('should respond with default response error when frame could not be handled', async function() { - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster {}()); + it('should respond with default response error when frame could not be handled', async function () { + receivingNode.endpoints[1].bind('onOff', new (class extends BoundCluster {})()); const sendingNodeHandleFrameSpy = sandbox.spy(sendingNode, 'handleFrame'); // Execute toggle and expect UNSUP_COMMAND as the command is not implemented (ZCL R8 2.5.12.2) - await assert.rejects(sendingNode.endpoints[1].clusters['onOff'].toggle(), { message: 'UNSUP_CLUSTER_COMMAND' }); + await assert.rejects(sendingNode.endpoints[1].clusters['onOff'].toggle(), { + message: 'UNSUP_CLUSTER_COMMAND', + }); // 1 frame received by sending node, the default response error assert.equal(sendingNodeHandleFrameSpy.callCount, 1); // Parse the received frame const frame = Endpoint.parseFrame(sendingNodeHandleFrameSpy.getCall(0).args[2]); - assert.equal(frame.cmdId, 0x0B); + assert.equal(frame.cmdId, 0x0b); assert.equal(frame.data[1], 0x81); // UNSUP_COMMAND }); - it('should not respond to a group cast frame', async function() { + it('should not respond to a group cast frame', async function () { const toggledSpy = sandbox.spy(); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() { - toggledSpy(); - } - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() { + toggledSpy(); + } + })(), + ); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); @@ -311,15 +339,16 @@ describe('Node', function() { assert.equal(receivingNodeSendFrameSpy.callCount, 0); }); - it('should not send a default response to a broadcast frame', async function() { + it('should not send a default response to a broadcast frame', async function () { const toggledSpy = sandbox.spy(); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() { - toggledSpy(); - } - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() { + toggledSpy(); + } + })(), + ); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); @@ -329,8 +358,8 @@ describe('Node', function() { assert.equal(receivingNodeSendFrameSpy.callCount, 0); }); - it('should not send a default response error to a broadcast frame', async function() { - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster {}()); + it('should not send a default response error to a broadcast frame', async function () { + receivingNode.endpoints[1].bind('onOff', new (class extends BoundCluster {})()); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); @@ -339,79 +368,82 @@ describe('Node', function() { assert.equal(receivingNodeSendFrameSpy.callCount, 0); }); - it('should send a default response to a unicast frame for the broadcast endpoint', async function() { - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() {} - - }()); + it('should send a default response to a unicast frame for the broadcast endpoint', async function () { + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() {} + })(), + ); const receivingNodeSendFrameSpy = sandbox.spy(receivingNode, 'sendFrame'); - await receivingNode.handleFrame(1, 6, Buffer.from([0x01, 0x01, 0x02]), { dstEndpoint: 0xFF }); + await receivingNode.handleFrame(1, 6, Buffer.from([0x01, 0x01, 0x02]), { dstEndpoint: 0xff }); assert.equal(receivingNodeSendFrameSpy.callCount, 1); }); - it('should write attributes', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'test'; - } - - get manufacturerName() { - return 'Athom'; - } - - set modelId(val) { - assert.equal(val, 'test1'); - } - - }()); + it('should write attributes', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'test'; + } + + get manufacturerName() { + return 'Athom'; + } + + set modelId(val) { + assert.equal(val, 'test1'); + } + })(), + ); await mockNode.endpoints[1].clusters['basic'].writeAttributes({ modelId: 'test1', }); }); - it('should discover attributes', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - get modelId() { - return 'test'; - } - - get manufacturerName() { - return 'Athom'; - } - - set modelId(val) { - assert.equal(val, 'test1'); - } - - }()); + it('should discover attributes', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + get modelId() { + return 'test'; + } + + get manufacturerName() { + return 'Athom'; + } + + set modelId(val) { + assert.equal(val, 'test1'); + } + })(), + ); const attrs = await mockNode.endpoints[1].clusters['basic'].discoverAttributes(); - ['modelId', 'manufacturerName'].forEach(a => { + ['modelId', 'manufacturerName'].forEach((a) => { assert(attrs.includes(a), `${a} is missing`); }); }); - it('should discover received commands', async function() { - mockNode.endpoints[1].bind('basic', new class extends BoundCluster { - - async factoryReset() { - debug('factory reset'); - } - - }()); + it('should discover received commands', async function () { + mockNode.endpoints[1].bind( + 'basic', + new (class extends BoundCluster { + async factoryReset() { + debug('factory reset'); + } + })(), + ); const cmds = await mockNode.endpoints[1].clusters['basic'].discoverCommandsReceived(); assert(cmds.includes('factoryReset')); }); - it('should support adding a Cluster with string attribute with 0x0000 ID', async function() { + it('should support adding a Cluster with string attribute with 0x0000 ID', async function () { class MyCluster extends Cluster { - static get ID() { return 0x1234; } @@ -421,7 +453,6 @@ describe('Node', function() { priceOption: { id: 0x0000, type: ZCLDataTypes.string }, }; } - } Cluster.addCluster(MyCluster); diff --git a/test/testTrxSequenceNumber.js b/test/testTrxSequenceNumber.js index 430857d2..cdee59b7 100644 --- a/test/testTrxSequenceNumber.js +++ b/test/testTrxSequenceNumber.js @@ -10,12 +10,12 @@ const OnOffCluster = require('../lib/clusters/onOff'); const sandbox = sinon.createSandbox(); -describe('Transaction sequence number', function() { +describe('Transaction sequence number', function () { let receivingNode; let sendingNode; let sentTrxSequenceNumbers; - beforeEach(function() { + beforeEach(function () { sentTrxSequenceNumbers = []; sendingNode = new Node({ sendFrame: (endpointId, clusterId, frame) => { @@ -37,18 +37,19 @@ describe('Transaction sequence number', function() { { endpointId: 2, inputClusters: [BasicCluster.ID] }, ], }); - receivingNode.endpoints[1].bind('onOff', new class extends BoundCluster { - - toggle() {} - - }()); + receivingNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + toggle() {} + })(), + ); }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); - it('should share one counter across endpoints and clusters', async function() { + it('should share one counter across endpoints and clusters', async function () { await sendingNode.endpoints[1].clusters.onOff.toggle(); const { basic } = sendingNode.endpoints[2].clusters; await basic.factoryReset(undefined, { waitForResponse: false }); @@ -58,7 +59,7 @@ describe('Transaction sequence number', function() { assert.deepStrictEqual(sentTrxSequenceNumbers, [first, (first + 1) % 256, (first + 2) % 256]); }); - it('should start at a random value and wrap around', function() { + it('should start at a random value and wrap around', function () { sandbox.stub(Math, 'random').returns(0.999); const node = new Node({ sendFrame: () => {}, endpointDescriptors: [] }); diff --git a/test/testZCLError.js b/test/testZCLError.js index 4c6afa51..8b00be3e 100644 --- a/test/testZCLError.js +++ b/test/testZCLError.js @@ -47,204 +47,238 @@ function makeToClientFrame(cmdId) { return header.toBuffer(); } -describe('ZCLError handling', function() { - it('should return ZCLError status code in Default Response when BoundCluster throws ZCLError', async function() { +describe('ZCLError handling', function () { + it('should return ZCLError status code in Default Response when BoundCluster throws ZCLError', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new ZCLError('NOT_AUTHORIZED'); - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new ZCLError('NOT_AUTHORIZED'); + } + })(), + ); await assert.rejects( () => node.endpoints[1].clusters.onOff.setOn(), - err => { + (err) => { assert.strictEqual(err.message, 'NOT_AUTHORIZED'); return true; }, ); }); - it('should return FAILURE status in Default Response when BoundCluster throws a generic Error', async function() { + it('should return FAILURE status in Default Response when BoundCluster throws a generic Error', async function () { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new Error('something went wrong'); - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new Error('something went wrong'); + } + })(), + ); await assert.rejects( () => node.endpoints[1].clusters.onOff.setOn(), - err => { + (err) => { assert.strictEqual(err.message, 'FAILURE'); return true; }, ); }); - it('should encode ZCLError status code in Default Response frame bytes', async function() { + it('should encode ZCLError status code in Default Response frame bytes', async function () { // NOT_AUTHORIZED = 0x7e per ZCL spec const NOT_AUTHORIZED_STATUS_BYTE = 0x7e; const SET_ON_CMD_ID = OnOffCluster.COMMANDS.setOn.id; - const DEFAULT_RESPONSE_CMD_ID = 0x0B; + const DEFAULT_RESPONSE_CMD_ID = 0x0b; const { controllerNode, deviceNode, capturedFrames } = createSpyNodePair(); - deviceNode.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new ZCLError('NOT_AUTHORIZED'); - } - - })()); + deviceNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new ZCLError('NOT_AUTHORIZED'); + } + })(), + ); await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn()); // capturedFrames only contains frames sent from the device back to the controller. // Find the Default Response frame among the captured device → controller frames. const defaultResponseFrame = capturedFrames - .map(f => ZCLStandardHeader.fromBuffer(f.data)) - .find(frame => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); + .map((f) => ZCLStandardHeader.fromBuffer(f.data)) + .find((frame) => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); assert.ok(defaultResponseFrame, 'Default Response frame was sent'); - assert.strictEqual(defaultResponseFrame.data[0], SET_ON_CMD_ID, - 'Default Response should reference the received setOn command ID'); - assert.strictEqual(defaultResponseFrame.data[1], NOT_AUTHORIZED_STATUS_BYTE, - 'Default Response should encode NOT_AUTHORIZED (0x7e) as the status byte'); + assert.strictEqual( + defaultResponseFrame.data[0], + SET_ON_CMD_ID, + 'Default Response should reference the received setOn command ID', + ); + assert.strictEqual( + defaultResponseFrame.data[1], + NOT_AUTHORIZED_STATUS_BYTE, + 'Default Response should encode NOT_AUTHORIZED (0x7e) as the status byte', + ); }); - it('should encode FAILURE (0x01) in Default Response frame bytes when a generic Error is thrown', async function() { + it('should encode FAILURE (0x01) in Default Response frame bytes when a generic Error is thrown', async function () { const FAILURE_STATUS_BYTE = 0x01; const SET_ON_CMD_ID = OnOffCluster.COMMANDS.setOn.id; - const DEFAULT_RESPONSE_CMD_ID = 0x0B; + const DEFAULT_RESPONSE_CMD_ID = 0x0b; const { controllerNode, deviceNode, capturedFrames } = createSpyNodePair(); - deviceNode.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new Error('something went wrong'); - } - - })()); + deviceNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new Error('something went wrong'); + } + })(), + ); await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn()); const defaultResponseFrame = capturedFrames - .map(f => ZCLStandardHeader.fromBuffer(f.data)) - .find(frame => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); + .map((f) => ZCLStandardHeader.fromBuffer(f.data)) + .find((frame) => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); assert.ok(defaultResponseFrame, 'Default Response frame was sent'); - assert.strictEqual(defaultResponseFrame.data[0], SET_ON_CMD_ID, - 'Default Response should reference the received setOn command ID'); - assert.strictEqual(defaultResponseFrame.data[1], FAILURE_STATUS_BYTE, - 'Default Response should encode FAILURE (0x01) as the status byte'); + assert.strictEqual( + defaultResponseFrame.data[0], + SET_ON_CMD_ID, + 'Default Response should reference the received setOn command ID', + ); + assert.strictEqual( + defaultResponseFrame.data[1], + FAILURE_STATUS_BYTE, + 'Default Response should encode FAILURE (0x01) as the status byte', + ); }); - it('should send SUCCESS status byte when ZCLError("SUCCESS") is thrown', async function() { + it('should send SUCCESS status byte when ZCLError("SUCCESS") is thrown', async function () { const SUCCESS_STATUS_BYTE = 0x00; const SET_ON_CMD_ID = OnOffCluster.COMMANDS.setOn.id; - const DEFAULT_RESPONSE_CMD_ID = 0x0B; + const DEFAULT_RESPONSE_CMD_ID = 0x0b; const { controllerNode, deviceNode, capturedFrames } = createSpyNodePair(); - deviceNode.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new ZCLError('SUCCESS'); - } - - })()); + deviceNode.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new ZCLError('SUCCESS'); + } + })(), + ); // The controller should not reject — a SUCCESS default response resolves the command await controllerNode.endpoints[1].clusters.onOff.setOn(); const defaultResponseFrame = capturedFrames - .map(f => ZCLStandardHeader.fromBuffer(f.data)) - .find(frame => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); + .map((f) => ZCLStandardHeader.fromBuffer(f.data)) + .find((frame) => frame.cmdId === DEFAULT_RESPONSE_CMD_ID); assert.ok(defaultResponseFrame, 'Default Response frame was sent'); - assert.strictEqual(defaultResponseFrame.data[0], SET_ON_CMD_ID, - 'Default Response should reference the received setOn command ID'); - assert.strictEqual(defaultResponseFrame.data[1], SUCCESS_STATUS_BYTE, - 'Default Response should encode SUCCESS (0x00) as the status byte'); + assert.strictEqual( + defaultResponseFrame.data[0], + SET_ON_CMD_ID, + 'Default Response should reference the received setOn command ID', + ); + assert.strictEqual( + defaultResponseFrame.data[1], + SUCCESS_STATUS_BYTE, + 'Default Response should encode SUCCESS (0x00) as the status byte', + ); }); // ZCL R8 2.5.12.2 - it('should send UNSUP_COMMAND (0x81) for a command the BoundCluster does not implement', async function() { + it('should send UNSUP_COMMAND (0x81) for a command the BoundCluster does not implement', async function () { const { controllerNode, deviceNode, capturedFrames } = createSpyNodePair(); deviceNode.endpoints[1].bind('onOff', new BoundCluster()); - await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn(), { message: 'UNSUP_CLUSTER_COMMAND' }); + await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn(), { + message: 'UNSUP_CLUSTER_COMMAND', + }); const defaultResponseFrame = ZCLStandardHeader.fromBuffer(capturedFrames[0].data); - assert.strictEqual(defaultResponseFrame.cmdId, 0x0B); + assert.strictEqual(defaultResponseFrame.cmdId, 0x0b); assert.strictEqual(defaultResponseFrame.data[1], 0x81); }); - it('should send UNSUP_COMMAND (0x81) for an unknown command id on a client cluster', async function() { + it('should send UNSUP_COMMAND (0x81) for an unknown command id on a client cluster', async function () { const { deviceNode, capturedFrames } = createSpyNodePair(); await deviceNode.handleFrame(1, OnOffCluster.ID, makeToClientFrame(0x7f), {}); assert.strictEqual(ZCLStandardHeader.fromBuffer(capturedFrames[0].data).data[1], 0x81); }); - it('should send UNSUPPORTED_CLUSTER (0xc3) for a server-to-client frame on a cluster the endpoint lacks', async function() { + it('should send UNSUPPORTED_CLUSTER (0xc3) for a server-to-client frame on a cluster the endpoint lacks', async function () { const { deviceNode, capturedFrames } = createSpyNodePair(); await deviceNode.handleFrame(1, LevelControlCluster.ID, makeToClientFrame(0x00), {}); assert.strictEqual(ZCLStandardHeader.fromBuffer(capturedFrames[0].data).data[1], 0xc3); }); - it('should send UNSUPPORTED_CLUSTER (0xc3) when the endpoint has no binding for the cluster', async function() { + it('should send UNSUPPORTED_CLUSTER (0xc3) when the endpoint has no binding for the cluster', async function () { const { controllerNode, capturedFrames } = createSpyNodePair(); - await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn(), { message: 'UNSUPPORTED_CLUSTER' }); + await assert.rejects(() => controllerNode.endpoints[1].clusters.onOff.setOn(), { + message: 'UNSUPPORTED_CLUSTER', + }); const defaultResponseFrame = ZCLStandardHeader.fromBuffer(capturedFrames[0].data); - assert.strictEqual(defaultResponseFrame.cmdId, 0x0B); + assert.strictEqual(defaultResponseFrame.cmdId, 0x0b); assert.strictEqual(defaultResponseFrame.data[1], 0xc3); }); - it('should propagate different ZCLError status codes', async function() { + it('should propagate different ZCLError status codes', async function () { const statuses = ['NOT_AUTHORIZED', 'INVALID_VALUE', 'INSUFFICIENT_SPACE']; for (const status of statuses) { const node = createMockNode({ loopback: true, - endpoints: [{ - endpointId: 1, - inputClusters: [OnOffCluster.ID], - }], + endpoints: [ + { + endpointId: 1, + inputClusters: [OnOffCluster.ID], + }, + ], }); - node.endpoints[1].bind('onOff', new (class extends BoundCluster { - - async setOn() { - throw new ZCLError(status); - } - - })()); + node.endpoints[1].bind( + 'onOff', + new (class extends BoundCluster { + async setOn() { + throw new ZCLError(status); + } + })(), + ); await assert.rejects( () => node.endpoints[1].clusters.onOff.setOn(), - err => { + (err) => { assert.strictEqual(err.message, status); return true; }, diff --git a/test/util/mockNode.js b/test/util/mockNode.js index a303794e..59e5f47c 100644 --- a/test/util/mockNode.js +++ b/test/util/mockNode.js @@ -21,17 +21,15 @@ const ElectricalMeasurementCluster = require('../../lib/clusters/electricalMeasu */ function createBoundClusterWithAttributes(attributes) { const ClusterClass = class extends BoundCluster { - constructor() { super(); // Store mutable attributes this._attributes = { ...attributes }; } - }; // Define getters/setters for each attribute - Object.keys(attributes).forEach(attrName => { + Object.keys(attributes).forEach((attrName) => { Object.defineProperty(ClusterClass.prototype, attrName, { get() { return this._attributes[attrName]; @@ -78,7 +76,7 @@ function createBoundClusterWithAttributes(attributes) { * @returns {Node} Configured mock Node */ function createMockNode(config) { - const endpointDescriptors = config.endpoints.map(ep => ({ + const endpointDescriptors = config.endpoints.map((ep) => ({ endpointId: ep.endpointId, inputClusters: ep.inputClusters || [], outputClusters: ep.outputClusters || [], @@ -87,16 +85,14 @@ function createMockNode(config) { // Create node reference first so sendFrame can reference it let node; const mockNode = { - sendFrame: config.loopback - ? (...args) => node.handleFrame(...args) - : () => Promise.resolve(), + sendFrame: config.loopback ? (...args) => node.handleFrame(...args) : () => Promise.resolve(), endpointDescriptors, }; node = new Node(mockNode); // Bind clusters with preset attribute values - config.endpoints.forEach(ep => { + config.endpoints.forEach((ep) => { if (!ep.clusters) return; Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { @@ -116,13 +112,13 @@ function createMockNode(config) { * @returns {[Node, Node]} Tuple of connected nodes */ function createConnectedNodePair(config1, config2) { - const endpointDescriptors1 = config1.endpoints.map(ep => ({ + const endpointDescriptors1 = config1.endpoints.map((ep) => ({ endpointId: ep.endpointId, inputClusters: ep.inputClusters || [], outputClusters: ep.outputClusters || [], })); - const endpointDescriptors2 = config2.endpoints.map(ep => ({ + const endpointDescriptors2 = config2.endpoints.map((ep) => ({ endpointId: ep.endpointId, inputClusters: ep.inputClusters || [], outputClusters: ep.outputClusters || [], @@ -145,7 +141,7 @@ function createConnectedNodePair(config1, config2) { node2 = new Node(mockNode2); // Bind clusters for node1 - config1.endpoints.forEach(ep => { + config1.endpoints.forEach((ep) => { if (!ep.clusters) return; Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { const boundCluster = createBoundClusterWithAttributes(attributes); @@ -154,7 +150,7 @@ function createConnectedNodePair(config1, config2) { }); // Bind clusters for node2 - config2.endpoints.forEach(ep => { + config2.endpoints.forEach((ep) => { if (!ep.clusters) return; Object.entries(ep.clusters).forEach(([clusterName, attributes]) => { const boundCluster = createBoundClusterWithAttributes(attributes); @@ -172,140 +168,152 @@ const MOCK_DEVICES = { /** * IAS Zone Motion Sensor */ - motionSensor: (overrides = {}) => createMockNode({ - endpoints: [{ - endpointId: 1, - inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], - clusters: { - basic: { - zclVersion: 3, - manufacturerName: 'MockDevice', - modelId: 'MockMotionSensor', - ...overrides.basic, - }, - powerConfiguration: { - batteryPercentageRemaining: 200, // 100% - ...overrides.powerConfiguration, + motionSensor: (overrides = {}) => + createMockNode({ + endpoints: [ + { + endpointId: 1, + inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockMotionSensor', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, // 100% + ...overrides.powerConfiguration, + }, + iasZone: { + zoneState: 1, // Enrolled + zoneType: 0x000d, // Motion sensor + zoneStatus: 0, + iasCIEAddress: '0x0000000000000000', + zoneId: 1, + ...overrides.iasZone, + }, + }, }, - iasZone: { - zoneState: 1, // Enrolled - zoneType: 0x000D, // Motion sensor - zoneStatus: 0, - iasCIEAddress: '0x0000000000000000', - zoneId: 1, - ...overrides.iasZone, - }, - }, - }], - }), + ], + }), /** * IAS Zone Contact Sensor (door/window) */ - contactSensor: (overrides = {}) => createMockNode({ - endpoints: [{ - endpointId: 1, - inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], - clusters: { - basic: { - zclVersion: 3, - manufacturerName: 'MockDevice', - modelId: 'MockContactSensor', - ...overrides.basic, + contactSensor: (overrides = {}) => + createMockNode({ + endpoints: [ + { + endpointId: 1, + inputClusters: [BasicCluster.ID, PowerConfigurationCluster.ID, IASZoneCluster.ID], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockContactSensor', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, + ...overrides.powerConfiguration, + }, + iasZone: { + zoneState: 1, + zoneType: 0x0015, // Contact switch + zoneStatus: 0, + iasCIEAddress: '0x0000000000000000', + zoneId: 1, + ...overrides.iasZone, + }, + }, }, - powerConfiguration: { - batteryPercentageRemaining: 200, - ...overrides.powerConfiguration, - }, - iasZone: { - zoneState: 1, - zoneType: 0x0015, // Contact switch - zoneStatus: 0, - iasCIEAddress: '0x0000000000000000', - zoneId: 1, - ...overrides.iasZone, - }, - }, - }], - }), + ], + }), /** * Temperature + Humidity Sensor */ - tempHumiditySensor: (overrides = {}) => createMockNode({ - endpoints: [{ - endpointId: 1, - inputClusters: [ - BasicCluster.ID, - PowerConfigurationCluster.ID, - TemperatureMeasurementCluster.ID, - RelativeHumidityCluster.ID, - ], - clusters: { - basic: { - zclVersion: 3, - manufacturerName: 'MockDevice', - modelId: 'MockTempHumidity', - ...overrides.basic, + tempHumiditySensor: (overrides = {}) => + createMockNode({ + endpoints: [ + { + endpointId: 1, + inputClusters: [ + BasicCluster.ID, + PowerConfigurationCluster.ID, + TemperatureMeasurementCluster.ID, + RelativeHumidityCluster.ID, + ], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockTempHumidity', + ...overrides.basic, + }, + powerConfiguration: { + batteryPercentageRemaining: 200, + ...overrides.powerConfiguration, + }, + temperatureMeasurement: { + measuredValue: 2150, // 21.50°C + minMeasuredValue: -4000, + maxMeasuredValue: 8500, + ...overrides.temperatureMeasurement, + }, + relativeHumidity: { + measuredValue: 6500, // 65.00% + minMeasuredValue: 0, + maxMeasuredValue: 10000, + ...overrides.relativeHumidity, + }, + }, }, - powerConfiguration: { - batteryPercentageRemaining: 200, - ...overrides.powerConfiguration, - }, - temperatureMeasurement: { - measuredValue: 2150, // 21.50°C - minMeasuredValue: -4000, - maxMeasuredValue: 8500, - ...overrides.temperatureMeasurement, - }, - relativeHumidity: { - measuredValue: 6500, // 65.00% - minMeasuredValue: 0, - maxMeasuredValue: 10000, - ...overrides.relativeHumidity, - }, - }, - }], - }), + ], + }), /** * Smart Plug with Power Metering */ - smartPlug: (overrides = {}) => createMockNode({ - endpoints: [{ - endpointId: 1, - inputClusters: [ - BasicCluster.ID, - OnOffCluster.ID, - MeteringCluster.ID, - ElectricalMeasurementCluster.ID, - ], - clusters: { - basic: { - zclVersion: 3, - manufacturerName: 'MockDevice', - modelId: 'MockSmartPlug', - ...overrides.basic, + smartPlug: (overrides = {}) => + createMockNode({ + endpoints: [ + { + endpointId: 1, + inputClusters: [ + BasicCluster.ID, + OnOffCluster.ID, + MeteringCluster.ID, + ElectricalMeasurementCluster.ID, + ], + clusters: { + basic: { + zclVersion: 3, + manufacturerName: 'MockDevice', + modelId: 'MockSmartPlug', + ...overrides.basic, + }, + onOff: { + onOff: false, + ...overrides.onOff, + }, + metering: { + currentSummationDelivered: 12345678, + multiplier: 1, + divisor: 1000, + ...overrides.metering, + }, + electricalMeasurement: { + activePower: 1500, // 150.0W + rmsVoltage: 2300, // 230.0V + rmsCurrent: 652, // 0.652A + ...overrides.electricalMeasurement, + }, + }, }, - onOff: { - onOff: false, - ...overrides.onOff, - }, - metering: { - currentSummationDelivered: 12345678, - multiplier: 1, - divisor: 1000, - ...overrides.metering, - }, - electricalMeasurement: { - activePower: 1500, // 150.0W - rmsVoltage: 2300, // 230.0V - rmsCurrent: 652, // 0.652A - ...overrides.electricalMeasurement, - }, - }, - }], - }), + ], + }), }; module.exports = { diff --git a/tsconfig.json b/tsconfig.json index 82403b49..a7c74bd0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,6 @@ "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "declaration": false, - "skipLibCheck": false, + "skipLibCheck": false } } From 545233dd274309b8b2ef72560e03533e1e8e56fd Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 4/6] docs: Record where the M/O annotation goes, and lowercase hex The annotation moves above the key, which is both what it means and what survives Prettier. Prettier also lowercases hex digits, so the id examples follow. --- AGENTS.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a28a2e6f..bff09c9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,7 +172,8 @@ const COMMANDS = { // --- Client to Server Commands --- // Description from spec. - commandName: { // Mandatory + // Mandatory + commandName: { id: 0x0000, args: { argName: ZCLDataTypes.uint8, @@ -186,7 +187,8 @@ const COMMANDS = { // --- Server to Client Commands --- // Description from spec. - notificationName: { // Optional + // Optional + notificationName: { id: 0x0020, // 32 direction: Cluster.DIRECTION_SERVER_TO_CLIENT, args: { @@ -224,11 +226,13 @@ module.exports = ExampleCluster; 1. **Description placement**: ABOVE the attribute/command 2. **M/O marker placement**: - Single-line: at END of line (`attrName: { ... }, // Mandatory`) - - Multi-line: on OPENING brace (`attrName: { // Mandatory`) + - Multi-line: on its own line ABOVE the key (`// Mandatory` then `attrName: {`), + below the description if there is one. Not on the opening brace: Prettier pulls a + comment there inside the braces, where it reads as if it belongs to `id`. - NEVER on closing brace 3. **Copy exactly**: Text from spec, 1-on-1 4. **Skip if >5 sentences**: Skip and only refer to section in spec -5. **Line wrap**: Respect 100 char limit (ESLint) +5. **Line wrap**: Respect 100 char limit (Prettier `printWidth`); run `npm run format:write` 6. **M/O source for attrs**: Server-side column in spec table 7. **M/O source for cmds**: - Server "receives" → server-side M/O @@ -252,7 +256,7 @@ pirUnoccupiedToOccupiedDelay: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, | Field | Format | Notes | |-------|--------|-------| -| `id` | Hex (`0x0000`) | Always 4-digit format (0x0000); add decimal comment if > 9 | +| `id` | Hex (`0x0000`) | Always 4-digit format (0x0000), lowercase digits; add decimal comment if > 9 | | `type` | `ZCLDataTypes.*` | See type reference below | | M/O | Inline comment | `// Mandatory`, `// Optional`, or `// Conditional` | @@ -260,7 +264,7 @@ pirUnoccupiedToOccupiedDelay: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, For hex values > 9 (where hex differs from decimal), add decimal in comment: ```javascript -id: 0x000A, // 10 +id: 0x000a, // 10 id: 0x0010, // 16 id: 0x0100, // 256 ``` @@ -271,9 +275,10 @@ id: 0x0000, id: 0x0009, ``` -For multi-line definitions, decimal goes on the `id:` line, M/O on opening brace: +For multi-line definitions, decimal goes on the `id:` line, M/O above the key: ```javascript -operatingMode: { // Optional +// Optional +operatingMode: { id: 0x0025, // 37 type: ZCLDataTypes.enum8({ normal: 0, @@ -300,7 +305,7 @@ attr3: { ... }, | Field | Required | Notes | |-------|----------|-------| -| `id` | Yes | Always 4-digit hex format (0x0000); add decimal comment if > 9 | +| `id` | Yes | Always 4-digit hex format (0x0000), lowercase digits; add decimal comment if > 9 | | `args` | If has params | Object with typed fields | | `response` | If expects response | Has own `id` and `args` | | `encodeMissingFieldsBehavior` | Controls the behavior of missing fields when encoding the struct | Set to `skip` if some fields are optional, omit if all fields are mandatory | @@ -313,7 +318,8 @@ attr3: { ... }, lockDoor: { id: 0x0000, ... }, // Mandatory // --- Server to Client Commands --- -operationEventNotification: { // Optional +// Optional +operationEventNotification: { id: 0x0020, // 32 direction: Cluster.DIRECTION_SERVER_TO_CLIENT, ... From b1fd714a7672a1f15364c7b08f3ad80e4c1dc374 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 5/6] ci: Run the Prettier check in the Lint workflow The format:check script was added alongside Prettier but nothing ran it, so the formatting was advisory: one commit formatted by hand and the next npm run format:write is a hundred-file diff again, which undoes the point of the reformat. --- .github/workflows/lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6759d2c2..d94386dd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,3 +33,5 @@ jobs: - run: npm ci - run: npm run lint + + - run: npm run format:check From 5b269517ea1250229b74ed0dcf6864a4937ca0c8 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 09:30:38 +0200 Subject: [PATCH 6/6] refactor: Use the node: prefix for core imports in lib The three EventEmitter imports now read node:events. The prefix cannot be shadowed by a local events.js and says outright that the module is built in. Only lib/ here; the test files are converted alongside the node:test migration they belong to. --- lib/Cluster.js | 2 +- lib/Endpoint.js | 2 +- lib/Node.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Cluster.js b/lib/Cluster.js index 2d8a52e1..371c65d2 100644 --- a/lib/Cluster.js +++ b/lib/Cluster.js @@ -1,6 +1,6 @@ 'use strict'; -const EventEmitter = require('events'); +const EventEmitter = require('node:events'); let { debug } = require('./util'); const { getLogId, ZCLError } = require('./util'); diff --git a/lib/Endpoint.js b/lib/Endpoint.js index 94e4faf2..fa273bae 100644 --- a/lib/Endpoint.js +++ b/lib/Endpoint.js @@ -1,6 +1,6 @@ 'use strict'; -const EventEmitter = require('events'); +const EventEmitter = require('node:events'); const Cluster = require('./Cluster'); const BoundCluster = require('./BoundCluster'); diff --git a/lib/Node.js b/lib/Node.js index 002ff4ac..8029d4c6 100644 --- a/lib/Node.js +++ b/lib/Node.js @@ -1,6 +1,6 @@ 'use strict'; -const EventEmitter = require('events'); +const EventEmitter = require('node:events'); let { debug } = require('./util'); const { getLogId } = require('./util');