Zigbee (zigbee v0.4.0)

Copy Markdown View Source

A from-scratch, pure-Elixir Zigbee stack.

This module is the backend-agnostic public API. You open a chip backend (which implements Zigbee.Adapter) and then drive it through these functions. The same calls work no matter which radio is underneath.

Quick start

Open the radio, form a coordinator network, then pair and read a sensor:

# 1. open the dongle (Silicon Labs EmberZNet backend) and become its subscriber
{:ok, zb} = Zigbee.start_link(Zigbee.EZSP.Adapter, device: "/dev/ttyACM0", speed: 460_800)
:ok = Zigbee.subscribe(zb, self())

# 2. form a coordinator network (also registers the default HA endpoint)
{:ok, params} = Zigbee.form_network(zb, channel: 15)

# 3. open joining and wait for a device (put the device into pairing mode now)
{:ok, dev} = Zigbee.Interview.open_and_wait(zb)

# 4. interview it: enumerate endpoints, bind + configure reporting for temp/humidity
{:ok, _report} = Zigbee.Interview.run(zb, dev.node_id, dev.eui64)

# 5. watch the readings roll in (°C / %)
Zigbee.Interview.collect(zb, 60_000)
#=> [%{cluster: 0x0402, endpoint: 1, value: 21.4, unit: "°C"}, ...]

Events

The subscriber (set with subscribe/2) receives backend-neutral events:

{:zigbee, :device_joined, %{node_id: _, eui64: _}}
{:zigbee, :device_left,   %{node_id: _, eui64: _}}
{:zigbee, :message, %Zigbee.Message{}}

Zigbee.Interview consumes these for you; consume them yourself only for custom flows. See Zigbee.Interview for pairing, Zigbee.ZCL / Zigbee.ZDO for the wire codecs, and Zigbee.Adapter for writing a new radio backend.

Summary

Functions

Register an application endpoint. Rarely needed directly; form_network/2 registers the default endpoint. Must be called before the network is up.

Form a centralized (trust-center) coordinator network and return its parameters.

The coordinator's own identifier: its 64-bit IEEE 802.15.4 extended address (EUI64), the radio's permanent, globally-unique hardware address, like a MAC address. Returned as a raw 8-byte little-endian binary (wire order).

Radio/stack info (e.g. %{protocol_version: 13, stack_version: "7.5.1.0"}).

Open the network for joining for seconds (0..254; 255 = no timeout).

Re-establish the network already stored on the radio (via networkInit), re-registering endpoints first. Returns {:ok, params}, or {:error, :no_network} if nothing is stored. Use this on restart (not form_network/2): forming makes a new network (new key) and orphans already-paired devices.

Re-establish the stored network, or form a fresh one if there is none. The right bring-up for a long-running coordinator: rejoins existing devices when possible, forms only on first run. See "Persistence & restart" in the README.

Remove (unpair) a paired device: instruct it to leave the network and drop it from the coordinator. node_id is the device's 16-bit network address and eui64 its raw 8-byte little-endian IEEE address (both known from the join / interview, e.g. the %{node_id, eui64} from Zigbee.Interview.open_and_wait/3).

Reset the coordinator: leave / tear down the current network, clearing the network state stored on the radio. The next bring-up must form_network/2.

Send a direct APS unicast to node_id and return {:ok, aps_seq}. payload is a raw APS payload. Build it with Zigbee.ZCL (on an application profile like 0x0104) or Zigbee.ZDO (on profile 0x0000). opts: :src_endpoint (default 1). Zigbee.Interview uses this under the hood.

Start a radio backend (a module implementing Zigbee.Adapter, e.g. Zigbee.EZSP.Adapter) and wrap it in a handle used by every other function here.

Register pid (default: the caller) to receive the normalized {:zigbee, _} events. Do this before Zigbee.Interview calls so join/message events reach it.

Wrap an already-started backend process in a handle.

Functions

add_endpoint(adapter, endpoint, profile, device_id, in_clusters, out_clusters)

Register an application endpoint. Rarely needed directly; form_network/2 registers the default endpoint. Must be called before the network is up.

form_network(adapter, opts \\ [])

Form a centralized (trust-center) coordinator network and return its parameters.

Options (all optional): :channel (11..26, default 15), :pan_id, :extended_pan_id (8 bytes), :tx_power (dBm), :network_key (16 bytes), :tc_link_key (16 bytes, the trust-center link-key derivation master; random by default), :endpoints (:default registers HA endpoint 1, :none, or a list of {endpoint, profile, device_id, in_clusters, out_clusters}), and :indirect_transmission_timeout (see below). Endpoints are registered here because they must exist before the network comes up.

:indirect_transmission_timeout

Milliseconds the coordinator buffers a unicast for a sleepy end device to collect on its next poll before discarding it (0..65535, default 7680 — the Zigbee MAC spec's macTransactionPersistenceTime). Raise it so buffered frames (attribute reads, binds, configure-reporting, leaves) survive longer poll gaps on very sleepy devices, at the cost of holding NCP packet buffers longer. It's volatile NCP config, re-applied on every form_network/2 and reestablish_network/2, so pass it on both.

identifier(adapter)

The coordinator's own identifier: its 64-bit IEEE 802.15.4 extended address (EUI64), the radio's permanent, globally-unique hardware address, like a MAC address. Returned as a raw 8-byte little-endian binary (wire order).

Unlike a node's 16-bit network address (the coordinator's is always 0x0000), the identifier never changes. Devices bind their clusters to it, so it's the stable identity that lets reestablish_network/2 bring paired devices back after a restart with no re-pairing.

info(adapter)

Radio/stack info (e.g. %{protocol_version: 13, stack_version: "7.5.1.0"}).

permit_joining(adapter, seconds \\ 180)

Open the network for joining for seconds (0..254; 255 = no timeout).

reestablish_network(adapter, opts \\ [])

Re-establish the network already stored on the radio (via networkInit), re-registering endpoints first. Returns {:ok, params}, or {:error, :no_network} if nothing is stored. Use this on restart (not form_network/2): forming makes a new network (new key) and orphans already-paired devices.

Accepts the same volatile-config options as form_network/2 (e.g. :endpoints, :indirect_transmission_timeout), which are re-applied to the NCP on each restart.

reestablish_or_form_network(handle, opts \\ [])

Re-establish the stored network, or form a fresh one if there is none. The right bring-up for a long-running coordinator: rejoins existing devices when possible, forms only on first run. See "Persistence & restart" in the README.

remove_device(adapter, node_id, eui64)

Remove (unpair) a paired device: instruct it to leave the network and drop it from the coordinator. node_id is the device's 16-bit network address and eui64 its raw 8-byte little-endian IEEE address (both known from the join / interview, e.g. the %{node_id, eui64} from Zigbee.Interview.open_and_wait/3).

Returns :ok once the radio accepts the request. This is coordinator-driven and authenticated with the trust-center link key, so it's more robust than an app-level leave; the device's actual departure arrives asynchronously as a {:zigbee, :device_left, %{node_id: _, eui64: _}} event to the subscriber. A device that is offline when removed is dropped from the coordinator's tables and will not be readmitted with its old key, but won't leave over the air until it is reachable again.

reset_network(adapter)

Reset the coordinator: leave / tear down the current network, clearing the network state stored on the radio. The next bring-up must form_network/2.

send_aps(adapter, node_id, profile, cluster, dst_endpoint, payload, opts \\ [])

Send a direct APS unicast to node_id and return {:ok, aps_seq}. payload is a raw APS payload. Build it with Zigbee.ZCL (on an application profile like 0x0104) or Zigbee.ZDO (on profile 0x0000). opts: :src_endpoint (default 1). Zigbee.Interview uses this under the hood.

start_link(backend, opts \\ [])

@spec start_link(
  module(),
  keyword()
) :: {:ok, Zigbee.Adapter.t()} | {:error, term()}

Start a radio backend (a module implementing Zigbee.Adapter, e.g. Zigbee.EZSP.Adapter) and wrap it in a handle used by every other function here.

opts are passed to the backend. For Zigbee.EZSP.Adapter: :device (serial port) and :speed (baud, 460_800 for the ZBT-2).

subscribe(adapter, pid \\ self())

Register pid (default: the caller) to receive the normalized {:zigbee, _} events. Do this before Zigbee.Interview calls so join/message events reach it.

wrap(backend, ref)

Wrap an already-started backend process in a handle.