zigbee-clusters 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,451 @@
1
+ # AGENTS.md
2
+
3
+ This file provides guidance to AI agents when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm test # Run all tests (mocha)
9
+ npm run lint # ESLint (extends athom config)
10
+ npm run build # Generate JSDoc documentation
11
+ ```
12
+
13
+ Run single test file:
14
+ ```bash
15
+ npx mocha test/onOff.js
16
+ ```
17
+
18
+ ## Architecture
19
+
20
+ Zigbee Cluster Library (ZCL) implementation for Node.js, designed for Homey's Zigbee stack.
21
+
22
+ ### Core Classes
23
+
24
+ - **ZCLNode** (`lib/Node.js`) - Entry point. Wraps Homey's ZigBeeNode, manages endpoints, routes incoming frames.
25
+ - **Endpoint** (`lib/Endpoint.js`) - Represents device endpoint. Contains `clusters` (client) and `bindings` (server). Routes frames to appropriate cluster.
26
+ - **Cluster** (`lib/Cluster.js`) - Base class for all clusters. Handles frame parsing, command execution, attribute operations. Commands auto-generate methods via `_addPrototypeMethods`.
27
+ - **BoundCluster** (`lib/BoundCluster.js`) - Server-side cluster for receiving commands from remote nodes.
28
+
29
+ ### Data Flow
30
+
31
+ ```
32
+ ZCLNode.handleFrame() → Endpoint.handleFrame() → Cluster/BoundCluster.handleFrame()
33
+ Cluster.sendCommand() → Endpoint.sendFrame() → ZCLNode.sendFrame()
34
+ ```
35
+
36
+ ### Cluster Implementation Pattern
37
+
38
+ Each cluster in `lib/clusters/` follows:
39
+ 1. Define `ATTRIBUTES` object with `{id, type}` using `ZCLDataTypes`
40
+ 2. Define `COMMANDS` object with `{id, args?}`
41
+ 3. Extend `Cluster` with static getters: `ID`, `NAME`, `ATTRIBUTES`, `COMMANDS`
42
+ 4. Call `Cluster.addCluster(MyCluster)` to register
43
+
44
+ Example: `lib/clusters/onOff.js`
45
+
46
+ ### Key Types
47
+
48
+ - `ZCLDataTypes` - Primitive types (uint8, bool, enum8, etc.) from `@athombv/data-types`
49
+ - `ZCLStruct` - Composite types for command arguments
50
+ - `CLUSTER` constant - Maps cluster names to `{ID, NAME, ATTRIBUTES, COMMANDS}`
51
+
52
+ ### Cluster ID References
53
+
54
+ Prefer `Cluster.ID` over hardcoded numbers:
55
+ ```javascript
56
+ // Good
57
+ const OnOffCluster = require('../lib/clusters/onOff');
58
+ inputClusters: [OnOffCluster.ID]
59
+
60
+ // Avoid
61
+ inputClusters: [6]
62
+ inputClusters: [0x0006]
63
+ ```
64
+
65
+ ### Test Pattern
66
+
67
+ Tests use mock nodes from `test/util/mockNode.js`:
68
+
69
+ **Single node with loopback** (command → same node's BoundCluster):
70
+ ```javascript
71
+ const { createMockNode } = require('./util');
72
+ const OnOffCluster = require('../lib/clusters/onOff');
73
+
74
+ const node = createMockNode({
75
+ loopback: true,
76
+ endpoints: [{ endpointId: 1, inputClusters: [OnOffCluster.ID] }],
77
+ });
78
+
79
+ node.endpoints[1].bind('onOff', new (class extends BoundCluster {
80
+ async setOn() { /* handle command */ }
81
+ })());
82
+
83
+ await node.endpoints[1].clusters.onOff.setOn(); // loops back to BoundCluster
84
+ ```
85
+
86
+ **Connected node pair** (node A sends → node B receives):
87
+ ```javascript
88
+ const { createConnectedNodePair } = require('./util');
89
+
90
+ const [sender, receiver] = createConnectedNodePair(
91
+ { endpoints: [{ endpointId: 1, inputClusters: [6] }] },
92
+ { endpoints: [{ endpointId: 1, inputClusters: [6] }] },
93
+ );
94
+
95
+ receiver.endpoints[1].bind('onOff', new BoundCluster());
96
+ await sender.endpoints[1].clusters.onOff.toggle();
97
+ ```
98
+
99
+ **Server-to-client notifications** (device → controller) still require manual frames:
100
+ ```javascript
101
+ const { ZCLStandardHeader } = require('../lib/zclFrames');
102
+
103
+ node.endpoints[1].clusters.iasZone.onZoneStatusChangeNotification = data => { ... };
104
+
105
+ const frame = new ZCLStandardHeader();
106
+ frame.cmdId = IASZoneCluster.COMMANDS.zoneStatusChangeNotification.id;
107
+ frame.frameControl.directionToClient = true;
108
+ frame.frameControl.clusterSpecific = true;
109
+ frame.data = Buffer.from([...]);
110
+
111
+ node.handleFrame(1, IASZoneCluster.ID, frame.toBuffer(), {});
112
+ ```
113
+
114
+ **Preset mock devices** for common sensor types:
115
+ ```javascript
116
+ const { MOCK_DEVICES } = require('./util');
117
+
118
+ const sensor = MOCK_DEVICES.motionSensor();
119
+ const boundCluster = sensor.endpoints[1].bindings.iasZone;
120
+ ```
121
+
122
+ ## Key Files
123
+
124
+ - `index.js` - Public exports
125
+ - `lib/clusters/index.js` - All cluster exports + `CLUSTER` constant
126
+ - `lib/zclTypes.js` - ZCL data types
127
+ - `lib/zclFrames.js` - ZCL frame parsing/serialization
128
+
129
+ ---
130
+
131
+ ## Adding/Updating Cluster Definitions
132
+
133
+ Reusable guide for adding new clusters or updating existing cluster definitions.
134
+
135
+ ### Source Reference
136
+
137
+ - **Spec PDF**: `docs/zigbee-cluster-specification-r8.pdf`
138
+ - **Extract text**: `pdftotext docs/zigbee-cluster-specification-r8.pdf -`
139
+ - **Cluster files**: `lib/clusters/*.js`
140
+
141
+ ---
142
+
143
+ ### File Structure Template
144
+
145
+ ```javascript
146
+ 'use strict';
147
+
148
+ const Cluster = require('../Cluster');
149
+ const { ZCLDataTypes } = require('../zclTypes');
150
+
151
+ // Reusable enum definitions (if needed)
152
+ const EXAMPLE_ENUM = ZCLDataTypes.enum8({
153
+ value1: 0,
154
+ value2: 1,
155
+ });
156
+
157
+ // ============================================================================
158
+ // Server Attributes
159
+ // ============================================================================
160
+ const ATTRIBUTES = {
161
+ // Section Name (0x0000 - 0x000F)
162
+
163
+ // Description from spec, copied 1-on-1.
164
+ // Multi-line if needed, wrapped at 100 chars.
165
+ attrName: { id: 0x0000, type: ZCLDataTypes.uint8 }, // Mandatory
166
+ };
167
+
168
+ // ============================================================================
169
+ // Commands
170
+ // ============================================================================
171
+ const COMMANDS = {
172
+ // --- Client to Server Commands ---
173
+
174
+ // Description from spec.
175
+ commandName: { // Mandatory
176
+ id: 0x0000,
177
+ args: {
178
+ argName: ZCLDataTypes.uint8,
179
+ },
180
+ response: {
181
+ id: 0x0000,
182
+ args: { status: ZCLDataTypes.uint8 },
183
+ },
184
+ },
185
+
186
+ // --- Server to Client Commands ---
187
+
188
+ // Description from spec.
189
+ notificationName: { // Optional
190
+ id: 0x0020, // 32
191
+ direction: Cluster.DIRECTION_SERVER_TO_CLIENT,
192
+ args: {
193
+ argName: ZCLDataTypes.uint8,
194
+ },
195
+ },
196
+ };
197
+
198
+ class ExampleCluster extends Cluster {
199
+ static get ID() {
200
+ return 0x0000; // Add decimal comment if > 9, e.g.: return 0x0101; // 257
201
+ }
202
+
203
+ static get NAME() {
204
+ return 'example';
205
+ }
206
+
207
+ static get ATTRIBUTES() {
208
+ return ATTRIBUTES;
209
+ }
210
+
211
+ static get COMMANDS() {
212
+ return COMMANDS;
213
+ }
214
+ }
215
+
216
+ Cluster.addCluster(ExampleCluster);
217
+ module.exports = ExampleCluster;
218
+ ```
219
+
220
+ ---
221
+
222
+ ### Comment Format Rules
223
+
224
+ 1. **Description placement**: ABOVE the attribute/command
225
+ 2. **M/O marker placement**:
226
+ - Single-line: at END of line (`attrName: { ... }, // Mandatory`)
227
+ - Multi-line: on OPENING brace (`attrName: { // Mandatory`)
228
+ - NEVER on closing brace
229
+ 3. **Copy exactly**: Text from spec, 1-on-1
230
+ 4. **Skip if >5 sentences**: Skip and only refer to section in spec
231
+ 5. **Line wrap**: Respect 100 char limit (ESLint)
232
+ 6. **M/O source for attrs**: Server-side column in spec table
233
+ 7. **M/O source for cmds**:
234
+ - Server "receives" → server-side M/O
235
+ - Server "generates" → client-side M/O
236
+ 8. **Conditional (M*)**: Use `// Conditional¹` with footnote for attrs/cmds marked "M*" in spec
237
+ - Reason on new line below: `// ¹ Reason here`
238
+ - Shared reasons use same footnote number
239
+
240
+ #### Example
241
+
242
+ ```javascript
243
+ currentLevel: { id: 0x0000, type: ZCLDataTypes.uint8 }, // Mandatory
244
+ pirOccupiedToUnoccupiedDelay: { id: 0x0010, type: ZCLDataTypes.uint16 }, // 16, Conditional¹
245
+ pirUnoccupiedToOccupiedDelay: { id: 0x0011, type: ZCLDataTypes.uint16 }, // 17, Conditional¹
246
+ // ¹ PIR sensor type supported
247
+ ```
248
+
249
+ ---
250
+
251
+ ### Attribute Definition Rules
252
+
253
+ | Field | Format | Notes |
254
+ |-------|--------|-------|
255
+ | `id` | Hex (`0x0000`) | Always 4-digit format (0x0000); add decimal comment if > 9 |
256
+ | `type` | `ZCLDataTypes.*` | See type reference below |
257
+ | M/O | Inline comment | `// Mandatory`, `// Optional`, or `// Conditional` |
258
+
259
+ #### Hex with Decimal Comments
260
+
261
+ For hex values > 9 (where hex differs from decimal), add decimal in comment:
262
+ ```javascript
263
+ id: 0x000A, // 10
264
+ id: 0x0010, // 16
265
+ id: 0x0100, // 256
266
+ ```
267
+
268
+ For values 0-9, no decimal comment needed:
269
+ ```javascript
270
+ id: 0x0000,
271
+ id: 0x0009,
272
+ ```
273
+
274
+ For multi-line definitions, decimal goes on the `id:` line, M/O on opening brace:
275
+ ```javascript
276
+ operatingMode: { // Optional
277
+ id: 0x0025, // 37
278
+ type: ZCLDataTypes.enum8({
279
+ normal: 0,
280
+ vacation: 1,
281
+ }),
282
+ },
283
+ ```
284
+
285
+ #### Section Comments
286
+
287
+ Group attrs by function with section headers:
288
+ ```javascript
289
+ // Section Name (0x0000 - 0x000F)
290
+ attr1: { ... },
291
+ attr2: { ... },
292
+
293
+ // Another Section (0x0010 - 0x001F)
294
+ attr3: { ... },
295
+ ```
296
+
297
+ ---
298
+
299
+ ### Command Definition Rules
300
+
301
+ | Field | Required | Notes |
302
+ |-------|----------|-------|
303
+ | `id` | Yes | Always 4-digit hex format (0x0000); add decimal comment if > 9 |
304
+ | `args` | If has params | Object with typed fields |
305
+ | `response` | If expects response | Has own `id` and `args` |
306
+ | `direction` | For server→client | `Cluster.DIRECTION_SERVER_TO_CLIENT` |
307
+
308
+ #### Command Sections
309
+
310
+ ```javascript
311
+ // --- Client to Server Commands ---
312
+ lockDoor: { id: 0x0000, ... }, // Mandatory
313
+
314
+ // --- Server to Client Commands ---
315
+ operationEventNotification: { // Optional
316
+ id: 0x0020, // 32
317
+ direction: Cluster.DIRECTION_SERVER_TO_CLIENT,
318
+ ...
319
+ },
320
+ ```
321
+
322
+ #### Command Direction Rules
323
+
324
+ Focus on **client→server commands** with inline `response:` when applicable:
325
+ ```javascript
326
+ lockDoor: {
327
+ id: 0x0000,
328
+ args: { pinCode: ZCLDataTypes.octstr },
329
+ response: {
330
+ id: 0x0000,
331
+ args: { status: ZCLDataTypes.uint8 },
332
+ },
333
+ },
334
+ ```
335
+
336
+ **Server→client commands** (events/notifications) should be evaluated per case:
337
+ - Implement if commonly needed (e.g., `operationEventNotification` for door locks)
338
+ - Skip obscure or rarely-used notifications unless specifically requested
339
+ - These require `direction: Cluster.DIRECTION_SERVER_TO_CLIENT`
340
+
341
+ ---
342
+
343
+ ### ZCLDataTypes Reference
344
+
345
+ #### Primitives
346
+ - `bool`, `uint8`, `uint16`, `uint24`, `uint32`, `uint48`, `uint64`
347
+ - `int8`, `int16`, `int24`, `int32`
348
+ - `string`, `octstr`
349
+
350
+ #### Enums
351
+ ```javascript
352
+ ZCLDataTypes.enum8({
353
+ valueName: 0,
354
+ anotherValue: 1,
355
+ })
356
+ ```
357
+
358
+ #### Bitmaps
359
+ ```javascript
360
+ ZCLDataTypes.map8('bit0', 'bit1', 'bit2')
361
+ ZCLDataTypes.map16('bit0', 'bit1', ...)
362
+ ZCLDataTypes.map64(...)
363
+ ```
364
+
365
+ #### Arrays
366
+ ```javascript
367
+ ZCLDataTypes.Array0(ZCLDataTypes.uint8)
368
+ ZCLDataTypes.Array8(...)
369
+ ```
370
+
371
+ #### Reusable Enums
372
+ Define at module level if used multiple times:
373
+ ```javascript
374
+ const USER_STATUS_ENUM = ZCLDataTypes.enum8({
375
+ available: 0,
376
+ occupied: 1,
377
+ });
378
+
379
+ // Then use in commands:
380
+ args: { userStatus: USER_STATUS_ENUM }
381
+ ```
382
+
383
+ #### Reusable Bitmaps
384
+ Same pattern for bitmaps used multiple times:
385
+ ```javascript
386
+ const ALARM_MASK = ZCLDataTypes.map8(
387
+ 'generalHardwareFault',
388
+ 'generalSoftwareFault',
389
+ 'reserved2',
390
+ 'reserved3',
391
+ );
392
+
393
+ // Then use in attributes/commands:
394
+ alarmMask: { id: 0x0010, type: ALARM_MASK },
395
+ ```
396
+
397
+ ---
398
+
399
+ ### Workflow: Adding/Updating a Cluster
400
+
401
+ #### 1. Extract Spec Section
402
+ ```bash
403
+ pdftotext docs/zigbee-cluster-specification-r8.pdf - | grep -A 500 "X.Y.Z Cluster Name"
404
+ ```
405
+
406
+ #### 2. Identify Elements
407
+ From spec tables, extract:
408
+ - Cluster ID and Name
409
+ - All attributes (ID, name, type, M/O)
410
+ - All commands (ID, name, args, direction, M/O)
411
+ - Descriptions (≤5 sentences)
412
+
413
+ #### 3. Create/Update File
414
+ - Use template above
415
+ - Follow naming: `lib/clusters/clusterName.js`
416
+ - Export in `lib/clusters/index.js`
417
+
418
+ #### 4. Validate
419
+ ```bash
420
+ npm run lint
421
+ npm test
422
+ npm run build
423
+ ```
424
+
425
+ ---
426
+
427
+ ### Checklist for Each Cluster
428
+
429
+ - [ ] Cluster ID correct (hex in class, matches spec)
430
+ - [ ] Cluster NAME matches spec (camelCase)
431
+ - [ ] All mandatory attrs present with `// Mandatory`
432
+ - [ ] All mandatory cmds present with `// Mandatory`
433
+ - [ ] Conditional attrs/cmds marked `// Conditional` (M* in spec)
434
+ - [ ] Optional attrs/cmds marked `// Optional`
435
+ - [ ] Descriptions copied from spec (≤5 sentences)
436
+ - [ ] Section comments group related attrs
437
+ - [ ] Client/server cmd sections separated
438
+ - [ ] Server→client cmds have `direction` field
439
+ - [ ] Responses defined where applicable
440
+ - [ ] Reusable enums/bitmaps extracted if used 2+ times
441
+ - [ ] Hex IDs used consistently (with decimal comments if > 9)
442
+ - [ ] Lint passes
443
+ - [ ] Tests pass
444
+
445
+ ---
446
+
447
+ ### Reference Examples
448
+
449
+ - **Best documented**: `lib/clusters/doorLock.js`
450
+ - **Simple attrs only**: `lib/clusters/metering.js`
451
+ - **Color/enum heavy**: `lib/clusters/colorControl.js`
package/README.md CHANGED
@@ -301,6 +301,28 @@ zclNode.endpoints[1].clusters["scenes"].ikeaSceneMove({ mode: 0, transitionTime:
301
301
 
302
302
  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).
303
303
 
304
+ ## TypeScript Types
305
+
306
+ This project includes auto-generated TypeScript definitions (`index.d.ts`) for all clusters, attributes, and commands.
307
+
308
+ ### Manual Generation
309
+
310
+ To regenerate TypeScript types after modifying cluster definitions:
311
+
312
+ ```bash
313
+ npm run generate-types
314
+ ```
315
+
316
+ This runs `scripts/generate-types.js` which loads all cluster modules and generates typed interfaces.
317
+
318
+ ### Automatic Generation (GitHub Actions)
319
+
320
+ TypeScript types are automatically regenerated when changes are pushed to the `develop` branch that affect:
321
+ - `lib/clusters/**/*.js` - cluster definitions
322
+ - `scripts/generate-types.js` - the generator script
323
+
324
+ The workflow commits updated types back to `develop` if changes are detected.
325
+
304
326
  ## Contributing
305
327
 
306
328
  Great if you'd like to contribute to this project, a few things to take note of before submitting a PR: