Creating a Zigbee Driver

Pairing

The first step in creating a Zigbee driver is to retrieve the following properties of the device:

  • manufacturerName
  • productId

These can both be found by pairing the device as Basic Zigbee Device to Homey. Pairing is completely handled by Homey, similar to Z-Wave drivers and in contrast to other types of drivers, you don't have to implement your own pairing wizard. After pairing check the device settings or go to the Zigbee developer tools and take a look at the nodes table where you can find the manufacturerName and productId.

The second step is finding out how the endpoints and clusters are structured on the device. In order to find this information, interview the device from the Zigbee developer tools. For Sleepy End Devices this interview might take a while. After it finishes it will provide the required information.

Manifest

After the manufacturerName, productId and endpoint definitions are found the driver's manifest needs to be created. This is where is defined that this driver is for a Zigbee device, for which Zigbee device specifically (hence the manufacturerName and productId), and what the endpoints and clusters of this Zigbee device are (hence the endpoint definitions).

In order to do so, add a zigbee object to the driver's manifest.

/app.json

{
  "id": "com.athom.example",
  // ..
  "drivers": [
    {
      "id": "my_driver",
      "name": {
        "en": "My Driver"
      },
      "class": "socket",
      "capabilities": ["onoff", "dim"],
      // ...
      "zigbee": {
        "manufacturerName": "DummyManuf",
        "productId": ["control outlet 123"],
        "endpoints": {
          "1": {
            "clusters": [0, 4, 5, 6],
            "bindings": [6]
          }
        },
        "learnmode": {
          "image": "/drivers/my_driver/assets/learnmode.svg",
          "instruction": {
            "en": "Press the button on your device three times"
          }
        }
      }
    }
  ]
}

The zigbee object contains the following properties:

  • manufacturerName: This is the manufacturer id which is needed to identify the device.

  • productId: This is the product id which is needed to identify the device. It is possible to add multiple product ids if the driver targets a couple of very similar devices with different product ids.

  • endpoints: This is the endpoint definition of the device. Only the endpoints and clusters listed here will be available on the ZCLNode instance (see the documentation on homey-zigbeedriver and zigbee-clusters in Interacting with Zigbee devices). The keys of the endpoints object refer to the endpoint id of the node.

    • clusters: This lists the cluster ids we want to implement as client. This means, the clusters we want to send commands to, or read attributes from, on the remote node.
    • bindings: This lists the cluster ids we want to implement as server. This means, the clusters we want to be able to receive commands on from a remote node. For each entry in bindings a bind request will be made to the remote node during pairing. In case you want to implement attribute reporting for a specific cluster, add the cluster id here and the required binding will be made during pairing.

Dependencies and Plugins

In order to create a Zigbee driver a Homey App needs the following dependencies:

The easiest way to manage this is to add the Zigbee plugin to .homeyplugins.json, this will ensure that every time you run homey app build, homey app run or homey app publish these dependencies are installed.

[
  {
    "id": "zigbee"
  }
]

Note that node-zigbee-clusters is a peerDependency of node-homey-zigbeedriver. This means that node-homey-zigbeedriver depends on node-zigbee-clusters being installed with a compatible version.

If you don't want to use the Zigbee plugin, install the dependencies manually by running:

$ npm i --save homey-zigbeedriver zigbee-clusters

Driver and Device

Finally, the driver needs to be created. Most of the time we can suffice with only /drivers/my_driver/device.js. Please refer to Drivers for more information on this topic.

The easiest way to implement a Zigbee driver is to use node-homey-zigbeedriver, which is a library we've created which does a lot of the heavy lifting for Zigbee apps. Take a look at Interacting with Zigbee devices for the specifics of this library. Below, a number of basic implementations of various aspects of a Zigbee driver is demonstrated based on node-homey -zigbeedriver.

Debugging

When developing a Zigbee driver, it can be very useful to see all Zigbee communication between Homey and the Zigbee node. Debug logging for this can be enabled very easily as follows.

It is not recommended to keep this debug logging enabled when publishing your app.

/drivers/my_driver/device.js

const { ZigBeeDevice } = require('homey-zigbeedriver');
const { debug } = require('zigbee-clusters');

// Enable debug logging of all relevant Zigbee communication
debug(true);

class MyZigBeeDevice extends ZigBeeDevice {}

Example logging of a received attribute report frame:

2020-08-07T13:04:30.933Z zigbee-clusters:cluster ep: 1, cl: illuminanceMeasurement (1024) received frame reportAttributes illuminanceMeasurement.reportAttributes {
  attributes: <Buffer 00 00 21 7e 00>
}

Commands

The zclNode is an instance of ZCLNode as exported by node-zigbee-clusters (check Interacting with Zigbee devices for more information on this library). It can be used to directly communicate with the node using the Zigbee Cluster Library (ZCL).

/drivers/my_driver/device.js

const { ZigBeeDevice } = require('homey-zigbeedriver');

class MyZigBeeDevice extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // Send the "toggle" command to cluster "onOff" on endpoint 1
    await zclNode.endpoints[1].clusters.onOff.toggle();

    // Read the "onOff" attribute from the "onOff" cluster
    const currentOnOffValue = await zclNode.endpoints[1].clusters.onOff.readAttributes('onOff');
  }
}

Capabilities

Using node-homey-zigbeedriver it is very easy to map Homey's capabilities to Zigbee clusters. The library contains a set of what we call system capabilities, which are basically very common mappings between capabilities and clusters. It is possible to extend these system capabilities when registering them, for more information take a look at the node-homey-zigbeedriver documentation and Interacting with Zigbee devices.

/drivers/my_driver/device.js

const { ZigBeeDevice } = require('homey-zigbeedriver');
const { CLUSTER } = require('zigbee-clusters');

class MyZigBeeDevice extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // This maps the `onoff` capability to the "onOff" cluster
    this.registerCapability('onoff', CLUSTER.ON_OFF);

    // This maps the `dim` capability to the "levelControl" cluster
    this.registerCapability('dim', CLUSTER.LEVEL_CONTROL);
  }
}

Attribute Reporting

As described in Zigbee, nodes can report attribute changes to any other bound node. This often requires a binding to be made to the node that should report. This can be done using the driver's manifest bindings property. After that, the attribute reporting must be configured on the node's cluster. The example below demonstrates two ways of configuring attribute reporting:

  1. Directly configuring the attribute reporting.
  2. Configuring the attribute reporting in combination with mapping it to a capability.

/drivers/my_driver/device.js

const { ZigBeeDevice } = require('homey-zigbeedriver');
const { CLUSTER } = require('zigbee-clusters');

class MyZigBeeDevice extends ZigBeeDevice {
  async onNodeInit({ zclNode }) {
    // 1.1.) Configure attribute reporting without registering a capability
    await this.configureAttributeReporting([
      {
        endpointId: 1,
        cluster: CLUSTER.COLOR_CONTROL,
        attributeName: 'currentHue',
        minInterval: 0,
        maxInterval: 300,
        minChange: 10,
      },
    ]);

    // 1.2.) Listen to attribute reports for the above configured attribute reporting
    zclNode.endpoints[1].clusters.colorControl.on('attr.currentHue', (currentHue) => {
      // Do something with the received attribute report
    });

    // 2) This maps the `dim` capability to the "levelControl" cluster and additionally configures attribute reporting for the `currentLevel` attribute as specified in the system capability
    this.registerCapability('dim', CLUSTER.LEVEL_CONTROL, {
      reportOpts: {
        configureAttributeReporting: {
          minInterval: 0, // No minimum reporting interval
          maxInterval: 60000, // Maximally every ~16 hours
          minChange: 5, // Report when value changed by 5
        },
      },
    });
  }
}

For more information on configuring attribute reporting check ZigBeeDevice#configureAttributeReporting.

Bindings and Groups

In order to act on incoming commands from bindings or groups it is necessary to implement a BoundCluster (this is exported by node-zigbee-clusters):

/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
    },
  }),
);

For more information on implementing a bound cluster checkout the node-zigbee-clusters documentation on Implementing a bound cluster.

Custom Clusters

It is possible to implement custom clusters, these are often manufacturer specific implementations of existing clusters. This is too in-depth to cover here, but is documented in Implementing a cluster and Implementing a custom cluster.

Sub Devices

This feature is available as of Homey v5.0.0 and requires homey-zigbeedriver@1.6.0 or higher. Additionally, Driver must extend ZigBeeDriver as exported by homey-zigbeedriver.

In some cases a single physical Zigbee device should be represented as multiple devices in Homey after pairing. Most physical Zigbee devices should be represented by a single Homey device for the best user experience. However, for some devices, like a socket with multiple outputs, the user experience is improved when there are multiple Homey devices representing it. Sub devices enable you to automatically create multiple devices in Homey after pairing a single physical Zigbee device. In order for Homey to create multiple instances of Device you need to define a devices property in your Zigbee driver's manifest. The keys of this object represent a unique sub device ID which will be added to the device data object as subDeviceId. For example, based on the manifest below:

/drivers/my-driver/device.js

const { subDeviceId } = this.getData();
// subDeviceId === 'secondOutlet'

This can be used in device.js to discern between the root device, and the various sub devices.

The sub device object can contain any property the root device can contain, e.g. class, name, and capabilties properties that are omitted in the sub device will be copied over from the root device. If the sub device should not get the same settings as the root device, make sure to add the settings: [] property to the sub device, as demonstrated below.

/app.json

{
  "id": "com.athom.example",
  // ...
  "drivers": [
    {
      "id": "my_driver",
      "name": {
        "en": "My Driver"
      },
      "class": "socket",
      "capabilities": ["onoff", "dim"],
      // ...
      "zigbee": {
        "manufacturerName": "DummyManuf",
        "productId": ["control outlet 123"],
        "endpoints": {
          "1": {
            "clusters": [0, 4, 5, 6],
            "bindings": [6]
          }
        },
        // ...
        "devices": {
          "secondOutlet": {
            "class": "light",
            "capabilities": ["onoff"],
            "name": {
              "en": "Second Outlet"
            },
            "settings": []
          }
        }
      }
    }
  ]
}

Each Device will have access to the same ZCLNode instance and can access all endpoints. By default, for each sub device a device instance, as exported in device.js, will be created. If the implementation of device.js is quite different between sub devices you can use Driver#onMapDeviceClass to properly separate the logic for the root and sub devices by implementing multiple Device classes.

/drivers/my_driver/driver.js

const { ZigBeeDriver } = require('homey-zigbeedriver');

const RootDevice = require('./device.js');
const SecondOutletDevice = require('./secondOutlet.device.js');

class MyDriver extends ZigBeeDriver {
  onMapDeviceClass(device) {
    if (device.getData().subDeviceId === 'secondOutlet') {
      return SecondOutletDevice;
    } else {
      return RootDevice;
    }
  }
}