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 +102 -76
- package/index.d.ts +7 -2
- package/lib/Cluster.js +15 -4
- package/package.json +1 -1
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
|
-
|
|
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-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
|
-
|
|
32
|
-
|
|
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(
|
|
49
|
-
const { ZCLNode, CLUSTER } = require(
|
|
63
|
+
const Homey = require("homey");
|
|
64
|
+
const { ZCLNode, CLUSTER } = require("zigbee-clusters");
|
|
50
65
|
|
|
51
66
|
class MyDevice extends Homey.Device {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
84
|
-
const { ZCLNode, CLUSTER } = require(
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
229
|
+
const LevelControlBoundCluster = require("../../lib/LevelControlBoundCluster");
|
|
206
230
|
|
|
207
231
|
// Register the `BoundCluster` implementation with the `ZCLNode`
|
|
208
|
-
zclNode.endpoints[1].bind(
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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[
|
|
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: (
|
|
15
|
-
|
|
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(
|
|
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
|
|
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
|
|