zigbee-clusters 1.7.3 → 2.0.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Zigbee Cluster Library for Node.js
2
2
 
3
3
  This project implements the Zigbee Cluster Library (ZCL) based on the Zigbee Cluster Library
4
- Specification ([documentation](https://etc.athom.com/zigbee_cluster_specification.pdf)). It is designed to work with Homey's Zigbee stack and can be used in Homey Apps to implement drivers for Zigbee devices that work with Homey.
4
+ Specification ([documentation](https://etc.athom.com/zigbee_cluster_specification.pdf)). It is designed to work with Homey's Zigbee stack and can be used in Homey Apps to implement drivers for Zigbee devices that work with Homey.
5
5
 
6
6
  Note: if you are looking for the best way to implement Zigbee drivers for Homey take a look at [node-homey-zigbeedriver](https://github.com/athombv/node-homey-zigbeedriver).
7
7
 
@@ -13,6 +13,16 @@ Make sure to take a look at the API documentation: [https://athombv.github.io/no
13
13
 
14
14
  `$ npm install --save zigbee-clusters`
15
15
 
16
+ ## Breaking changes
17
+
18
+ v2.0.0
19
+
20
+ - Changed `Cluster.readAttributes` signature, attributes must now be specified as an array of strings.
21
+
22
+ ```js
23
+ zclNode.endpoints[1].clusters.basic.readAttributes(["modelId", "manufacturerName"]);
24
+ ```
25
+
16
26
  ## Release
17
27
 
18
28
  Merge to production and include `#patch`, `#minor` or `#major` in the PR/commit message. Or merge to production and run the "Deploy" workflow and provide a version bump parameter.
@@ -27,13 +37,16 @@ It is important to understand the structure of a Zigbee node:
27
37
  [![](https://mermaid.ink/img/eyJjb2RlIjoiZ3JhcGggVERcbiAgQVtOb2RlXSAtLT4gQihFbmRwb2ludCAxKVxuICBBIC0tPiBEKEVuZHBvaW50IC4uLilcbiAgQiAtLT4gRShDbHVzdGVyIE9uT2ZmKVxuICBCIC0tPiBGKENsdXN0ZXIgTGV2ZWxDb250cm9sKVxuICBCIC0tPiBHKENsdXN0ZXIgLi4uKVxuICBFIC0tPiBIKENvbW1hbmQgJ3RvZ2dsZScpXG4gIEUgLS0-IEkoQ29tbWFuZCAnc2V0T24nKVxuICBFIC0tPiBKKEF0dHJpYnV0ZSAnb25PZmYnKSIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgQVtOb2RlXSAtLT4gQihFbmRwb2ludCAxKVxuICBBIC0tPiBEKEVuZHBvaW50IC4uLilcbiAgQiAtLT4gRShDbHVzdGVyIE9uT2ZmKVxuICBCIC0tPiBGKENsdXN0ZXIgTGV2ZWxDb250cm9sKVxuICBCIC0tPiBHKENsdXN0ZXIgLi4uKVxuICBFIC0tPiBIKENvbW1hbmQgJ3RvZ2dsZScpXG4gIEUgLS0-IEkoQ29tbWFuZCAnc2V0T24nKVxuICBFIC0tPiBKKEF0dHJpYnV0ZSAnb25PZmYnKSIsIm1lcm1haWQiOnsidGhlbWUiOiJkZWZhdWx0In0sInVwZGF0ZUVkaXRvciI6ZmFsc2V9)
28
38
 
29
39
  ### Client/Server clusters
40
+
30
41
  A cluster can be implemented in two ways:
31
- * As server
32
- * As client
42
+
43
+ - As server
44
+ - As client
33
45
 
34
46
  From the Zigbee Cluster Library Specification "Typically, the entity that stores the attributes of a cluster is referred to as the server of that cluster and an entity that affects or manipulates those attributes is referred to as the client of that cluster." More information on this can be found in the Zigbee Cluster Library Specification [section 2.2.2.](https://etc.athom.com/zigbee_cluster_specification.pdf).
35
47
 
36
48
  ### Bindings and bound clusters
49
+
37
50
  The concept of server/client is important for the following reason. Nodes can be receivers of commands (i.e. servers), or senders of commands (i.e. clients), and sometimes both. An example on how to send a command to a node can be found [below](#basic-communication-with-node). Receiving commands from a node requires a binding to be made from the controller to the cluster on the node, and the implementation of a `BoundCluster` (i.e. server cluster) to receive and handle the incoming commands. For an example on implementing a `BoundCluster` see [below](#implementing-a-bound-cluster).
38
51
 
39
52
  ## Usage
@@ -43,68 +56,79 @@ In order to communicate with a Zigbee node retrieve a `node` instance from `Mana
43
56
  ### Basic communication with node
44
57
 
45
58
  ## Sending a command
59
+
46
60
  `/drivers/my-driver/device.js`
61
+
47
62
  ```js
48
- const Homey = require('homey');
49
- const { ZCLNode, CLUSTER } = require('zigbee-clusters');
63
+ const Homey = require("homey");
64
+ const { ZCLNode, CLUSTER } = require("zigbee-clusters");
50
65
 
51
66
  class MyDevice extends Homey.Device {
52
- onInit() {
53
- // Get ZigBeeNode instance from ManagerZigBee
54
- this.homey.zigbee.getNode(this)
55
- .then(async node => {
56
- // Create ZCLNode instance
57
- const zclNode = new ZCLNode(node);
58
-
59
- // Send toggle command to onOff cluster on endpoint 1
60
- await zclNode.endpoints[1].clusters[CLUSTER.ON_OFF.NAME].toggle();
61
-
62
- // Send moveToLevel command to levelControl cluster on endpoint 1 and don't wait for
63
- // the default response confirmation.
64
- await zclNode.endpoints[1].clusters[CLUSTER.LEVEL_CONTROL.NAME].moveToLevel({
65
- level: 100,
66
- transitionTime: 2000
67
- }, {
68
- // This is an optional flag that disables waiting for a default response from the
69
- // receiving node as a confirmation that the command is received and executed.
70
- // You should only use this flag if the device does not follow the
71
- // Zigbee specification and refuses to send a default response.
72
- waitForResponse: true,
73
- });
74
- });
75
- }
67
+ onInit() {
68
+ // Get ZigBeeNode instance from ManagerZigBee
69
+ this.homey.zigbee.getNode(this).then(async (node) => {
70
+ // Create ZCLNode instance
71
+ const zclNode = new ZCLNode(node);
72
+
73
+ // Send toggle command to onOff cluster on endpoint 1
74
+ await zclNode.endpoints[1].clusters[CLUSTER.ON_OFF.NAME].toggle();
75
+
76
+ // Send moveToLevel command to levelControl cluster on endpoint 1 and don't wait for
77
+ // the default response confirmation.
78
+ await zclNode.endpoints[1].clusters[CLUSTER.LEVEL_CONTROL.NAME].moveToLevel(
79
+ {
80
+ level: 100,
81
+ transitionTime: 2000,
82
+ },
83
+ {
84
+ // This is an optional flag that disables waiting for a default response from the
85
+ // receiving node as a confirmation that the command is received and executed.
86
+ // You should only use this flag if the device does not follow the
87
+ // Zigbee specification and refuses to send a default response.
88
+ waitForResponse: true,
89
+
90
+ // This is an optional property that allows for adjusting the response
91
+ // timeout (25000ms) before the command is considered rejected.
92
+ timeout: 10000,
93
+ }
94
+ );
95
+ });
96
+ }
76
97
  }
77
98
  ```
78
99
 
79
100
  ## Receiving an attribute report
80
101
 
81
102
  `/drivers/my-driver/device.js`
103
+
82
104
  ```js
83
- const Homey = require('homey');
84
- const { ZCLNode, CLUSTER } = require('zigbee-clusters');
105
+ const Homey = require("homey");
106
+ const { ZCLNode, CLUSTER } = require("zigbee-clusters");
85
107
 
86
108
  class MyDevice extends Homey.Device {
87
109
  onInit() {
88
110
  // Get ZigBeeNode instance from ManagerZigBee
89
- this.homey.zigbee.getNode(this)
90
- .then(async node => {
91
- // Create ZCLNode instance
92
- const zclNode = new ZCLNode(node);
93
-
94
- // Configure reporting
95
- await zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].configureReporting({
96
- currentSaturation: {
97
- minInterval: 0,
98
- maxInterval: 300,
99
- minChange: 1
100
- }
101
- });
102
-
103
- // And listen for incoming attribute reports by binding a listener on the cluster
104
- zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].on('attr.currentSaturation', (currentSaturation) => {
105
- // handle reported attribute value
106
- });
111
+ this.homey.zigbee.getNode(this).then(async (node) => {
112
+ // Create ZCLNode instance
113
+ const zclNode = new ZCLNode(node);
114
+
115
+ // Configure reporting
116
+ await zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].configureReporting({
117
+ currentSaturation: {
118
+ minInterval: 0,
119
+ maxInterval: 300,
120
+ minChange: 1,
121
+ },
107
122
  });
123
+
124
+ // And listen for incoming attribute reports by binding a listener on the cluster
125
+ zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].on(
126
+ "attr.currentSaturation",
127
+ (currentSaturation) => {
128
+ // handle reported attribute value
129
+ }
130
+ );
131
+ });
108
132
  }
109
133
  }
110
134
  ```
@@ -112,11 +136,12 @@ class MyDevice extends Homey.Device {
112
136
  ### Implementing a cluster
113
137
 
114
138
  It is very easy to add support for a new cluster or add commands and/or attributes to an existing
115
- cluster. All implemented clusters are listed in [lib/clusters/index.js](https://github.com/athombv/node-zigbee-clusters/blob/production/lib/clusters/index.js). It also exports a constant `CLUSTER` object for easy reference to a specific cluster name and/or id (e.g. `CLUSTER.WINDOW_COVERING` -> `{NAME: "windowCovering", ID: 258})`.
139
+ cluster. All implemented clusters are listed in [lib/clusters/index.js](https://github.com/athombv/node-zigbee-clusters/blob/production/lib/clusters/index.js). It also exports a constant `CLUSTER` object for easy reference to a specific cluster name and/or id (e.g. `CLUSTER.WINDOW_COVERING` -> `{NAME: "windowCovering", ID: 258})`.
116
140
 
117
141
  This example shows in a simplified way how the OnOff cluster is implemented ([actual implementation](https://github.com/athombv/node-zigbee-clusters/blob/production/lib/clusters/onOff.js)). All the information with regard to the ids, names, available attributes and commands can be found in the Zigbee Cluster Library Specification [section 3.8.](https://etc.athom.com/zigbee_cluster_specification.pdf):
118
142
 
119
143
  `zigbee-clusters/lib/clusters/onOff.js`
144
+
120
145
  ```js
121
146
  // Define the cluster attributes
122
147
  const ATTRIBUTES = {
@@ -140,13 +165,12 @@ const COMMANDS = {
140
165
 
141
166
  // Implement the OnOff cluster by extending `Cluster`
142
167
  class OnOffCluster extends Cluster {
143
-
144
168
  static get ID() {
145
169
  return 6; // The cluster id
146
170
  }
147
171
 
148
172
  static get NAME() {
149
- return 'onOff'; // The cluster name
173
+ return "onOff"; // The cluster name
150
174
  }
151
175
 
152
176
  static get ATTRIBUTES() {
@@ -156,32 +180,32 @@ class OnOffCluster extends Cluster {
156
180
  static get COMMANDS() {
157
181
  return COMMANDS; // Returns the defined commands
158
182
  }
159
-
160
183
  }
161
184
 
162
185
  // Add the cluster to the clusters that will be available on the `ZCLNode`
163
186
  Cluster.addCluster(OnOffCluster);
164
187
 
165
188
  module.exports = OnOffCluster;
166
-
167
189
  ```
168
190
 
169
191
  After a cluster is implemented it can be used on a `ZCLNode` instance like this:
192
+
170
193
  ```
171
194
  await zclNode.endpoints[1].clusters[CLUSTER.ON_OFF.NAME].toggle();
172
195
  ```
173
- Note that `CLUSTER.ON_OFF.NAME` is just a string that refers to `onOff` in `zigbee-clusters/lib/clusters/onOff.js`
174
196
 
197
+ Note that `CLUSTER.ON_OFF.NAME` is just a string that refers to `onOff` in `zigbee-clusters/lib/clusters/onOff.js`
175
198
 
176
199
  ### Implementing a bound cluster
200
+
177
201
  Zigbee nodes can send commands to Homey via bound clusters. This requires a binding to be created on a specific endpoint and cluster. Next, a `BoundCluster` implementation must be registered with the `ZCLNode` which implements handlers for the incomming commands:
178
202
 
179
203
  `/lib/LevelControlBoundCluster.js`
204
+
180
205
  ```js
181
- const { BoundCluster } = require('zigbee-clusters');
206
+ const { BoundCluster } = require("zigbee-clusters");
182
207
 
183
208
  class LevelControlBoundCluster extends BoundCluster {
184
-
185
209
  constructor({ onMove }) {
186
210
  super();
187
211
  this._onMove = onMove;
@@ -197,30 +221,34 @@ class LevelControlBoundCluster extends BoundCluster {
197
221
  }
198
222
 
199
223
  module.exports = LevelControlBoundCluster;
200
-
201
224
  ```
225
+
202
226
  `/drivers/my-driver/device.js`
203
227
 
204
228
  ```js
205
- const LevelControlBoundCluster = require('../../lib/LevelControlBoundCluster');
229
+ const LevelControlBoundCluster = require("../../lib/LevelControlBoundCluster");
206
230
 
207
231
  // Register the `BoundCluster` implementation with the `ZCLNode`
208
- zclNode.endpoints[1].bind(CLUSTER.LEVEL_CONTROL.NAME, new LevelControlBoundCluster({
209
- onMove: (payload) => {
210
- // Do something with the received payload
211
- },
212
- }));
232
+ zclNode.endpoints[1].bind(
233
+ CLUSTER.LEVEL_CONTROL.NAME,
234
+ new LevelControlBoundCluster({
235
+ onMove: (payload) => {
236
+ // Do something with the received payload
237
+ },
238
+ })
239
+ );
213
240
  ```
214
241
 
215
242
  ### Implementing a custom cluster
243
+
216
244
  There are cases where it is required to implement a custom cluster, for example to handle manufacturer specific cluster implementations. Often these manufacturer specific cluster implementations are extensions of existing clusters. An example is the `IkeaSpecificSceneCluster` ([complete implementation](https://github.com/athombv/com.ikea.tradfri-example/tree/master/lib/IkeaSpecificSceneCluster.js)):
217
245
 
218
246
  `lib/IkeaSpecificSceneCluster.js`
247
+
219
248
  ```js
220
- const { ScenesCluster, ZCLDataTypes } = require('zigbee-clusters');
249
+ const { ScenesCluster, ZCLDataTypes } = require("zigbee-clusters");
221
250
 
222
251
  class IkeaSpecificSceneCluster extends ScenesCluster {
223
-
224
252
  // Here we override the `COMMANDS` getter from the `ScenesClusters` by
225
253
  // extending it with the custom command we'd like to implement `ikeaSceneMove`.
226
254
  static get COMMANDS() {
@@ -228,7 +256,7 @@ class IkeaSpecificSceneCluster extends ScenesCluster {
228
256
  ...super.COMMANDS,
229
257
  ikeaSceneMove: {
230
258
  id: 0x08,
231
- manufacturerId: 0x117C,
259
+ manufacturerId: 0x117c,
232
260
  args: {
233
261
  mode: ZCLDataTypes.enum8({
234
262
  up: 0,
@@ -252,32 +280,30 @@ class IkeaSpecificSceneCluster extends ScenesCluster {
252
280
  },
253
281
  };
254
282
  }
255
-
256
283
  }
257
284
 
258
285
  module.exports = IkeaSpecificSceneCluster;
259
-
260
-
261
286
  ```
262
287
 
263
288
  `/drivers/my-driver/device.js`
289
+
264
290
  ```js
265
- const IkeaSpecificSceneCluster = require('../../lib/IkeaSpecificSceneCluster');
291
+ const IkeaSpecificSceneCluster = require("../../lib/IkeaSpecificSceneCluster");
266
292
 
267
293
  // Important: we have created a new `Cluster` instance which needs to be added before
268
294
  // it becomes available on any `ZCLNode` instance.
269
295
  Cluster.addCluster(IkeaSpecificSceneCluster);
270
296
 
271
297
  // Example invocation of custom cluster command
272
- zclNode.endpoints[1].clusters['scenes'].ikeaSceneMove({mode: 0, transitionTime: 10});
273
-
298
+ zclNode.endpoints[1].clusters["scenes"].ikeaSceneMove({ mode: 0, transitionTime: 10 });
274
299
  ```
275
300
 
276
301
  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).
277
302
 
278
303
  ## Contributing
304
+
279
305
  Great if you'd like to contribute to this project, a few things to take note of before submitting a PR:
280
- * This project enforces ESLint, validate by running `npm run lint`.
281
- * This project implements a basic test framework based on mocha, see [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory.
282
- * This project uses several [GitHub Action workflows](https://github.com/athombv/node-zigbee-clusters/blob/production/.github/workflows) (e.g. ESLint, running test and versioning/publishing).
283
306
 
307
+ - This project enforces ESLint, validate by running `npm run lint`.
308
+ - This project implements a basic test framework based on mocha, see [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory.
309
+ - This project uses several [GitHub Action workflows](https://github.com/athombv/node-zigbee-clusters/blob/production/.github/workflows) (e.g. ESLint, running test and versioning/publishing).
package/index.d.ts CHANGED
@@ -11,8 +11,13 @@ type ConstructorOptions = {
11
11
  sendFrame: (endpointId: number, clusterId: number, frame: Buffer) => Promise<void>;
12
12
  };
13
13
  type ZCLNodeCluster = EventEmitter & {
14
- readAttributes: (...attributeNames: string[]) => Promise<{ [attributeName: string]: any }>;
15
- writeAttributes: (attributes: { [attributeName: string]: any }) => Promise<{ [attributeName: string]: { id: number, status: 'SUCCESS' | 'FAILURE' } }>;
14
+ readAttributes: (
15
+ attributeNames: string[],
16
+ opts: { timeout: number }
17
+ ) => Promise<{ [attributeName: string]: any }>;
18
+ writeAttributes: (attributes: {
19
+ [attributeName: string]: any;
20
+ }) => Promise<{ [attributeName: string]: { id: number; status: "SUCCESS" | "FAILURE" } }>;
16
21
  };
17
22
  type ZCLNodeEndpoint = {
18
23
  clusters: { [clusterName: string]: ZCLNodeCluster };
package/lib/Cluster.js CHANGED
@@ -398,9 +398,14 @@ class Cluster extends EventEmitter {
398
398
  * Command which reads a given set of attributes from the remote cluster.
399
399
  * Note: do not mix regular and manufacturer specific attributes.
400
400
  * @param {string[]} attributeNames
401
+ * @param {{timeout: number}} opts
401
402
  * @returns {Promise<{}>} - Object with attribute values (e.g. `{ onOff: true }`)
402
403
  */
403
- async readAttributes(...attributeNames) {
404
+ async readAttributes(attributeNames, opts) {
405
+ if (attributeNames instanceof Array === false) {
406
+ throw new Error('Expected attribute names array, as of zigbee-clusters@2.0.0 call readAttributes([\'myAttr\'])');
407
+ }
408
+
404
409
  if (!attributeNames.length) {
405
410
  attributeNames = Object.keys(this.constructor.attributes);
406
411
  }
@@ -424,7 +429,7 @@ class Cluster extends EventEmitter {
424
429
  const { attributes } = await super.readAttributes({
425
430
  attributes: [...attrIds],
426
431
  manufacturerId,
427
- });
432
+ }, opts);
428
433
 
429
434
  debug(this.logId, 'read attributes result', { attributes });
430
435
  const result = this.constructor.attributeArrayStatusDataType.fromBuffer(attributes, 0);
@@ -956,7 +961,7 @@ class Cluster extends EventEmitter {
956
961
  return new Promise((resolve, reject) => {
957
962
  const t = setTimeout(() => {
958
963
  delete this._trxHandlers[trxSequenceNumber];
959
- reject(new Error('Timeout: Expected Default Response'));
964
+ reject(new Error('Timeout: Expected Response'));
960
965
  }, timeout);
961
966
  this._trxHandlers[trxSequenceNumber] = async frame => {
962
967
  delete this._trxHandlers[trxSequenceNumber];
@@ -1084,8 +1089,14 @@ class Cluster extends EventEmitter {
1084
1089
  return this.sendFrame(payload);
1085
1090
  }
1086
1091
 
1092
+ // Check if a valid timeout override is provided
1093
+ let responseTimeout;
1094
+ if (typeof opts.timeout === 'number') {
1095
+ responseTimeout = opts.timeout;
1096
+ }
1097
+
1087
1098
  const [response] = await Promise.all([
1088
- this._awaitPacket(payload.trxSequenceNumber),
1099
+ this._awaitPacket(payload.trxSequenceNumber, responseTimeout),
1089
1100
  this.sendFrame(payload),
1090
1101
  ]);
1091
1102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zigbee-clusters",
3
- "version": "1.7.3",
3
+ "version": "2.0.0",
4
4
  "description": "Zigbee Cluster Library for Node.js",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",