This project implements the Zigbee Cluster Library (ZCL) based on the Zigbee Cluster Library Specification (documentation). 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.
Note: if you are looking for the best way to implement Zigbee drivers for Homey take a look at node-homey-zigbeedriver.
The node-homey-zigbeedriver library implements this project and does a lot of the heavy lifting that is required for most Zigbee drivers for Homey. In the case you need to divert from node-homey-zigbeedriver it is possible to directly use the Zigbee Cluster Library for Node.js.
Make sure to take a look at the API documentation: https://athombv.github.io/node-zigbee-clusters.
$ npm install --save zigbee-clusters
v2.0.0
- Changed
Cluster.readAttributes
signature, attributes must now be specified as an array of strings.
zclNode.endpoints[1].clusters.basic.readAttributes(["modelId", "manufacturerName"]);
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.
A Zigbee cluster is an abstraction on top of the Zigbee protocol which allows implementing functionality for many types of devices. A list of all available clusters can be found in the Zigbee Cluster Library Specification section 2.2.. If you are familiar with Z-Wave Command Classes, Zigbee clusters are very similar.
It is important to understand the structure of a Zigbee node:
A cluster can be implemented in two ways:
- As server
- As client
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..
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. 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.
In order to communicate with a Zigbee node retrieve a node
instance from ManagerZigBee
and create a ZCLNode
instance using that node. This step encapsulates the node
with the Zigbee Clusters functionality and allows sending and receiving ZCL commands.
/drivers/my-driver/device.js
const Homey = require("homey");
const { ZCLNode, CLUSTER } = require("zigbee-clusters");
class MyDevice extends Homey.Device {
onInit() {
// Get ZigBeeNode instance from ManagerZigBee
this.homey.zigbee.getNode(this).then(async (node) => {
// Create ZCLNode instance
const zclNode = new ZCLNode(node);
// Send toggle command to onOff cluster on endpoint 1
await zclNode.endpoints[1].clusters[CLUSTER.ON_OFF.NAME].toggle();
// Send moveToLevel command to levelControl cluster on endpoint 1 and don't wait for
// the default response confirmation.
await zclNode.endpoints[1].clusters[CLUSTER.LEVEL_CONTROL.NAME].moveToLevel(
{
level: 100,
transitionTime: 2000,
},
{
// This is an optional flag that disables waiting for a default response from the
// receiving node as a confirmation that the command is received and executed.
// You should only use this flag if the device does not follow the
// Zigbee specification and refuses to send a default response.
waitForResponse: true,
// This is an optional property that allows for adjusting the response
// timeout (25000ms) before the command is considered rejected.
timeout: 10000,
}
);
});
}
}
/drivers/my-driver/device.js
const Homey = require("homey");
const { ZCLNode, CLUSTER } = require("zigbee-clusters");
class MyDevice extends Homey.Device {
onInit() {
// Get ZigBeeNode instance from ManagerZigBee
this.homey.zigbee.getNode(this).then(async (node) => {
// Create ZCLNode instance
const zclNode = new ZCLNode(node);
// Configure reporting
await zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].configureReporting({
currentSaturation: {
minInterval: 0,
maxInterval: 300,
minChange: 1,
},
});
// And listen for incoming attribute reports by binding a listener on the cluster
zclNode.endpoints[1].clusters[CLUSTER.COLOR_CONTROL.NAME].on(
"attr.currentSaturation",
(currentSaturation) => {
// handle reported attribute value
}
);
});
}
}
It is very easy to add support for a new cluster or add commands and/or attributes to an existing
cluster. All implemented clusters are listed in 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})
.
This example shows in a simplified way how the OnOff cluster is implemented (actual implementation). 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.:
zigbee-clusters/lib/clusters/onOff.js
// Define the cluster attributes
const ATTRIBUTES = {
onOff: { id: 0, type: ZCLDataTypes.bool },
};
// Define the cluster commands (with potential required arguments)
const COMMANDS = {
toggle: { id: 2 },
onWithTimedOff: {
id: 66,
// Optional property that can be used to implement two commands with the same id but different directions. Both commands must have a direction property in that case. See lib/clusters/iasZone.js as example.
// direction: Cluster.DIRECTION_SERVER_TO_CLIENT
args: {
onOffControl: ZCLDataTypes.uint8, // Use the `ZCLDataTypes` object to specify types
onTime: ZCLDataTypes.uint16,
offWaitTime: ZCLDataTypes.uint16,
},
},
};
// Implement the OnOff cluster by extending `Cluster`
class OnOffCluster extends Cluster {
static get ID() {
return 6; // The cluster id
}
static get NAME() {
return "onOff"; // The cluster name
}
static get ATTRIBUTES() {
return ATTRIBUTES; // Returns the defined attributes
}
static get COMMANDS() {
return COMMANDS; // Returns the defined commands
}
}
// Add the cluster to the clusters that will be available on the `ZCLNode`
Cluster.addCluster(OnOffCluster);
module.exports = OnOffCluster;
After a cluster is implemented it can be used on a ZCLNode
instance like this:
await zclNode.endpoints[1].clusters[CLUSTER.ON_OFF.NAME].toggle();
Note that CLUSTER.ON_OFF.NAME
is just a string that refers to onOff
in zigbee-clusters/lib/clusters/onOff.js
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:
/lib/LevelControlBoundCluster.js
const { BoundCluster } = require("zigbee-clusters");
class LevelControlBoundCluster extends BoundCluster {
constructor({ onMove }) {
super();
this._onMove = onMove;
}
// This function name is directly derived from the `move`
// command in `zigbee-clusters/lib/clusters/levelControl.js`
// the payload received is the payload specified in
// `LevelControlCluster.COMMANDS.move.args`
move(payload) {
this._onMove(payload);
}
}
module.exports = LevelControlBoundCluster;
/drivers/my-driver/device.js
const LevelControlBoundCluster = require("../../lib/LevelControlBoundCluster");
// Register the `BoundCluster` implementation with the `ZCLNode`
zclNode.endpoints[1].bind(
CLUSTER.LEVEL_CONTROL.NAME,
new LevelControlBoundCluster({
onMove: (payload) => {
// Do something with the received payload
},
})
);
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):
lib/IkeaSpecificSceneCluster.js
const { ScenesCluster, ZCLDataTypes } = require("zigbee-clusters");
class IkeaSpecificSceneCluster extends ScenesCluster {
// Here we override the `COMMANDS` getter from the `ScenesClusters` by
// extending it with the custom command we'd like to implement `ikeaSceneMove`.
static get COMMANDS() {
return {
...super.COMMANDS,
ikeaSceneMove: {
id: 0x08,
manufacturerId: 0x117c,
args: {
mode: ZCLDataTypes.enum8({
up: 0,
down: 1,
}),
transitionTime: ZCLDataTypes.uint16,
},
},
};
}
// It is also possible to implement manufacturer specific attributes, but beware, do not mix
// these with regular attributes in one command (e.g. `Cluster#readAttributes` should be
// called with only manufacturer specific attributes or only with regular attributes).
static get ATTRIBUTES() {
return {
manufAttribute: {
id: 0,
type: ZCLDataTypes.uint8,
manufacturerId: 0x1234,
},
};
}
}
module.exports = IkeaSpecificSceneCluster;
/drivers/my-driver/device.js
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 });
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.
Great if you'd like to contribute to this project, a few things to take note of before submitting a PR:
- This project enforces ESLint, validate by running
npm run lint
. - This project implements a basic test framework based on mocha, see test directory.
- This project uses several GitHub Action workflows (e.g. ESLint, running test and versioning/publishing).