Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Device Builder

Every zigbee-rs application starts the same way: you describe what your device is, and the builder assembles the full Zigbee stack for you. The DeviceBuilder pattern lets you configure addresses, channels, endpoints, clusters, power mode, and device metadata in a single fluent chain — then call .build() to get a ready-to-run ZigbeeDevice.

Creating a Builder

The entry point is always ZigbeeDevice::builder(mac), where mac is your platform’s MacDriver implementation:

use zigbee_runtime::ZigbeeDevice;
use zigbee_mac::esp::EspMac;           // or nrf::NrfMac, mock::MockMac, …

let mac = EspMac::new();
let device = ZigbeeDevice::builder(mac)
    // ... configuration ...
    .build();

Under the hood this calls DeviceBuilder::new(mac), which sets sensible defaults:

FieldDefault
device_typeDeviceType::EndDevice
channel_maskChannelMask::ALL_2_4GHZ
power_modePowerMode::AlwaysOn
manufacturer"zigbee-rs"
model"Generic"
sw_build"0.1.0"
date_code""

You only override what you need.

Configuration Methods

Device Type

Set the Zigbee role — this affects how the stack joins and routes:

use zigbee_nwk::DeviceType;

// End Device — joins a network, does not route (default)
builder.device_type(DeviceType::EndDevice)

// Router — joins a network and relays frames for others
builder.device_type(DeviceType::Router)

// Coordinator — forms a new network (PAN coordinator)
builder.device_type(DeviceType::Coordinator)

Channel Mask

Control which 2.4 GHz channels (11–26) the device scans when joining:

use zigbee_types::ChannelMask;

// Scan all channels (default)
builder.channels(ChannelMask::ALL_2_4GHZ)

// Scan only channels 15, 20, and 25
builder.channels(ChannelMask::from_channels(&[15, 20, 25]))

// Single channel — useful for testing
builder.channels(ChannelMask::single(15))

Power Mode

Determines sleep behavior. This also sets rx_on_when_idle in the MAC capability info sent during association:

use zigbee_runtime::power::PowerMode;

// Always on — router or mains-powered end device (default)
builder.power_mode(PowerMode::AlwaysOn)

// Sleepy End Device — wakes to poll periodically
builder.power_mode(PowerMode::Sleepy {
    poll_interval_ms: 5_000,     // poll parent every 5 s
    wake_duration_ms: 500,       // stay awake 500 ms after activity
})

// Deep sleep — wake only on timer (extreme battery savings)
builder.power_mode(PowerMode::DeepSleep {
    wake_interval_s: 3600,       // wake once per hour
})

When PowerMode::Sleepy or PowerMode::DeepSleep is set, the builder automatically calls nwk.set_rx_on_when_idle(false) so the coordinator knows this is a Sleepy End Device and will buffer frames for it.

Device Metadata

These values populate the Basic cluster (0x0000) attributes that Zigbee coordinators and tools like Zigbee2MQTT read during device interview:

builder
    .manufacturer("Acme Corp")       // ManufacturerName (attr 0x0004)
    .model("TempSensor-v2")          // ModelIdentifier  (attr 0x0005)
    .sw_build("1.3.0")               // SWBuildID        (attr 0x4000)
    .date_code("20260101")           // DateCode         (attr 0x0006)

Adding Endpoints

Zigbee devices expose functionality through endpoints (1–240). Each endpoint has a profile ID, a device ID, and a set of server/client clusters.

Use the .endpoint() method with a closure that configures the endpoint’s clusters:

builder.endpoint(
    1,        // endpoint number (1-240)
    0x0104,   // profile ID: Home Automation
    0x0302,   // device ID: Temperature Sensor
    |ep| {
        ep.cluster_server(0x0000)   // Basic
          .cluster_server(0x0001)   // Power Configuration
          .cluster_server(0x0003)   // Identify
          .cluster_server(0x0402)   // Temperature Measurement
    },
)

EndpointBuilder Methods

The closure receives an EndpointBuilder with these methods:

MethodDescription
cluster_server(id)Add a server-side cluster (you implement it)
cluster_client(id)Add a client-side cluster (you send commands)
device_version(v)Set the device version (default: 1)

Server clusters are clusters your device implements — other devices can read attributes and send commands to them. Client clusters are clusters your device sends commands to — for example, a light switch has On/Off as a client cluster.

You can add up to 16 clusters per endpoint and 8 endpoints per device.

Multiple Endpoints

Some devices expose multiple functions. For example, a multi-sensor:

builder
    .endpoint(1, 0x0104, 0x0302, |ep| {
        ep.cluster_server(0x0000)   // Basic
          .cluster_server(0x0402)   // Temperature
    })
    .endpoint(2, 0x0104, 0x0302, |ep| {
        ep.cluster_server(0x0405)   // Relative Humidity
    })
    .endpoint(3, 0x0104, 0x0402, |ep| {
        ep.cluster_server(0x0500)   // IAS Zone (contact)
    })

Using Templates

For common device types, zigbee-rs provides pre-built templates in zigbee_runtime::templates that set the correct device type, endpoint, profile, device ID, and clusters for you:

use zigbee_runtime::templates;

// Temperature sensor (endpoint 1, device ID 0x0302)
// Clusters: Basic, Power Config, Identify, Temperature Measurement
let device = templates::temperature_sensor(mac)
    .manufacturer("My Company")
    .model("TH-Sensor-01")
    .build();

Templates return a DeviceBuilder, so you can chain additional configuration after them.

Available Templates

TemplateDevice IDTypeKey Clusters
temperature_sensor0x0302EndDeviceBasic, PowerCfg, Identify, Temp
temperature_humidity_sensor0x0302EndDevice+ Relative Humidity
on_off_light0x0100RouterBasic, Identify, Groups, Scenes, On/Off
dimmable_light0x0101Router+ Level Control
color_temperature_light0x010CRouter+ Color Control
contact_sensor0x0402EndDeviceBasic, PowerCfg, Identify, IAS Zone
occupancy_sensor0x0107EndDeviceBasic, PowerCfg, Identify, Occupancy
smart_plug0x0009RouterBasic, Identify, Groups, Scenes, On/Off, Electrical Meas
thermostat0x0301RouterBasic, Identify, Groups, Thermostat, Temp

Note: Templates set the device type for you. Lights and plugs default to Router (they’re mains-powered and relay traffic). Sensors default to EndDevice.

Building the Device

Once configuration is complete, call .build() to construct the full stack:

let mut device = ZigbeeDevice::builder(mac)
    .device_type(DeviceType::EndDevice)
    .manufacturer("Acme Corp")
    .model("TempSensor-v2")
    .sw_build("1.3.0")
    .channels(ChannelMask::from_channels(&[15, 20, 25]))
    .power_mode(PowerMode::Sleepy {
        poll_interval_ms: 5_000,
        wake_duration_ms: 500,
    })
    .endpoint(1, 0x0104, 0x0302, |ep| {
        ep.cluster_server(0x0000)   // Basic
          .cluster_server(0x0001)   // Power Configuration
          .cluster_server(0x0003)   // Identify
          .cluster_server(0x0402)   // Temperature Measurement
          .cluster_server(0x0405)   // Relative Humidity
    })
    .build();

What .build() Does

The builder constructs the entire layer stack:

  1. Creates the NWK layer with the MAC driver and device type
  2. Sets rx_on_when_idle based on power mode
  3. Wraps NWK in the APS layer
  4. Wraps APS in the ZDO layer and registers all endpoint descriptors
  5. Sets the node descriptor (logical type, power descriptor)
  6. Wraps ZDO in the BDB layer for commissioning
  7. Creates the ReportingEngine for automatic attribute reporting
  8. Creates the PowerManager with the configured power mode

The result is a ZigbeeDevice<M> ready for start() and the event loop.

Complete Example

Here’s a full example of a battery-powered temperature + humidity sensor:

use zigbee_runtime::{ZigbeeDevice, ClusterRef, UserAction};
use zigbee_runtime::power::PowerMode;
use zigbee_mac::nrf::NrfMac;
use zigbee_nwk::DeviceType;
use zigbee_types::ChannelMask;

#[embassy_executor::main]
async fn main(spawner: embassy_executor::Spawner) {
    let mac = NrfMac::new(/* peripherals */);

    let mut device = ZigbeeDevice::builder(mac)
        .device_type(DeviceType::EndDevice)
        .manufacturer("Acme Corp")
        .model("TH-Sensor-01")
        .sw_build("1.3.0")
        .date_code("20260325")
        .channels(ChannelMask::ALL_2_4GHZ)
        .power_mode(PowerMode::Sleepy {
            poll_interval_ms: 7_500,
            wake_duration_ms: 500,
        })
        .endpoint(1, 0x0104, 0x0302, |ep| {
            ep.cluster_server(0x0000)   // Basic
              .cluster_server(0x0001)   // Power Configuration
              .cluster_server(0x0003)   // Identify
              .cluster_server(0x0402)   // Temperature Measurement
              .cluster_server(0x0405)   // Relative Humidity
        })
        .build();

    // Join the network
    device.user_action(UserAction::Join);

    // ... enter event loop (see Event Loop chapter)
}

What’s Next

After building, you need to:

  1. Start the event loop — call tick() and process_incoming() in a loop to drive the stack
  2. Register cluster instances — pass ClusterRef slices to tick() so the runtime can handle attribute reads/writes and send reports
  3. Persist state — call save_state(nv) after joining so the device can rejoin quickly after reboot