Zigbee (zigbee v0.3.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), and :endpoints (:default registers HA endpoint 1, :none, or a list of {endpoint, profile, device_id, in_clusters, out_clusters}). Endpoints are registered here because they must exist before the network comes up.

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.

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.