Control4 DriverWorks References / ZigbeeV2: Driver API / Markdown

Control4 System API Reference

ZigbeeV2

C4:ZigbeeV2() gives a Zigbee 3 device driver an object for its own device: ZDO binds and binding table reads, raw ZCL frames, poll control and check-in settings, device and network status, and OTA image registration, with results delivered to Lua callbacks. This reference covers all 63 method names on OS 4.3.0 and the 16 present on OS 4.2.1, and marks each entry by how much of it was seen working.

Call
C4:ZigbeeV2()
Object
C4LuaZigbeeV2
OS 4.2.1
16 names
OS 4.3.0
63 names
Signing
unsigned, except SendZdo and off-box OTA URLs

Confidence markers

Every method, callback and data shape on this page carries a marker that says how much of it was seen working on a controller and how much is reasoned from its names, parameters and messages.

Markers
MarkerMeaning
Observed on OS 4.3.0 Called on a controller running OS 4.3.0. The returns, callbacks and message texts quoted were seen. A detail in the entry that was not seen is marked inferred where it appears.
Errors observed, effect not observed The argument checks and refusal messages were seen. A valid call was deliberately not made, because it changes the device or the controller, so what it does is inferred.
Signature only: behavior not observed The name, parameters and messages are exact. The call was never seen to run, and its behavior is inferred.
Shape inferred, not observed For callbacks and data shapes. Argument order and field names are exact, but the payload was never captured, and the meaning of the values is inferred.

OS 4.2.1 is covered by name only. The 16 names it carries were confirmed present, but none was called on 4.2.1, so every statement about 4.2.1 behavior is inferred from 4.3.0. Identifiers, numbers and message strings are exact throughout.

The model

One object per call, one callback registration per device. C4:ZigbeeV2() returns a new object every time, always bound to the calling driver's own Zigbee device. The controller sends that device's callbacks to whichever object called Init() last, and drops the registration as soon as any object of the driver is garbage collected. Create one object, keep it for the life of the driver, and never create a second.

The object talks to the controller's Zigbee server, not to the radio. A command returns a commandId at once. The controller queues the frame, reports each step on a status callback carrying that id, and delivers the device's answer, if one comes, on a response callback. Getters answer immediately, from state the controller already holds (inferred).

Scope and state
ItemScopeNotes
objectper call A new C4LuaZigbeeV2 userdata each time. Arguments to the factory are ignored.
target devicethe caller's own No method takes a device address. SendZcl refuses targetEuid.
callback registrationone per device Made by Init(). The most recent Init() wins, and collecting any object of the driver removes it.
callback functionsper object 22 slots. A 4.2.1 name and its 4.3.0 name set the same slot.
tracked ZCL commandIdsper object, 256 (inferred) Only these reach OnZclSentStatus. Inferred: when full, the oldest is dropped.
ZCL sequence numberper object Starts at 0x80 and stays within 0x80 to 0xFF. A frame the controller refuses still uses one (observed).
bind configper device Held by the controller, so it outlives the object (inferred).
Kinds of call
KindMethodsReturnsResults
Command Send*, SetSysCheckInConfig, GetSysQueue, SetSysQueueEmpty commandId, message; SendZcl, SendZclSetLongPollInterval, SendZclSetShortPollInterval and SendZclFastPollStop add seq A status callback for each step, then a response callback when the device or the server answers.
Getter GetZdoBindConfig, and every GetSys* except GetSysQueue table, "" or nil, message None. Synchronous.
Local call Init, SetZdoBindConfig, the 30 callback setters A number, 1 or 0 with a message, or the object SetZdoBindConfig reports later on OnZdoBindConfigStatusEvent (inferred).
Availability by OS
FamilyOS 4.2.1 namesOS 4.3.0 names
RegistrationInitInit
Bind, unbind, bind table CmdZdoBind, CmdZdoUnbind, CmdZdoGetBind SendZdoBind, SendZdoUnbind, SendZdoGetBindTable
Check-in interval CmdZclSetCheckInInterval, CmdZclGetCheckInInterval SendZclWriteCheckInInterval, SendZclReadCheckInInterval
Check-in response settings CmdSetCheckInRes, CmdGetCheckInRes SetSysCheckInConfig, GetSysCheckInConfig
Their callbacks 8 Cmd*Cb, Res*Cb and CheckInResHandledCb setters 8 matching On* setters
Poll intervals, raw ZCL and ZDO, bind config, device state, queue, OTA none 18 more methods and 14 more callbacks

OS 4.3.0 adds 47 names: 15 new names for the 4.2.1 functions, which keep their old names as aliases, and 32 for new functions. Code written against the 4.2.1 names finds them on both releases. Releases before 4.2.1 are not covered. On OS 4.3.0 the PUC Lua host carries the same object and the same 63 names as the LuaJIT host; it was not called there.

Unsigned drivers can call everything except SendZdo, which refuses them outright (observed), and SendSysOtaAddImage refuses image URLs that point off the controller unless the driver is signed (inferred).

Call flow

driver controller device Bind SendZdoBind(6, 1) returns commandId, "" at once Bind_req OnZdoSentStatus(id, 61445, "ZSTATUS_QUEUE_RECEIVED") OnZdoSentStatus(id, 5, "ZSTATUS_IN_PROGRESS") OnZdoSentStatus(id, 0, "ZSTATUS_OK") Bind_rsp OnZdoBindResponse(0, "ZDO_STATUS_SUCCESS") no commandId, no cluster: keep one bind outstanding Refused SendZdoBind(6, 1, 255) OnZdoSentStatus(id, 61447, name) ZSTATUS_INCORRECT_GATEWAY_ENDPOINT, and nothing follows Read the table SendZdoGetBindTable(0) Mgmt_Bind_req, index 0 OnZdoSentStatus x3: 61445, 5, 0 Mgmt_Bind_rsp OnZdoGetBindTableResponse(page) entries = 4, startIndex = 0, three binds SendZdoGetBindTable(3) only while startIndex + #binds < entries OnZdoGetBindTableResponse(page) startIndex = 3, one bind
A bind, a refused bind, and an illustrative four-record table read in two pages. The status sequence and the three-record page size were observed on OS 4.3.0. Ochre arrows are callbacks the object delivers to the driver. Dashed steps are conditional.

Conventions

Getting the object

Test for the factory, create the object once, and keep it in a variable that lives as long as the driver. An unknown method name indexes to nil, so individual 4.3.0 methods can be tested the same way (observed).

local zv2   -- file scope: the driver's only ZigbeeV2 object

function OnDriverLateInit()
  if not C4.ZigbeeV2 then return end
  zv2 = C4:ZigbeeV2()
  if zv2.SendZdoBind then --[[ OS 4.3.0 surface ]] end
end

Inferred, not tested: create it in OnDriverLateInit or later, the same rule that applies to the older Zigbee calls. A driver whose device is not a Zigbee node still gets an object, and its calls fail with "Device not found in Zigbee Manager".

Colon syntax only

Every method checks that its first argument is the object. Called without one, as zv2.GetSysMeshStatus(), it returns nothing; called with a plain table in its place, it raises "An unknown error occurred" (both observed). Assigning a field on the object raises an error, so keep driver state in your own tables.

Three ways to fail

Failure modes
ProblemWhat happensExample
Missing or mistyped argument A Lua error is raised. "Not enough arguments passed (expected 1: index)"
Value out of range or unsupported Returns 0 and a message. Calls that return a sequence number add a third value: 0 when the object itself refuses the call, but the sequence number the refused frame used when the controller refuses it, as in 0, "Invalid cluster ID provided", 128. Test only the first value. 0, "SendZdoGetBindTable invalid index: -1"
Refused or lost on the way A commandId is returned, then a status callback carries a non-zero status. 61447, "ZSTATUS_INCORRECT_GATEWAY_ENDPOINT"

Type errors name the parameter: "clusterId should be a number", with " (or nil)" appended for optional parameters, as in "dstEndpoint should be a number (or nil)". Wrap calls in pcall when their arguments come from user input.

Any command or getter can also fail because the controller cannot reach the device's Zigbee record. A command then returns 0 and a getter nil, with one of "Device not found in Zigbee Manager", "Device not found on the mesh", "Zg server not found for device" or "ZigbeeManager is null" as the message (inferred).

Argument coercion

How arguments are read
InputResultSeen
A numeric string where a number is expected Accepted as the number. SendZdoGetBindTable("3") read from index 3.observed
A fraction Truncated. SendZdoBind(6, 0.5) is refused as endpoint 0.observed
A number where a string is expected Converted to text. For an EUI that text is then read as hex.observed
A value too wide for its field Refused where the method or the controller checks it; otherwise not refused, and the value is cut on the way to the device, so SendZdoGetBindTable(256) reads from 0. observed for 256; rule inferred
0 or 1 for a boolean parameter Accepted, but both count as true. inferred
Extra argumentsIgnored.observed

EUIs are 16-digit hex strings

Every EUI the API accepts or reports as a string is 16 hex digits, reported in upper case, for example "0200000000000001". Pass EUIs the same way. The one exception is GetSysDeviceInfo().euid, which arrives as a Lua number and has lost precision. Do not rebuild an EUI string from it (inferred: an EUI with the top bit set would also come out negative).

commandId is a double

A commandId is a 64-bit value delivered as a Lua number, so it arrives as a value like 2.4418784162631213e+17 and is not exact. The value a command returns and the value its callbacks carry convert the same way, so comparing them with == works (observed). Do not format one with %d or do arithmetic on it. A commandId of 0 always means the command was refused.

What each result can be matched by

Correlation
CallbackCarriesMatch it by
OnZdoSentStatus, OnZclSentStatus, OnSysCheckInConfigSentStatus, OnSysQueueSentStatus, OnSysOtaSentStatus commandIdThe returned commandId.
OnZdoBindResponse, OnZdoUnbindResponse nothingKeep one bind or unbind outstanding at a time.
OnZdoGetBindTableResponse startIndexThe index you asked for. Simplest with one read outstanding.
OnZclReceivedEvent seqNumberThe seq that SendZcl returned.
OnSysQueueResponse cmdIdThe commandId that GetSysQueue returned.
OnSysOtaAddImageResponse commandId (first argument) The commandId that SendSysOtaAddImage returned.
OnZclRead*Response the value onlyOne read per attribute outstanding.

Callbacks for different commands interleave. A bind table page was observed arriving between two status callbacks of the next command.

Binding tables come in pages of three

Each SendZdoGetBindTable answer held at most three records (observed); inferred, the device sets the page size. entries is the size of the whole table, so keep asking from startIndex + #binds until that reaches entries, and stop on an empty page.

Omitted endpoints

For SendZdoBind on OS 4.3.0, omitting dstEndpoint or passing 0 lets the controller pick the coordinator endpoint that lists the cluster as a client cluster (observed). If no coordinator endpoint lists it, the bind is refused with status 61447 and nothing else. Whether 4.2.1 picks the endpoint was not established, so on 4.2.1 pass it.

For SendZcl, a Basic read with the endpoint omitted went out with no destination endpoint and was answered from endpoint 1, the only endpoint with Basic on that two-endpoint device (observed). What happens when a cluster sits on several endpoints is not known, so pass dstEndpoint.

OS 4.2.1 and OS 4.3.0 names

On OS 4.3.0 each 4.2.1 name is a separate Lua function that calls the same implementation as its new name, and gives identical results (observed). The refusal texts differ between releases: on 4.2.1 they begin with the old name, as in "CmdZdoBind invalid clusterId: ", so test the first return value, never the message.

Aliases
OS 4.2.1 nameOS 4.3.0 name
CmdZdoBindSendZdoBind
CmdZdoUnbindSendZdoUnbind
CmdZdoGetBindSendZdoGetBindTable
CmdSetCheckInResSetSysCheckInConfig
CmdGetCheckInResGetSysCheckInConfig
CmdZclSetCheckInIntervalSendZclWriteCheckInInterval
CmdZclGetCheckInIntervalSendZclReadCheckInInterval
CmdZdoExecStatusCbOnZdoSentStatus
CmdZclExecStatusCbOnZclSentStatus
CmdSetCheckInExecStatusCbOnSysCheckInConfigSentStatus
ResZdoBindCbOnZdoBindResponse
ResZdoUnbindCbOnZdoUnbindResponse
ResZdoGetBindCbOnZdoGetBindTableResponse
ResZclGetCheckInIntervalCbOnZclReadCheckInIntervalResponse
CheckInResHandledCbOnSysCheckInHandledEvent

Two meanings of bind

In Composer, the connection that ties a Zigbee driver to its device is also called a binding. Every bind on this page is a ZDO bind instead: an entry in the device's own binding table that tells the device where to send reports and commands for one cluster.

What is fixed and what is not

Method names, parameter names, table keys, status names and numbers, and the message texts are the interface, and are quoted exactly. There is no helper library around this API. Variable and function names in the examples, such as zv2, onSent and readBindTable, are placeholders chosen for this page.

Methods

Called on the object with colon syntax, grouped by what they touch. The callbacks they lead to are in the next section.

Object and registration

C4:ZigbeeV2

Factory. Observed on OS 4.3.0.
C4:ZigbeeV2()  --> object

Returns a new C4LuaZigbeeV2 userdata on every call, bound to the calling driver's own device. Its tostring is C4LuaZigbeeV2 (0x...). Arguments are ignored: C4:ZigbeeV2(12345, "x") still returns an object for the caller's device.

Side effects. None until Init(). When the object is later garbage collected it removes the device's callback registration, whichever object made it. See Behavior notes.

Failure. Inferred: on a driver whose device is not a Zigbee node the object is still created and its calls fail with "Device not found in Zigbee Manager".

Availability. OS 4.2.1 and OS 4.3.0. Unsigned drivers can call it.

Init

Local call. Observed on OS 4.3.0.
zv2:Init()  --> 0 | 1

Registers this object as the receiver of the device's callbacks, replacing any earlier registration for the same device, including one made by another object.

Returns. 0 on success. Inferred: 1 when the controller's Zigbee manager is unavailable. Calling it again returns 0 and registers again, which is how a lost registration is restored.

Without it. Commands and getters work, but no callback reaches this object. Status and response callbacks for its commands go to whichever object of the device did call Init(), and nowhere if none did (observed).

Availability. OS 4.3.0. Present on OS 4.2.1, not called there.

Binds and the bind table

SendZdoBind, SendZdoUnbind

Command. Observed on OS 4.3.0.
zv2:SendZdoBind(clusterId, srcEndpoint[, dstEndpoint, dstDeviceEuid])
zv2:SendZdoUnbind(clusterId, srcEndpoint[, dstEndpoint, dstDeviceEuid])
  --> commandId, message

Asks the device to add, or remove, one entry in its own binding table: from clusterId on srcEndpoint of the device to an endpoint on another node, by default the controller's coordinator.

Parameters
NameTypeNotes
clusterIdnumber req The cluster. Values below 1 are refused (-1 observed; 0 inferred), so cluster 0x0000 can be neither bound nor unbound here. No upper limit is checked; inferred, the value is cut to 16 bits.
srcEndpointnumber req The device endpoint that hosts the cluster. Values below 1 are refused.
dstEndpointnumber or nil Endpoint on the destination. Omit it or pass 0 and the controller picks the coordinator endpoint that lists the cluster. Negative values are refused.
dstDeviceEuidstring or nil Destination EUI, 16 hex digits. Omit it or pass "" for the coordinator. A number is turned into text and read as hex: 12345 bound to 0000000000012345. A string that is not hex makes a bind fall back to the coordinator; inferred, an unbind returns "Invalid destination device EUID" instead.

Returns. commandId, "" once the controller accepts the command. Progress arrives on OnZdoSentStatus with that commandId, then the device's answer on OnZdoBindResponse or OnZdoUnbindResponse, which carry no commandId.

Failures
ResultWhen
raises "Not enough arguments passed (expected 2: clusterId, srcEndpoint[, dstEndpoint, dstDeviceEuid])" Fewer than two arguments.
raises "clusterId should be a number", "srcEndpoint should be a number", "dstEndpoint should be a number (or nil)", "dstDeviceEuid should be a string (or nil)" Wrong type.
0, "SendZdoBind invalid clusterId: -1" clusterId below 1.
0, "SendZdoBind invalid srcEndpoint: 0" srcEndpoint below 1, including 0.5.
0, "SendZdoBind invalid dstEndpoint: -1" dstEndpoint below 0. Not observed.
status 61447 "ZSTATUS_INCORRECT_GATEWAY_ENDPOINT", then nothing The destination endpoint does not list the cluster, or no coordinator endpoint does.

SendZdoUnbind refuses with the same "SendZdoBind" prefix (observed). Inferred: the message can also be one of the controller's own, "Device not found in Zigbee Manager", "Device not found on the mesh", "Zg server not found for device" or "ZigbeeManager is null".

Side effects. Changes the device's binding table. Binding a record that already exists succeeds and adds no duplicate (observed). The device then sends that cluster's reports and commands to the destination; attribute reporting itself is set up separately with ZCL.

Availability. OS 4.3.0. The same implementation answers to CmdZdoBind and CmdZdoUnbind, the names present on OS 4.2.1, where the refusals begin "CmdZdoBind invalid". Unsigned drivers can call both.

SendZdoGetBindTable

Command. Observed on OS 4.3.0.
zv2:SendZdoGetBindTable(index)  --> commandId, message

Reads one page of the device's binding table with a ZDO Mgmt_Bind_req.

Parameters
NameTypeNotes
indexnumber req First record to return, counting from 0. A numeric string works. Negative values are refused. 256 read from 0; inferred, the index is sent as one byte.

Returns. commandId, "". The page arrives on OnZdoGetBindTableResponse. An index at or past the end returns an empty binds with entries still set to the table size.

Failure. Raises "Not enough arguments passed (expected 1: index)" or "index should be a number", which booleans also get. Returns 0, "SendZdoGetBindTable invalid index: -1" for a negative index.

Availability. OS 4.3.0. Also answers to CmdZdoGetBind, the OS 4.2.1 name, where the refusal begins "CmdZdoGetBind invalid index: ".

Bind config

SetZdoBindConfig

Local call. Errors observed, effect not observed.
zv2:SetZdoBindConfig(entries)  --> 1, "" | 0, message
-- entries: { { clusterId = n, srcEndpoint = n,
--              dstEndpoint = n, dstDeviceEuid = s }, ... }, or nil

Hands the controller the complete list of unicast binds the device should hold. Inferred: from then on the controller keeps the device's binding table matched to the list, adding binds that are missing and removing every other unicast bind.

Entry fields
NameTypeNotes
clusterIdnumber req 1 or more. Inferred: cut to 16 bits.
srcEndpointnumber req 1 or more. Inferred: cut to 8 bits.
dstEndpointnumber or nil 0 or more. Inferred: 0 or absent lets the controller pick.
dstDeviceEuidstring or nil Destination EUI. Inferred: absent means the coordinator.

Returns. 1, "" when stored (inferred). This is not a commandId. Progress arrives on OnZdoBindConfigStatusEvent (inferred).

Failures
ResultWhen
0, "SetZdoBindConfig expects a table or nil argument" Any value that is neither. Observed for a boolean and a string.
0, "SetZdoBindConfig entry 1 is not a table" Observed.
0, "SetZdoBindConfig entry 1 missing required field: clusterId" Observed.
0, "SetZdoBindConfig entry 1 missing required field: srcEndpoint" Observed.
0, "SetZdoBindConfig entry N invalid clusterId: X" Below 1. Not observed.
0, "SetZdoBindConfig entry N invalid srcEndpoint: X" Below 1. Not observed.
0, "SetZdoBindConfig entry N invalid dstEndpoint: X" Below 0. Not observed.

A refused list changes nothing: the stored config read the same before and after each refusal (observed).

Side effects. Inferred, never exercised, and not reversible by the API:

nil, no argument, or an empty table clears the list, and the controller then unbinds every unicast bind on the device. The list claims every bind with a 64-bit destination address, whatever node it points at, so binds made by other tools or between two devices are removed too. The list is held by the controller per device: it outlives the object, is sent again when the device comes back online, and is dropped when the device is removed. Sending an unchanged list does nothing. Re-apply the list after a controller restart.

Availability. OS 4.3.0 only. Unsigned drivers can call it.

GetZdoBindConfig

Getter. Observed on OS 4.3.0.
zv2:GetZdoBindConfig()  --> entries, "" | nil, message

Returns the list last stored with SetZdoBindConfig, from the controller. Inferred: it does not read the device.

Returns. An array of { clusterId, srcEndpoint, dstEndpoint, dstDeviceEuid }. Inferred: dstEndpoint is left out when 0 and dstDeviceEuid when empty. Observed only as {}, with nothing stored.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

Sending ZCL and ZDO

SendZcl

Command. Observed on OS 4.3.0.
zv2:SendZcl({ clusterId = n, commandId = n, ... })
  --> commandId, message, seq

Sends one ZCL frame to the device. The object builds the ZCL header from the fields below; payload carries the bytes that follow it.

Table fields
NameTypeNotes
clusterIdnumber req 0 to 0xFFFF. The controller also refuses ids outside its cluster list with 0, "Invalid cluster ID provided", seq (observed for 0xEF00, 0xFF03 and 0xFCC0); see Clusters SendZcl accepts.
commandIdnumber req The ZCL command identifier, 0 to 0xFF.
frameTypestring "cluster", the default (inferred), or "global". Any other string, or a number, is refused.
directionstring "toServer", the default (inferred), or "toClient".
payloadstring Bytes after the ZCL header. A value of another type is ignored.
dstEndpointnumber Device endpoint, 0 to 0xFE. endpoint is accepted as another name for it. Default 0. Inferred: the controller then resolves the endpoint. This was seen only for a cluster the device hosts on one endpoint. Pass it explicitly.
srcEndpointnumber 0 to 0xFE. Default 0; inferred, the controller picks.
manufacturerCodenumber Sets the manufacturer-specific bit and code.
disableDefaultResponseboolean Default true. Only a boolean is read; anything else leaves the default.
profileIdnumber Default 0x0104. The controller refuses unknown profiles (observed for 0xC05E, "Invalid profile ID provided"). Inferred accepted list: 0x0000, 0x0104, 0x0105, 0x0109, 0xA1E0, 0xC00E and 0xC25C to 0xC25E.
apsOptionsnumber Default 0x0100. Inferred: above 0xFFFF is refused by the controller.
addressModestring Only "unicast".
groupId, broadcastAddr, targetEuid, priorityabsent Any value is refused.

Returns. commandId, "", seq. seq is the ZCL transaction sequence number the object put in the header, always 0x80 to 0xFF; the device's answer carries it back as seqNumber. Status arrives on OnZclSentStatus. The answer arrives on OnZclReceivedEvent when that callback is set, and on the driver's OnZigbeePacketIn when it is not (both observed).

Failure. A refusal never raises. The object's own checks (the messages that begin "SendZcl") return 0, message, 0. The controller's checks return 0, message, seq: observed for "Invalid cluster ID provided" and "Invalid profile ID provided", inferred for its endpoint and APS checks. Test the first value only.

Refusals
MessageWhen
"SendZcl expects a single table argument" No argument, or not a table.
"SendZcl: required field 'clusterId' missing or not a number" Missing, or not a number.
"SendZcl: required field 'commandId' missing or not a number" Missing, or not a number.
"SendZcl: frameType must be 'cluster' or 'global'" Any other string, or a number.
"SendZcl: direction must be 'toServer' or 'toClient'" Any other value.
"SendZcl: addressMode 'group' is not supported yet (unicast only)" Any addressMode but "unicast"; the value is quoted.
"SendZcl: 'groupId' is not supported yet (unicast to bound device only)" Also for broadcastAddr, targetEuid and priority.
"SendZcl: invalid clusterId: 70000" Above 0xFFFF or negative.
"SendZcl: invalid commandId: 256"Above 0xFF.
"SendZcl: invalid endpoint: 255" dstEndpoint or endpoint above 0xFE.
"SendZcl: invalid srcEndpoint: 255"Above 0xFE.
"Invalid cluster ID provided", "Invalid profile ID provided" From the controller. Observed; the third value is the seq.
"Invalid APS options provided" From the controller. Inferred, not observed.

Frame type default. Inferred, never sent (do not test it on Basic): an omitted frameType means cluster-specific. A global Read Attributes (command 0x00) sent without frameType = "global" becomes command 0x00 of that cluster instead, which is Reset to Factory Defaults on Basic and Off on On/Off.

Side effects. Advances the object's sequence number, even when the controller then refuses the frame (observed), and adds the commandId to the object's tracked set.

Availability. OS 4.3.0 only. Unsigned drivers can call it (observed). A Basic read with frameType = "global" was observed end to end; the manufacturerCode, apsOptions, srcEndpoint and direction = "toClient" options were not exercised, and profileId only as a refusal.

SendZdo

Command. Errors observed, effect not observed.
zv2:SendZdo({ clusterId = n, payload = s })  --> commandId, message

Sends a raw ZDO request to the device. Signed drivers and agents only.

Table fields
NameTypeNotes
clusterIdnumber req ZDO cluster, 0 to 0xFFFF.
payloadstring ZDO frame bytes. Whether the leading transaction sequence byte belongs in it is not known.
priorityabsent Refused if present.

Failure. From an unsigned driver every call, with any argument or none, returns 0, "SendZdo requires a signed driver or agent" (observed). Signed callers can also get "SendZdo expects a single table argument", "SendZdo: 'priority' is not supported yet", "SendZdo: required field 'clusterId' missing or not a number" and "SendZdo: invalid clusterId: N".

Results. Inferred: status on OnZdoSentStatus. Of the device's ZDO answers, only Bind_rsp, Unbind_rsp and Mgmt_Bind_rsp reach Lua, through the bind callbacks. Any other ZDO response is dropped.

Availability. OS 4.3.0 only. Signed drivers and agents only.

Poll control and check-in

These calls use the device's Poll Control cluster (0x0020), which sleepy end devices use to agree how often they wake and poll. Intervals and timeouts are in quarter seconds, as the ZCL defines those attributes (inferred for this API). None of these was tested on a device that has the cluster.

SendZclReadCheckInInterval, SendZclReadLongPollInterval, SendZclReadShortPollInterval, SendZclReadFastPollTimeout

Command. Observed on OS 4.3.0.
zv2:SendZclReadCheckInInterval(dstEndpoint[, srcEndpoint])
zv2:SendZclReadLongPollInterval(dstEndpoint[, srcEndpoint])
zv2:SendZclReadShortPollInterval(dstEndpoint[, srcEndpoint])
zv2:SendZclReadFastPollTimeout(dstEndpoint[, srcEndpoint])
  --> commandId, message

Each sends a global Read Attributes for one Poll Control attribute, with the default response enabled.

Attributes
MethodAttributeValue callback
SendZclReadCheckInInterval0x0000 Check-in Interval, uint32 OnZclReadCheckInIntervalResponse
SendZclReadLongPollInterval0x0001 Long Poll Interval, uint32 OnZclReadLongPollIntervalResponse
SendZclReadShortPollInterval0x0002 Short Poll Interval, uint16 OnZclReadShortPollIntervalResponse
SendZclReadFastPollTimeout0x0003 Fast Poll Timeout, uint16 OnZclReadFastPollTimeoutResponse
Parameters
NameTypeNotes
dstEndpointnumber req Device endpoint that hosts Poll Control. The controller refuses values above 0xFE.
srcEndpointnumber or nil Default 0. The controller refuses values above 0xFE.

Returns. commandId, "": two values, no sequence number. Status arrives on OnZclSentStatus.

Failure. Raises "Not enough arguments passed (expected 1: dstEndpoint[, srcEndpoint])", "dstEndpoint should be a number" or "srcEndpoint should be a number (or nil)". Returns 0, "Invalid destination endpoint provided" for a dstEndpoint of 300 and 0, "Invalid source endpoint provided" for a srcEndpoint of 300.

Results. On a device without Poll Control each read reported status 61445, 5, 0, and the device answered with a Default Response carrying status 0xC3 (unsupported cluster), which arrived on OnZclReceivedEvent; no value callback fired (observed). On a device with the cluster the value is expected on the matching callback above (inferred).

Availability. OS 4.3.0. SendZclReadCheckInInterval also answers to CmdZclGetCheckInInterval, present on OS 4.2.1. The other three are OS 4.3.0 only.

SendZclWriteCheckInInterval

Command. Errors observed, effect not observed.
zv2:SendZclWriteCheckInInterval(interval, dstEndpoint[, srcEndpoint])
  --> commandId, message

Writes the Check-in Interval attribute (0x0000, uint32) with a global Write Attributes.

Parameters
NameTypeNotes
intervalnumber req Quarter seconds. Negative values are refused.
dstEndpointnumber req Device endpoint that hosts Poll Control.
srcEndpointnumber or nil Default 0.

Returns. commandId, "". Status on OnZclSentStatus; inferred, the device's Write Attributes Response arrives as an incoming frame.

Failure. Raises "Not enough arguments passed (expected 2: interval, dstEndpoint[, srcEndpoint])" or "interval should be a number" (observed). Returns 0, "Invalid interval: N" for a negative interval.

Availability. OS 4.3.0. Also answers to CmdZclSetCheckInInterval, present on OS 4.2.1, whose parameter names there are not known.

SendZclSetLongPollInterval, SendZclSetShortPollInterval

Command. Errors observed, effect not observed.
zv2:SendZclSetLongPollInterval(interval, dstEndpoint[, srcEndpoint])
zv2:SendZclSetShortPollInterval(interval, dstEndpoint[, srcEndpoint])
  --> commandId, message, seq

Send the Poll Control commands Set Long Poll Interval (0x02, uint32 payload) and Set Short Poll Interval (0x03, uint16 payload) as cluster-specific frames, with the default response enabled.

Parameters
NameTypeNotes
intervalnumber req Quarter seconds. Not range checked; inferred, cut to the payload width.
dstEndpointnumber req Device endpoint that hosts Poll Control.
srcEndpointnumber or nil Default 0.

Returns. commandId, "", seq. Status on OnZclSentStatus.

Failure. Raises "Not enough arguments passed (expected 2: interval, dstEndpoint[, srcEndpoint])", "interval should be a number" or "dstEndpoint should be a number" (observed). Inferred: a controller refusal returns 0, message, seq, as for SendZcl.

Availability. OS 4.3.0 only.

SendZclFastPollStop

Command. Errors observed, effect not observed.
zv2:SendZclFastPollStop(dstEndpoint[, srcEndpoint])  --> commandId, message, seq

Sends Fast Poll Stop (0x01, no payload), telling the device to leave fast polling before its timeout.

Returns. commandId, "", seq. Status on OnZclSentStatus.

Failure. Raises "Not enough arguments passed (expected 1: dstEndpoint[, srcEndpoint])" or "dstEndpoint should be a number" (observed). Inferred: a controller refusal returns 0, message, seq.

Availability. OS 4.3.0 only.

GetSysCheckInConfig

Getter. Observed on OS 4.3.0.
zv2:GetSysCheckInConfig()  --> config, "" | nil, message

Returns how the controller answers the device's Poll Control Check-in commands.

Returns. { fastPoll = boolean, timeout = number, doNotSendResponse = boolean }. Three devices without Poll Control all returned { doNotSendResponse = false, fastPoll = true, timeout = 240 }. Inferred meanings: fastPoll, the Check-in Response tells the device to start fast polling; timeout, the fast poll timeout sent with it, in quarter seconds, so 240 is 60 seconds; doNotSendResponse, the controller does not answer check-ins itself.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0. Also answers to CmdGetCheckInRes, present on OS 4.2.1.

SetSysCheckInConfig

Command. Errors observed, effect not observed.
zv2:SetSysCheckInConfig([fastPoll, timeout, doNotSendResponse])
  --> commandId, message

Changes how the controller answers this device's check-ins (inferred).

Parameters
NameTypeNotes
fastPollboolean or nil Default true.
timeoutnumber or nil Default 32, not the 240 that GetSysCheckInConfig reported, so SetSysCheckInConfig(true) changes the timeout to 32. Negative values are refused.
doNotSendResponseboolean or nil Default false.

No arguments is a write. Every parameter is optional, so SetSysCheckInConfig() is not a read: it sends fastPoll = true, timeout = 32 and doNotSendResponse = false (inferred).

Answering check-ins. Inferred: the controller answers the device's check-ins itself unless doNotSendResponse is true. A driver that also answers through C4:SendZigbeePacket should set it, or not answer.

Returns. commandId, "". Status on OnSysCheckInConfigSentStatus.

Failure. Raises "fastPoll should be a boolean (or nil)" or "timeout should be a number (or nil)" (observed). Returns 0, "SetSysCheckInConfig invalid timeout: N" for a negative timeout. Whether GetSysCheckInConfig then reports the new values was not checked.

Availability. OS 4.3.0. Also answers to CmdSetCheckInRes, present on OS 4.2.1, where the refusal begins "CmdSetCheckInRes invalid timeout: ".

Device state and the queue

GetSysDeviceInfo

Getter. Observed on OS 4.3.0.
zv2:GetSysDeviceInfo()  --> info, "" | nil, message

Returns what the controller knows about the device: network identity, manufacturer and model strings, firmware, and the endpoint and cluster inventory. The shape is under Device info.

endpoints is keyed by endpoint number, so iterate it with pairs. euid is an inexact number. firmwareVersion is several lines of text. lastMessageAge stayed the same across reads more than an hour apart, and did not move on a device that reported every two seconds, so it is not a live age (observed).

Failure. Inferred: nil with "Device info not available" or "Device not found in Zigbee Manager".

Availability. OS 4.3.0 only.

GetSysDeviceStatus

Getter. Observed on OS 4.3.0.
zv2:GetSysDeviceStatus()  --> status, "" | nil, message

Returns. { statusInt = 0, status = "ONLINE" } (observed) or { statusInt = 1, status = "OFFLINE" }, with rebootTimestamp added when it is not zero. Inferred: there are only these two values, and the controller's unknown, offline and unstable states all report 1, "OFFLINE". rebootTimestamp is probably in milliseconds.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

GetSysDeviceDiscovery

Getter. Observed on OS 4.3.0.
zv2:GetSysDeviceDiscovery()  --> discovery, "" | nil, message

Returns { status, statusInt }: how far the controller has got interviewing the device after it joined.

statusInt and status
statusIntstatus
0NONE
1NEW
2STACK_REV
4ENDPOINTS
8CLUSTERS
16NEIGHBORS
32CHILDREN
64DONE
128FAILED

64, "DONE" was observed; the other rows are inferred. OnSysDeviceDiscoveryEvent numbers the same process differently.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

GetSysMeshStatus

Getter. Observed on OS 4.3.0.
zv2:GetSysMeshStatus()  --> mesh, "" | nil, message

Returns { meshStatus }, the state of the Zigbee network the device is on: "UNKNOWN", "UP", "DOWN", "BUSY", "IDLE" or "OPEN". "UP" was observed; the others are inferred, and "OPEN" probably means joining is allowed.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

GetSysQueue

Command. Observed on OS 4.3.0.
zv2:GetSysQueue()  --> commandId, message

Asks the controller for this device's queue of outgoing messages. Despite the name it is asynchronous.

Results. OnSysQueueResponse(report), then OnSysQueueSentStatus(commandId, 0, "ZSTATUS_OK"), in that order (observed). The report is under Queue report.

Failure. 0, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

SetSysQueueEmpty

Command. Signature only: behavior not observed.
zv2:SetSysQueueEmpty()  --> commandId, message

Inferred: discards this device's pending outgoing messages in the controller. It takes no arguments and checks none, so any call acts.

Results. Status on OnSysQueueSentStatus. Inferred: refused while the controller is still setting the device up after it joined.

Failure. 0, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

OTA images

SendSysOtaAddImage

Command. Errors observed, effect not observed.
zv2:SendSysOtaAddImage({ { url = s, sha256 = s }, ... })
  --> commandId, message

Registers firmware image files for the controller to offer the device over the OTA Upgrade cluster (inferred).

Image fields
NameTypeNotes
urlstring req Where the controller fetches the image. An unsigned driver may only name the controller itself: controller://..., or http:// or https:// with the host director, 127.0.0.1 or localhost, followed by the end of the URL or one of : / ? #. Case is ignored.
sha256string Digest of the image; inferred, as hex.

Returns. commandId, "". Inferred: status on OnSysOtaSentStatus, a per-image report on OnSysOtaAddImageResponse, and upgrade progress on OnSysOtaProgressEvent.

Failure. Returns 0, "SendSysOtaAddImage: images[1] must be a table" or 0, "SendSysOtaAddImage: images[1].url is required" (observed), and 0, "SendSysOtaAddImage: images[N] off-box URL '<url>' requires a signed driver or agent" for an unsigned driver naming another host (not observed).

Side effects. Inferred, never exercised: nil, no argument, a value that is not a table, or an empty list removes every image registered for the device.

Availability. OS 4.3.0 only. Off-box URLs need a signed driver or agent.

GetSysOtaInfo

Getter. Observed on OS 4.3.0.
zv2:GetSysOtaInfo()  --> ota, "" | nil, message

Returns the device's OTA state and the images registered for it: { stateInt, state, currentVersion, progress, images }. Observed idle, with stateInt = 1, state = "idle", the device's firmware version and an empty images. The shape is under OTA.

Failure. nil, message as under Three ways to fail (inferred).

Availability. OS 4.3.0 only.

Callbacks

Each callback is set with the method of the same name and later runs with the arguments below. Only the object that called Init() last receives any. The object is never passed to the callback.

Callback setters

Local call. Observed on OS 4.3.0.
zv2:OnZdoSentStatus(func)  --> zv2
zv2:OnZdoSentStatus(onSent):OnZdoBindResponse(onBind)   -- setters chain

Every setter, all 22 On* names and the 8 older *Cb names, stores func and returns the object, so calls chain.

Failure. Raises "Not enough arguments passed (expected 1: func)" with no argument, and "func should be a function" for nil, a number or a string.

No setter clears a slot, because nil is refused. The only way to stop callbacks is to drop the object, which also removes the device's registration. A 4.2.1 name and its 4.3.0 name set the same slot: the last one set wins, and a function set under the other name never runs (observed). Inferred: an error raised inside a callback is caught and logged by the controller, and return values are ignored.

All callbacks
SetterOS 4.2.1 nameArgumentsSeen
OnZdoSentStatusCmdZdoExecStatusCbcommandId, status, statusNameobserved
OnZclSentStatusCmdZclExecStatusCbcommandId, status, statusNameobserved
OnSysCheckInConfigSentStatusCmdSetCheckInExecStatusCbcommandId, status, statusNamenot seen
OnZdoBindResponseResZdoBindCbzdoStatus, zdoStatusNameobserved
OnZdoUnbindResponseResZdoUnbindCbzdoStatus, zdoStatusNameobserved once
OnZdoGetBindTableResponseResZdoGetBindCbpageobserved
OnZclReceivedEventnoneframeobserved
OnZclReadCheckInIntervalResponseResZclGetCheckInIntervalCbvaluenot seen
OnZclReadLongPollIntervalResponsenonevaluenot seen
OnZclReadShortPollIntervalResponsenonevaluenot seen
OnZclReadFastPollTimeoutResponsenonevaluenot seen
OnSysCheckInHandledEventCheckInResHandledCbhandlednot seen
OnSysDeviceStatusChangedEventnonestatusnot seen
OnSysDeviceInfoEventnoneinfonot seen
OnSysDeviceDiscoveryEventnonestateInt, stateName, endpointnot seen
OnSysMeshStatusEventnonemeshStatusnot seen
OnZdoBindConfigStatusEventnonebindStatenot seen
OnSysQueueSentStatusnonecommandId, status, statusNameobserved
OnSysQueueResponsenonereportobserved
OnSysOtaSentStatusnonecommandId, status, statusNamenot seen
OnSysOtaAddImageResponsenonecommandId, reportnot seen
OnSysOtaProgressEventnoneprogressnot seen

Callbacks marked not seen did not fire during ordinary traffic; no join, leave or check-in was provoked.

OnZdoSentStatus, OnZclSentStatus, OnSysCheckInConfigSentStatus, OnSysQueueSentStatus, OnSysOtaSentStatus

Callback. Observed on OS 4.3.0.
function onSent(commandId, status, statusName) end

Progress of a command, keyed by the commandId it returned. statusName is the name of status; see Status values.

Which commands report where
SetterReportsSeen
OnZdoSentStatus Bind, unbind and bind table commands from any object of the device (observed); raw ZDO commands (inferred).observed
OnZclSentStatus SendZcl and the poll control commands (observed); only those this object issued (inferred).observed
OnSysCheckInConfigSentStatus SetSysCheckInConfig.not seen
OnSysQueueSentStatus GetSysQueue, SetSysQueueEmpty.observed
OnSysOtaSentStatus SendSysOtaAddImage.not seen

A ZDO or ZCL command normally reports 61445 "ZSTATUS_QUEUE_RECEIVED", then 5 "ZSTATUS_IN_PROGRESS", then 0 "ZSTATUS_OK", in quick succession (observed). GetSysQueue reported only 0. ZSTATUS_OK means the frame was sent; the device's answer is a separate callback. Inferred: any status other than 61445 and 5 is final.

Inferred: OnZclSentStatus fires only for commands this object issued and still tracks. It tracks at most 256, drops the oldest first, and stops tracking a command at its final status. Frames sent with C4:SendZigbeePacket never reach it (observed).

Availability. OS 4.3.0. The first three also answer to CmdZdoExecStatusCb, CmdZclExecStatusCb and CmdSetCheckInExecStatusCb, present on OS 4.2.1.

OnZdoBindResponse, OnZdoUnbindResponse

Callback. Observed on OS 4.3.0.
function onBind(zdoStatus, zdoStatusName) end   -- 0, "ZDO_STATUS_SUCCESS"

The device's Bind_rsp or Unbind_rsp. zdoStatus is the ZDO status number and zdoStatusName its name; see ZDO status values.

It carries no commandId, cluster or endpoint, so a driver can only attribute it by having one request outstanding. Inferred: it fires for every bind or unbind response the device sends, whoever sent the request.

Availability. OS 4.3.0. Also ResZdoBindCb and ResZdoUnbindCb, present on OS 4.2.1.

OnZdoGetBindTableResponse

Callback. Observed on OS 4.3.0.
function onTable(page) end

One page of the device's binding table, shaped as under Bind table page. binds is an array. Inferred: the address mode of each record is dropped, so a group-addressed record cannot be told apart from a unicast one.

Availability. OS 4.3.0. Also ResZdoGetBindCb, present on OS 4.2.1.

OnZclReceivedEvent

Callback. Observed on OS 4.3.0.
function onZcl(frame) end

Each ZCL frame from the device: attribute reports, answers to SendZcl, and default responses. frame.payload is the whole ZCL frame, header included. The shape is under Incoming ZCL frame.

Side effect. Setting it on the registered object takes those frames away from the driver's OnZigbeePacketIn. The one exception is a Read Attributes Response with a sequence number below 0x80, which goes only to OnZigbeePacketIn. See Behavior notes.

Availability. OS 4.3.0 only.

OnZclReadCheckInIntervalResponse, OnZclReadLongPollIntervalResponse, OnZclReadShortPollIntervalResponse, OnZclReadFastPollTimeoutResponse

Callback. Shape inferred, not observed.
function onPollValue(value) end

Inferred: the object reads every Poll Control (0x0020) Read Attributes Response from the device and passes each attribute value to its slot: 0x0000 and 0x0001 as uint32, 0x0002 and 0x0003 as uint16. Records of another type, or with a failure status, are skipped, as are manufacturer-specific responses. These fire as well as OnZclReceivedEvent, and for any read of those attributes, however it was sent.

Availability. OS 4.3.0. The first also answers to ResZclGetCheckInIntervalCb, present on OS 4.2.1.

OnSysCheckInHandledEvent

Callback. Shape inferred, not observed.
function onCheckIn(handled) end   -- always true

Inferred: fires when the controller has answered a Check-in command from the device, with handled always true.

Availability. OS 4.3.0. Also CheckInResHandledCb, present on OS 4.2.1.

OnSysDeviceStatusChangedEvent

Callback. Shape inferred, not observed.
function onDeviceStatus(status) end   -- { statusInt, status, rebootTimestamp }

The same table as GetSysDeviceStatus. Inferred: fires when the device goes online or offline and when it reboots, with rebootTimestamp present only when it is not zero.

Side effect. Inferred, not tested: while this callback is set, the controller stops applying the device's online, offline and reboot changes through the older driver path. Test a driver that relies on OnZigbeeOnlineStatusChanged before setting it.

Availability. OS 4.3.0 only.

OnSysDeviceInfoEvent

Callback. Shape inferred, not observed.
function onDeviceInfo(info) end

The same table as GetSysDeviceInfo. Inferred: fires when the controller's record of the device changes, such as after the device is interviewed again.

Availability. OS 4.3.0 only.

OnSysDeviceDiscoveryEvent

Callback. Shape inferred, not observed.
function onDiscovery(stateInt, stateName, endpoint) end

Steps of the controller's interview of the device. The numbering is not the one GetSysDeviceDiscovery uses.

stateInt and stateName
stateIntstateName
0NEW
1NODE_QUEUE
2NODE_SEND
3NODE_WAITING
4ENDPOINTS_QUEUE
5ENDPOINTS_SEND
6ENDPOINTS_WAITING
7SIMPLE_QUEUE
8SIMPLE_SEND
9SIMPLE_WAITING
10LQI_QUEUE
11LQI_SEND
12LQI_WAITING
13IEEE_QUEUE
14IEEE_SEND
15IEEE_WAITING
16JOIN
17JOIN_NODE_QUEUE
18JOIN_NODE_SEND
19JOIN_NODE_WAITING
20JOIN_KEY_WAITING
21DONE
255FAILED

endpoint is the device endpoint the step concerns (inferred).

Availability. OS 4.3.0 only.

OnSysMeshStatusEvent

Callback. Shape inferred, not observed.
function onMesh(meshStatus) end   -- a string, as in GetSysMeshStatus

A single string, not a table. Inferred: it may never fire on OS 4.3.0. Poll GetSysMeshStatus instead.

Availability. OS 4.3.0 only.

OnZdoBindConfigStatusEvent

Callback. Shape inferred, not observed.
function onBindConfig(bindState) end   -- { state = "synced" }

Inferred: reports how far the controller has got matching the device's binding table to the SetZdoBindConfig list. state is "synced", "pending", "disabled" or "unknown"; "disabled" means the device refused ZDO binds and the controller stopped trying.

Availability. OS 4.3.0 only.

OnSysQueueResponse

Callback. Observed on OS 4.3.0.
function onQueue(report) end

The answer to GetSysQueue, shaped as under Queue report. It arrived before the command's OnSysQueueSentStatus.

Availability. OS 4.3.0 only.

OnSysOtaAddImageResponse, OnSysOtaProgressEvent

Callback. Shape inferred, not observed.
function onOtaAdded(commandId, report) end
function onOtaProgress(progress) end

OnSysOtaAddImageResponse reports the result of SendSysOtaAddImage for each image, with the commandId first. OnSysOtaProgressEvent reports the device's upgrade as it moves between states. Both shapes are under OTA.

Availability. OS 4.3.0 only.

Data shapes

Field names are exact. Values in the literals are composed examples, shaped like the observed ones. Optional fields are left out of the table when they have no value, as rebootTimestamp is when it is zero; empty lists such as binds are still present.

Status values

Confidence: Observed on OS 4.3.0.

status and statusName in every *SentStatus callback. The full set has 154 values, all named ZSTATUS_*; these are the ones a driver is most likely to meet. The four marked observed were seen; the rest are inferred from their names.

Status values
statusstatusNameMeaning
61445ZSTATUS_QUEUE_RECEIVEDAccepted into the controller's queue. Observed.
5ZSTATUS_IN_PROGRESSBeing sent. Observed.
0ZSTATUS_OKSent. Final. Observed.
61447ZSTATUS_INCORRECT_GATEWAY_ENDPOINTThe coordinator endpoint does not model the cluster. Final. Observed.
61448ZSTATUS_INCORRECT_DEVICE_ENDPOINTThe device endpoint does not fit the request.
61441ZSTATUS_ZSERVER3_DEVICE_QUEUE_FULLThe device's queue is full.
61442ZSTATUS_ZSERVER3_DEVICE_SHARED_QUEUE_FULLA shared queue is full.
61443ZSTATUS_ZSERVER3_ZIGBEE_GATEWAY_QUEUE_FULLThe radio's queue is full.
61444ZSTATUS_ZSERVER3_DEVICE_OFFLINEThe device is offline.
61446ZSTATUS_PARTIAL_SUCCESSPart of a request succeeded.
7ZSTATUS_TIMEOUTTimed out.
3074ZSTATUS_ZIGBEE_DELIVERY_FAILEDThe network could not deliver it.
3104ZSTATUS_ZIGBEE_NO_APS_ACKThe device did not acknowledge it.
3110ZSTATUS_ZIGBEE_SEND_UNICAST_NO_ROUTENo route to the device.

ZDO status values

Confidence: Observed on OS 4.3.0.

zdoStatus and zdoStatusName in the bind callbacks, and statusInt and status in a bind table page. Only 0 "ZDO_STATUS_SUCCESS" was seen; the other names are exact but were not produced.

ZDO status values
ValueName
0ZDO_STATUS_SUCCESS
128ZDO_STATUS_INVALID_REQUEST_TYPE
129ZDO_STATUS_DEVICE_NOT_FOUND
130ZDO_STATUS_INVALID_ENDPOINT
131ZDO_STATUS_NOT_ACTIVE
132ZDO_STATUS_NOT_SUPPORTED
133ZDO_STATUS_TIMEOUT
134ZDO_STATUS_NO_MATCH
136ZDO_STATUS_NO_ENTRY
137ZDO_STATUS_NO_DESCRIPTOR
138ZDO_STATUS_INSUFFICIENT_SPACE
139ZDO_STATUS_NOT_PERMITTED
140ZDO_STATUS_TABLE_FULL
141ZDO_STATUS_NOT_AUTHORIZED
142ZDO_STATUS_DEVICE_BINDING_TABLE_FULL
143ZDO_STATUS_INVALID_INDEX
144ZDO_STATUS_FRAME_TOO_LARGE
145ZDO_STATUS_BAD_KEY_NEGOTIATION_METHOD
146ZDO_STATUS_TEMPORARY_FAILURE
173APS_STATUS_SECURITY_FAIL
197NWK_STATUS_ALREADY_PRESENT
199NWK_STATUS_NWK_TABLE_FULL
200NWK_STATUS_UNKNOWN_DEVICE
214NWK_STATUS_MISSING_TLV
215NWK_STATUS_INVALID_TLV

Bind table page

Confidence: Observed on OS 4.3.0.

The argument to OnZdoGetBindTableResponse.

{
  statusInt  = 0,
  status     = "ZDO_STATUS_SUCCESS",
  entries    = 4,        -- records in the whole table
  startIndex = 0,        -- table index of binds[1]
  binds = {              -- an array; at most 3 per page observed
    { clusterId = 6,    srcEndpoint = 1, dstEndpoint = 1,
      dstDeviceEuid = "0200000000000001" },
    { clusterId = 1794, srcEndpoint = 1, dstEndpoint = 3,
      dstDeviceEuid = "0200000000000001" },
    { clusterId = 0,    srcEndpoint = 1, dstEndpoint = 1,
      dstDeviceEuid = "0200000000000001" },   -- SendZdoUnbind refuses cluster 0
  },
}

Device info

Confidence: Observed on OS 4.3.0.

Returned by GetSysDeviceInfo and passed to OnSysDeviceInfoEvent. The field list is exact and was seen on three devices; the meanings marked inferred were not confirmed.

{
  euid             = 1.4411518807585587e+17, -- the EUI as a number: inexact
  networkId        = 18344,     -- inferred: the 16-bit network address
  state            = 0,
  stateName        = "NONE",    -- inferred others: JOINED 16, UNRESPONSIVE 17,
                                -- LEAVE_SENT 32, LEFT 48, UNKNOWN 255
  lastMessageAge   = 5172,      -- unit unknown; not a live age
  controllerId     = 20,        -- inferred: device id of the hosting controller
  hardwareVersion  = 1,
  manufacturer     = "<manufacturer name the device reports>",
  product          = "<model the device reports>",
  firmwareVersion  = "DeviceFwVersion:\n     versionStr: 0.0.12\n     version: 12\n",
  stackRevision    = 22,
  capabilities     = 142,
  capabilitiesName = "ROUTER",  -- inferred others: SLEEPY 128, END_DEVICE 140,
                                -- COORDINATOR 143, TRUST_CENTER 207, else UNKNOWN
  endpoints = {
    [1] = {                     -- keyed by endpoint number: iterate with pairs
      deviceType     = 81,
      deviceTypeName = "SMART_PLUG",
      profileId      = 260,
      profileName    = "HA",
      serverClusters = {
        { id = 0,     name = "ZCL_BASIC" },
        { id = 6,     name = "ZCL_ON_OFF" },
        { id = 65280, name = "0x0000ff00" },    -- unknown ids come back as hex
      },
      clientClusters = { { id = 25, name = "ZCL_OTA_BOOTLOAD" } },
    },
  },
}

Incoming ZCL frame

Confidence: Observed on OS 4.3.0.

The argument to OnZclReceivedEvent. This one is an On/Off attribute report.

{
  profileId   = 260,
  clusterId   = 6,
  seqNumber   = 5,       -- ZCL transaction sequence number, also in payload
  apsOptions  = 256,     -- 256 seen on reports, 352 on answers to SendZcl
  srcEndpoint = 1,       -- device endpoint
  dstEndpoint = 1,       -- controller endpoint
  payload     = "\24\5\10\0\0\16\1",
  -- the whole ZCL frame: frame control 0x18, seq 5, command 0x0A (Report
  -- Attributes), attribute 0x0000, type 0x10 (boolean), value 1
}

SendZcl request

Confidence: Observed on OS 4.3.0.

A read of two Basic attributes, the case observed end to end.

local id, err, seq = zv2:SendZcl({
  clusterId   = 0x0000,
  commandId   = 0x00,        -- Read Attributes
  frameType   = "global",    -- required: the default is "cluster" (inferred)
  dstEndpoint = 1,
  payload     = "\4\0\5\0",  -- attribute ids 0x0004 and 0x0005, little endian
})
-- id ~= 0: the answer comes back with seqNumber == seq

Queue report

Confidence: Observed on OS 4.3.0.

The argument to OnSysQueueResponse. Only an empty queue was seen, so the shape of messages entries is inferred.

{
  cmdId         = 2.4418e+17,   -- the commandId GetSysQueue returned
  pendingCount  = 0,
  inflightCount = 0,
  total         = 0,            -- pendingCount + inflightCount
  messages = {
    { cmdId = 2.4418e+17, priority = 0, command = "<text>", inflight = false },
  },
}

Bind config entry

Confidence: Shape inferred, not observed.

What SetZdoBindConfig takes and GetZdoBindConfig returns. Field names are exact.

{
  { clusterId = 0x0006, srcEndpoint = 1 },           -- controller picks the rest
  { clusterId = 0x0702, srcEndpoint = 1, dstEndpoint = 3,
    dstDeviceEuid = "0200000000000001" },
}

OTA

Confidence: Shape inferred, not observed.

Only an idle GetSysOtaInfo with no images was seen. Field names are exact; the value lists are inferred.

-- GetSysOtaInfo()
{
  stateInt       = 1,           -- observed idle; inferred: 0 unknown, 2 downloading,
                                -- 3 started, 4 completed, 5 failed
  state          = "idle",      -- also "unknown", "downloading", "started",
                                -- "completed", "failed", "transferring"
  currentVersion = "0.0.12",    -- the device's firmware version
  progress       = 0,           -- unit not confirmed
  images = {
    { url = "controller://...", manufacturerId = 0, imageTypeId = 0, version = 0,
      storedFileName = "...", statusStr = "SUCCESS", selected = false },
  },
}

-- OnSysOtaAddImageResponse(commandId, report)
{
  statusInt = 1, statusStr = "SUCCESS",
  images = {
    { url = "controller://...", statusInt = 1, statusStr = "SUCCESS",
      manufacturerId = 0, imageTypeId = 0, version = 0, storedFileName = "..." },
  },
}
-- statusInt and statusStr: 0 UNKNOWN, 1 SUCCESS, 2 FAILURE, 3 URL_NOT_FOUND,
-- 4 DOWNLOAD_FAILED, 5 INVALID_IMAGE, 6 DUPLICATE_IMAGE_TYPE, 7 ALREADY_EXISTS

-- OnSysOtaProgressEvent(progress)
{ state = "downloading", stateInt = 2, progress = 0, manufacturerId = 0,
  imageTypeId = 0,
  firmwareVersion = 12,   -- a number (the version integer), not the text
  filename = "..." }

Clusters SendZcl accepts

Confidence: Observed on OS 4.3.0.

The controller accepts only the 117 cluster ids below and refuses the rest with 0, "Invalid cluster ID provided", seq. Refusal was observed for 0xEF00, 0xFF03 and 0xFCC0; the accepted list was not exercised id by id. A cluster the device reports in GetSysDeviceInfo can still be refused: 0xFF03 was. Inferred from the list: 0xFF01, 0x000C and 0x0012 are refused too.

Accepted cluster ids
RangeIds
0x0000 to 0x00FF0x0000-0x000B, 0x000F, 0x0015-0x0016, 0x0019-0x001B, 0x0020-0x0021, 0x0025
0x0100 to 0x03FF0x0100-0x0103, 0x0200-0x0204, 0x0300-0x0301
0x0400 to 0x06FF0x0400-0x0406, 0x040C-0x0429, 0x0500-0x0502, 0x0600-0x0601, 0x0614-0x0615
0x0700 to 0x0BFF0x0700-0x070B, 0x0800, 0x0900-0x0905, 0x0A01-0x0A02, 0x0B00-0x0B05
0x1000 and up0x1000, 0xFC00-0xFC0B, 0xFC57

Behavior notes

Collecting any object unregisters the device

Observed. The registration belongs to the device, and every object removes it when it is garbage collected, including an object that never called Init(). A throwaway C4:ZigbeeV2():GetSysDeviceInfo() therefore silences the driver's long-lived object until that object calls Init() again.

Keep one object. If another was ever created and dropped, call Init() on the one you keep.

The last Init() gets every callback

Observed. A second object that calls Init() takes the status and response callbacks for the device, including those for commands the first object sends. OnZclSentStatus is the exception: an object reports only commands it issued itself, so ZCL commands sent by one object while another holds the registration report no status (inferred).

OnZclReceivedEvent takes incoming ZCL away from OnZigbeePacketIn

Observed. While the registered object has OnZclReceivedEvent set, the device's attribute reports, default responses and answers to SendZcl arrive there and not at OnZigbeePacketIn. No report reached the older handler while the callback was set. Delivery to OnZigbeePacketIn resumed the moment that object was replaced by one without the handler. A registered object without the handler leaves the older path alone.

The exception is a Read Attributes Response whose sequence number is below 0x80: it goes only to OnZigbeePacketIn, which keeps answers to reads sent with C4:SendZigbeePacket on the older path. Observed for one read; inferred, the exception covers no other command, so responses such as Configure Reporting still move to the callback.

OnSysDeviceStatusChangedEvent may take over online status

Inferred, not tested. While it is set, the controller appears to stop applying online, offline and reboot changes through the older driver path.

Sequence numbers 0x80 to 0xFF belong to the object

Observed. Every ZCL frame the object builds uses a sequence number from 0x80 to 0xFF; a new object started at 128. The Read Attributes exception above applies only below 0x80. So while OnZclReceivedEvent is set, a driver that also sends reads with C4:SendZigbeePacket gets their answers on OnZigbeePacketIn only if its own sequence numbers stay below 0x80 (inferred).

Bind responses carry no id

Observed. OnZdoBindResponse and OnZdoUnbindResponse say only whether it worked. With two binds outstanding there is no way to tell which answer is which. Send one, wait for its response or a final status, then send the next.

A number EUI is read as hex

Observed. Passing the number 12345 as dstDeviceEuid bound to 0000000000012345, a node that does not exist, and the device accepted it. Always pass a 16-digit hex string, or leave the argument out.

Old and new names share one slot

Observed. ResZdoBindCb and OnZdoBindResponse set the same slot, and so on for every pair. Setting both leaves only the last one; a function set under the other name never runs.

SetZdoBindConfig removes unlisted unicast binds

Inferred, never exercised. The controller treats every bind with a 64-bit destination on the device as its own to manage, and unbinds any that is not in the list, including binds made by other tools or by the user between two devices. nil, no argument and {} all mean an empty list, which unbinds everything.

Some calls act with no arguments

SetSysCheckInConfig() writes its defaults, SetSysQueueEmpty() empties the queue, SetZdoBindConfig() clears the bind list and SendSysOtaAddImage() removes every image (all inferred). None of them is a safe probe.

Two calls need a signed driver

SendZdo refuses unsigned drivers for every call (observed). SendSysOtaAddImage refuses URLs that do not point at the controller (inferred). Everything else, including binds and raw ZCL, works unsigned.

SendZcl defaults to a cluster command

Inferred, not exercised. Without frameType = "global", command 0x00 is the cluster's own command 0x00: Reset to Factory Defaults on Basic, Off on On/Off.

Cluster 0x0000 cannot be bound or unbound here

SendZdoBind and SendZdoUnbind refuse cluster ids below 1 (-1 observed). A device's table can still hold a Basic bind made some other way (observed), and SendZdoUnbind cannot remove it. SetZdoBindConfig would (inferred): a cluster-0 bind can never be in its list, so it is unbound with every other unlisted bind.

Some values wrap instead of failing

Only some limits are checked. Where a method has no explicit check, a value too wide for its field is cut down rather than refused: SendZdoGetBindTable(256) reached the controller as 256 and the device answered from index 0 (observed). The SendZdoBind upper bounds, the SetZdoBindConfig fields and the poll intervals behave the same way (inferred). SendZcl's own limits and the controller's endpoint, cluster and profile checks do refuse (observed). Range-check your own inputs.

Message text differs between releases

Observed on 4.3.0 and present on 4.2.1: the same refusal reads "SendZdoBind invalid srcEndpoint: 0" on one and "CmdZdoBind invalid srcEndpoint: 0" on the other. Test the first return value.

lastMessageAge is not live

Observed. It stayed the same across reads more than an hour apart, and did not move on a device that reported every two seconds. Use your own receive times for liveness.

Discovery has two numberings

GetSysDeviceDiscovery reports a bit value, where DONE is 64 (observed). The event OnSysDeviceDiscoveryEvent reports a step number, where DONE is 21 (inferred). Compare names, not numbers.

OS 4.2.1 handles less incoming traffic

Inferred. On OS 4.2.1 the object has no OnZclReceivedEvent and appears to ignore incoming frames from every cluster except Poll Control, so the takeover of OnZigbeePacketIn described above cannot happen there.

Integration recipe

Bind two clusters to the controller one at a time, then read the binding table back page by page. It uses the names present on both OS 4.2.1 and OS 4.3.0; on 4.3.0 they are the same functions as SendZdoBind, SendZdoGetBindTable, OnZdoSentStatus, OnZdoBindResponse and OnZdoGetBindTableResponse. It was not run on 4.2.1. Leaving dstEndpoint out was observed only on 4.3.0; on 4.2.1 pass it. Helper names are placeholders.

-- 1. The driver's only ZigbeeV2 object. Never create another, even for a
--    one-off getter: collecting it would unregister this device.
local zv2

-- 2. One bind in flight, because a bind response carries no commandId.
local pending = {}   -- { { cluster = n, ep = n }, ... }
local current        -- the bind in flight, with its commandId
local guard          -- timer for a device that never answers

local function finishCurrent()
  if guard then guard:Cancel() end
  current, guard = nil, nil
end

local function readBindTable(startIndex)
  zv2:CmdZdoGetBind(startIndex)
end

-- 3. Send the next bind; when none are left, read the table back.
local function sendNextBind()
  if current then return end
  current = table.remove(pending, 1)
  if not current then return readBindTable(0) end
  local id, err = zv2:CmdZdoBind(current.cluster, current.ep)
  if id == 0 then
    print("bind refused: " .. err)
    finishCurrent()
    return sendNextBind()
  end
  current.id = id
  guard = C4:SetTimer(15000, function()
    print(string.format("no answer for cluster 0x%04X", current.cluster))
    finishCurrent()
    sendNextBind()
  end)
end

-- 4. 61445 and 5 are progress, 0 means sent, anything else ends the bind.
--    61447 on OS 4.2.1: retry with an explicit dstEndpoint.
local function onSent(commandId, status, statusName)
  if not current or commandId ~= current.id then return end
  if status ~= 0 and status ~= 5 and status ~= 61445 then
    print("bind not sent: " .. statusName)
    finishCurrent()
    sendNextBind()
  end
end

-- 5. The device's answer to the bind in flight.
local function onBind(zdoStatus, zdoStatusName)
  if not current then return end
  print("bind answered: " .. zdoStatusName)
  finishCurrent()
  sendNextBind()
end

-- 6. Pages of up to three records; ask again until entries is reached.
local function onTable(page)
  if page.statusInt ~= 0 then return end
  for _, b in ipairs(page.binds) do
    print(string.format("0x%04X ep %d -> %s ep %d",
      b.clusterId, b.srcEndpoint, b.dstDeviceEuid, b.dstEndpoint))
  end
  local nextIndex = page.startIndex + #page.binds
  if #page.binds > 0 and nextIndex < page.entries then
    readBindTable(nextIndex)
  end
end

-- 7. Wire it up once the driver is running: callbacks, Init, then work.
function OnDriverLateInit()
  if not C4.ZigbeeV2 then return end
  zv2 = C4:ZigbeeV2()
  zv2:CmdZdoExecStatusCb(onSent)
  zv2:ResZdoBindCb(onBind)
  zv2:ResZdoGetBindCb(onTable)
  zv2:Init()
  pending = { { cluster = 0x0006, ep = 1 }, { cluster = 0x0702, ep = 1 } }
  sendNextBind()
end