Zigbee (zigbee v0.1.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, :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.

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), 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.

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.