A cluster definition in lib/clusters/*.js is data, not code: ids, types, and the specification
text that explains them. Cluster._addPrototypeMethods turns each entry in COMMANDS into a method
at registration time, so there is nothing to implement beyond the definition itself.
This page describes the conventions those definitions follow. See Contributing for the checks every pull request has to pass.
Three revisions are checked in as docs/zigbee-cluster-specification-r{6,7,8}.pdf. Write
definitions against r8. r6 and r7 are there to look up what an older revision said about a
definition that devices in the field still implement.
Each cluster has its own numbered section. From its tables you need the cluster id and name, every attribute (id, name, type, mandatory or optional) and every command (id, name, arguments, direction, mandatory or optional).
The clearest way in is an existing cluster:
lib/clusters/doorLock.jscarries spec comments, mandatory and optional markers and conditional footnotes, so it shows what a fully documented cluster looks like.lib/clusters/ota.jsshows optional and variant command fields.lib/clusters/onOff.jsis a minimal definition with no spec comments, which is what most of the older clusters look like.
Name the file after the cluster in camelCase, call Cluster.addCluster() at the bottom, and export
it from lib/clusters/index.js.
Data types come from @athombv/data-types; lib/zclTypes.js re-exports them as ZCLDataTypes and
adds enum8Status. The set is wider than the handful most clusters use:
node -e "console.log(Object.keys(require('./lib/zclTypes').ZCLDataTypes).join(' '))"Define an enum or bitmap at module level as soon as two definitions share it.
Both static get ID() and the attribute and command ids are written in a mix of hex and decimal
across the repo. Leave a file's existing style alone rather than reformatting around your change.
New definitions use four-digit lowercase hex for attribute and command ids, with the decimal in a comment where the two differ:
id: 0x0000,
id: 0x000a, // 10
id: 0x0100, // 256For a multi-line definition the decimal goes on the id: line.
The description from the specification goes above the attribute or command, copied as written and wrapped to the Prettier width. When it runs past five sentences, name the spec section instead.
Each entry carries a literal // Mandatory, // Optional or // Conditional comment. The marker
goes at the end of the line for a single-line definition, and on its own line directly above the key
for a multi-line one. Not on the opening brace, where Prettier pulls it inside and it reads as if it
belongs to id, and not on the closing brace.
For attributes the marker comes from the server-side column of the spec table. For commands, server
"receives" is the server-side value and server "generates" the client-side one. An entry the spec
marks M* is conditional, with the condition as a numbered footnote below the group:
numberOfPINUsersSupported: { id: 0x0012, type: ZCLDataTypes.uint16 }, // 18, Conditional¹
numberOfRFIDUsersSupported: { id: 0x0013, type: ZCLDataTypes.uint16 }, // 19, Conditional²
// ¹ PIN credential support
// ² RFID credential supportMost of the older clusters carry no markers at all, so adding one attribute to such a file does not mean backfilling the rest.
Client-to-server commands take an inline response where the spec has one:
lockDoor: {
id: 0x0000,
args: { pinCode: ZCLDataTypes.octstr },
response: {
id: 0x0000,
args: { status: ZCLDataTypes.uint8 },
},
},A server-to-client command is defined standalone, carrying
direction: Cluster.DIRECTION_SERVER_TO_CLIENT, only when the server can send it unsolicited: a
notification such as operationEventNotification, or a response that doubles as one such as
upgradeEndResponse. A response that only ever follows a request, such as
queryNextImageResponse, is already covered by the inline response and must not be repeated.
encodeMissingFieldsBehavior: 'skip' belongs on any struct where some fields are optional, and is
left off when every field is mandatory. This holds for a response struct as much as for the
command itself. When the fields present depend on another field's value, list them all and comment
each group:
args: {
status: ZCLDataTypes.enum8Status,
// When status is SUCCESS
fileOffset: ZCLDataTypes.uint32,
imageData: ZCLDataTypes.buffer,
// When status is WAIT_FOR_DATA
currentTime: ZCLDataTypes.uint32,
requestTime: ZCLDataTypes.uint32,
},IMAGE_BLOCK_RESPONSE in lib/clusters/ota.js is the worked version, with every field the spec
lists. It sits at module level because imageBlockRequest, imagePageRequest and the standalone
imageBlockResponse all share it, the same rule as for enums and bitmaps above.
A definition is worth a test when it encodes or decodes something a plain attribute list does not:
command arguments, a variant payload, a notification. test/onOff.js is the smallest example,
test/ota.js covers variant fields. Writing tests explains the test harness.
Run npm run generate-types on top of the usual lint, format and test commands. index.d.ts is
generated from the cluster definitions and committed, so changing a definition changes it too.