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

ZCL Foundation Commands

Foundation commands are the global command set shared by every ZCL cluster. They handle attribute reading, writing, reporting, discovery, and default responses. In zigbee-rs, the runtime processes these automatically — your cluster code rarely needs to touch them directly.

All foundation types live in zigbee_zcl::foundation.

Command Overview

IDCommandDirectionResponse ID
0x00Read AttributesClient → Server0x01
0x01Read Attributes ResponseServer → Client
0x02Write AttributesClient → Server0x04
0x03Write Attributes UndividedClient → Server0x04
0x04Write Attributes ResponseServer → Client
0x05Write Attributes No ResponseClient → Server
0x06Configure ReportingClient → Server0x07
0x07Configure Reporting ResponseServer → Client
0x08Read Reporting ConfigurationClient → Server0x09
0x09Read Reporting Configuration ResponseServer → Client
0x0AReport AttributesServer → Client
0x0BDefault ResponseEither
0x0CDiscover AttributesClient → Server0x0D
0x0DDiscover Attributes ResponseServer → Client
0x11Discover Commands ReceivedClient → Server0x12
0x13Discover Commands GeneratedClient → Server0x14
0x15Discover Attributes ExtendedClient → Server0x16

These are defined as a Rust enum:

#![allow(unused)]
fn main() {
pub enum FoundationCommandId {
    ReadAttributes           = 0x00,
    ReadAttributesResponse   = 0x01,
    WriteAttributes          = 0x02,
    WriteAttributesUndivided = 0x03,
    WriteAttributesResponse  = 0x04,
    WriteAttributesNoResponse = 0x05,
    ConfigureReporting       = 0x06,
    ConfigureReportingResponse = 0x07,
    ReadReportingConfig      = 0x08,
    ReadReportingConfigResponse = 0x09,
    ReportAttributes         = 0x0A,
    DefaultResponse          = 0x0B,
    DiscoverAttributes       = 0x0C,
    DiscoverAttributesResponse = 0x0D,
    DiscoverCommandsReceived = 0x11,
    DiscoverCommandsReceivedResponse = 0x12,
    DiscoverCommandsGenerated = 0x13,
    DiscoverCommandsGeneratedResponse = 0x14,
    DiscoverAttributesExtended = 0x15,
    DiscoverAttributesExtendedResponse = 0x16,
}
}

Read Attributes (0x00 / 0x01)

The most common foundation command. A coordinator or binding partner reads attribute values from a cluster.

Request — a list of AttributeIds:

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::read_attributes::*;

let req = ReadAttributesRequest::parse(&payload)?;
// req.attributes: Vec<AttributeId, 16>
}

Processing — the runtime calls process_read_dyn() automatically:

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::read_attributes::process_read_dyn;

let response = process_read_dyn(cluster.attributes(), &request);
// Each record: { id, status, data_type, value }
}

Each ReadAttributeRecord in the response contains:

  • id — the attribute ID that was requested
  • statusSuccess, UnsupportedAttribute, or WriteOnly
  • data_type / value — present only when status == Success

Write Attributes (0x02 / 0x04)

Writes one or more attributes. The runtime enforces access control (read-only attributes are rejected) and type checking (mismatched data types are rejected).

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::write_attributes::*;

let req = WriteAttributesRequest::parse(&payload)?;
let resp = process_write_dyn(cluster.attributes_mut(), &req);
// resp.records: Vec<WriteAttributeStatusRecord, 16>
}

Write Attributes Undivided (0x03) provides all-or-nothing semantics — if any single attribute write would fail, none are applied:

#![allow(unused)]
fn main() {
let resp = process_write_undivided_dyn(cluster.attributes_mut(), &req);
}

Write Attributes No Response (0x05) silently writes without sending a response frame:

#![allow(unused)]
fn main() {
process_write_no_response_dyn(cluster.attributes_mut(), &req);
}

Per the ZCL spec, if all writes succeed the response is a single byte 0x00 (Success). Only failed attributes appear individually in the response.


Configure Reporting (0x06 / 0x07)

Configures periodic and change-triggered attribute reports. The ReportingEngine stores these configurations and decides when to generate ReportAttributes (0x0A) frames.

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::reporting::*;

let req = ConfigureReportingRequest::parse(&payload)?;
for config in &req.configs {
    engine.configure_for_cluster(endpoint, cluster_id, config.clone())?;
}
}

Each ReportingConfig contains:

FieldDescription
directionSend (0x00) or Receive (0x01)
attribute_idWhich attribute to report
data_typeZCL data type of the attribute
min_intervalMinimum seconds between reports
max_intervalMaximum seconds between reports (0xFFFF = disable periodic)
reportable_changeMinimum value change to trigger report (analog types only)

Report Attributes (0x0A)

Sent by the server when a configured report triggers. The ReportingEngine handles this:

#![allow(unused)]
fn main() {
// In the main loop, advance timers:
engine.tick(elapsed_seconds);

// Then check each cluster for due reports:
let mut reports = heapless::Vec::new();
engine.check_and_collect_dyn(
    endpoint, cluster_id, cluster.attributes(), &mut reports,
);
if !reports.is_empty() {
    let payload = ReportAttributes { reports };
    // Send payload as ZCL frame with command ID 0x0A
}
}

The engine tracks per-attribute state:

  • Elapsed time since last report
  • Last reported value for change detection
  • For analog types: checks if change exceeds the configured threshold
  • For discrete types: any value change triggers a report

Default Response (0x0B)

Sent in reply to any command that lacks a cluster-specific response, unless the sender set the “disable default response” flag.

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::default_response::DefaultResponse;

let dr = DefaultResponse {
    command_id: 0x00,           // The command this responds to
    status: ZclStatus::Success, // Result
};
let mut buf = [0u8; 2];
dr.serialize(&mut buf);
}

The runtime generates Default Responses automatically when handle_command() returns Ok(empty_vec) or Err(status).


Discover Attributes (0x0C / 0x0D)

Lets a client enumerate which attributes a cluster supports, starting from a given attribute ID.

#![allow(unused)]
fn main() {
use zigbee_zcl::foundation::discover::*;

let req = DiscoverAttributesRequest::parse(&payload)?;
let resp = process_discover_dyn(cluster.attributes(), &req);
// resp.complete: bool (true = all attributes returned)
// resp.attributes: Vec<DiscoverAttributeInfo, 16>
//   each: { id: AttributeId, data_type: ZclDataType }
}

Discover Attributes Extended (0x15 / 0x16)

Like Discover Attributes, but also returns access control flags per attribute:

#![allow(unused)]
fn main() {
let resp = process_discover_extended_dyn(cluster.attributes(), &req);
// resp.attributes: Vec<DiscoverAttributeExtendedInfo, 16>
//   each: { id, data_type, access_control }
//   access_control bits: 0x01=readable, 0x02=writable, 0x04=reportable
}

Discover Commands (0x11–0x14)

Enumerates which cluster-specific commands a cluster supports. Each cluster implements received_commands() and generated_commands():

#![allow(unused)]
fn main() {
// The Cluster trait provides:
fn received_commands(&self) -> heapless::Vec<u8, 32>;
fn generated_commands(&self) -> heapless::Vec<u8, 32>;

// Processing:
let resp = process_discover_commands(
    &cluster.received_commands(), req.start_command_id, req.max_results,
);
}

How the Runtime Handles Foundation Commands

You almost never handle foundation commands yourself. The zigbee-rs runtime:

  1. Parses the incoming ZCL frame and checks frame_control.frame_type
  2. Foundation frames (frame_type = 0b00) are dispatched to the appropriate handler:
    • Read Attributes → process_read_dyn()
    • Write Attributes → process_write_dyn() or process_write_undivided_dyn()
    • Configure Reporting → ReportingEngine::configure_for_cluster()
    • Discover → process_discover_dyn() / process_discover_extended_dyn()
  3. Cluster-specific frames (frame_type = 0b01) are dispatched to your cluster’s handle_command()
  4. Reporting is driven by the event loop calling engine.tick() + check_and_collect_dyn() periodically

This means your Cluster implementation only needs to:

  • Register attributes with correct AttributeAccess modes
  • Implement handle_command() for cluster-specific commands
  • Implement received_commands() / generated_commands() for discovery