Guide: Adding a New Zigbee Cluster to the CDMB Plugin
Introduction
This guide walks you through adding support for a new Zigbee cluster to the IotManagedIntegrationsHubSDK-Zigbee-CDMB-Plugin package. It is written for developers who are new to this project.
By the end of this guide, you will understand:
- What the plugin does and how it is structured
- The data flow from a Zigbee device to the cloud and back
- The data-driven architecture that makes most clusters require zero custom code
- Every file you need to create or modify, with concrete examples
Key difference from the Z-Wave CDMB plugin: The Z-Wave plugin requires implementing 4 custom functions per Command Class (Send, HandleReport, Control, Map). The Zigbee plugin uses a data-driven architecture — most clusters need only static data definitions (attribute and command mappings). The framework handles all read/write/command/report logic generically. Custom handler functions are only needed for non-standard payloads, custom read logic, or cluster-specific ZCL events.
Background Concepts
What is CDMB?
CDMB stands for Common Data Model Bridge. It is the layer that translates between protocol-specific device communication (Z-Wave, Zigbee, Matter) and a unified cloud representation. This plugin is the Zigbee implementation of that bridge.
What is a Zigbee Cluster?
A Zigbee cluster is a standardized set of attributes and commands defined by the Zigbee Cluster Library (ZCL) specification. Think of it like an interface or API contract. For example:
- On/Off (0x0006) — on/off switches and lights
- Level Control (0x0008) — dimmable lights, adjustable fan speeds
- Door Lock (0x0101) — door locks
- Thermostat (0x0201) — HVAC thermostats
Each cluster defines:
- Attributes — state values the device holds (e.g., "OnOff", "CurrentLevel")
- Commands — actions you can send to the device (e.g., "Off", "On", "MoveToLevel")
- Reports — periodic or change-driven attribute updates from the device
What is a Matter Cluster?
The cloud side uses the Matter cluster model as its universal language for talking to devices — regardless of whether the device is Z-Wave, Zigbee, or native Matter. A cluster is a logical grouping of attributes, commands, and events. Why Matter clusters? Matter is an industry-standard smart home protocol. By using Matter clusters as the cloud-side representation, the cloud doesn't need to know or care what protocol a device actually uses. It just speaks "cluster language," and the CDMB plugin handles the translation. Real-world analogy: Think of clusters as a universal remote control. The cloud always presses the same "On/Off" button (the On/Off cluster), and the CDMB plugin translates that into the correct Zigbee ZCL command for the specific device. Here are some examples of how Zigbee clusters map to Matter/cloud clusters:
| Zigbee Cluster | Zigbee ID | Matter/Cloud Cluster | Matter ID | What It Controls |
|---|---|---|---|---|
| On/Off | 0x0006 | On/Off | 0x0006 | Turning devices on or off |
| --- | --- | --- | --- | --- |
| Level Control | 0x0008 | Level Control | 0x0008 | Dimming lights, adjusting fan speed |
| Door Lock | 0x0101 | Door Lock | 0x0101 | Locking and unlocking doors |
| Power Configuration | 0x0001 | Power Source | 0x002F | Reporting battery level |
| Thermostat | 0x0201 | Thermostat | 0x0201 | HVAC control |
| IAS Zone | 0x0500 | Boolean State | 0x0045 | Contact/motion sensors |
Key relationships to understand:
- Direct mapping — Most Zigbee clusters map directly to a Matter cluster with the same or similar ID (e.g., On/Off 0x0006 → On/Off 0x0006)
- ID translation — Some clusters have different IDs between Zigbee and Matter (e.g., Zigbee Power Configuration 0x0001 → Matter Power Source 0x002F)
- Attribute ID translation — Even when cluster IDs match, individual attribute IDs may differ between Zigbee ZCL and Matter (e.g., Zigbee "BatteryPercentageRemaining" attribute 0x0021 → Matter attribute 0x000C)
The core job of this plugin is to map Zigbee ZCL clusters to Matter clusters so the cloud can interact with Zigbee devices using a protocol-agnostic model.
How Data Flows
Cloud (Matter cluster commands/attributes)
↕
CDMB Plugin (this code — translates between Matter and ZCL)
↕
Zigbee Shim Layer (iotmi_zigbee/ — abstracts the Zigbee adapter)
↕
Zigbee Radio (physical device communication)
There are two main data flows: Control path (cloud → device):
- Cloud sends a cluster command (e.g., "turn on the light")
zigbeeParseRequest()parses the JSON into azigbeeTask_tzigbeeExecuteTask()looks up the cluster inclusterMapTable[]- The framework dispatches to the appropriate handler:
- Write (0xff01):
zigbeeAttributesWriteHandler()— translates Matter attribute IDs to Zigbee IDs using the cluster's attribute table, builds a ZCL Write Attributes frame - Read (0xff02):
zigbeeAttributesReadHandler()(or customreadHandler) — builds a ZCL Read Attributes frame - Command:
zigbeeClusterCommandHandler()(or customcommandHandler) — translates Matter command ID to Zigbee command ID using the cluster's command table, builds a ZCL cluster-specific command frame
- Write (0xff01):
Report path (device → cloud):
- Zigbee device sends a ZCL report or response frame
iotshZigbeeIncomingMessageHandler()looks up the cluster by Zigbee cluster ID- For global frames (attribute reports/responses):
zigbeeGlobalEventHandler()translates Zigbee attribute IDs back to Matter IDs using the cluster's attribute table - For cluster-specific frames: dispatches to
clusterSpecificEventHandlerif defined - The translated attributes are sent to the cloud as JSON
Why this matters: Because the framework handles all the ZCL frame construction and parsing generically, most clusters only need to provide the data tables that map between Matter and Zigbee IDs. No custom code required.
Project Structure
IotManagedIntegrationsHubSDK-Zigbee-CDMB-Plugin/
├── CMakeLists.txt # Build config
├── include/
│ ├── iotmi_cdmb_zigbee_common_types.h # Core data structures and constants
│ ├── iotmi_cdmb_zigbee_core.h # Public API (init, task handler, callbacks)
│ ├── iotmi_cdmb_zigbee_events.h # Incoming ZCL event handling
│ ├── iotmi_cdmb_zigbee_operations.h # Outgoing ZCL command handling
│ ├── iotmi_cdmb_zigbee_reporting.h # ZCL reporting configuration
│ ├── iotmi_cdmb_zigbee_onboarding.h # Device onboarding / capability reports
│ ├── iotmi_cdmb_zigbee_utils.h # Lookup utilities
│ └── iotmi_zigbee/ # Zigbee shim layer headers
│ ├── iotmi_zigbee_types.h # Node, endpoint, cluster structs
│ ├── iotmi_zigbee_zcl.h # ZCL frame structures
│ └── iotmi_zigbee_zcl_id.h # ZCL cluster/attribute/command ID definitions
├── src/
│ ├── iotmi_cdmb_zigbee_core.c # Main service — dispatch table, task execution
│ ├── iotmi_cdmb_zigbee_events.c # Incoming ZCL message handling
│ ├── iotmi_cdmb_zigbee_operations.c # Outgoing ZCL command construction
│ ├── iotmi_cdmb_zigbee_reporting.c # Reporting configuration table
│ ├── iotmi_cdmb_zigbee_onboarding.c # Device type table, capability reports
│ ├── iotmi_cdmb_zigbee_utils.c # Lookup functions
│ └── clusters/ # ★ One .c file per cluster (data definitions)
│ ├── iotmi_cdmb_zigbee_binary_switch.c
│ ├── iotmi_cdmb_zigbee_level_control.c
│ ├── iotmi_cdmb_zigbee_thermostat.c
│ ├── iotmi_cdmb_zigbee_fan_control.c
│ ├── iotmi_cdmb_zigbee_color_control.c
│ ├── iotmi_cdmb_zigbee_temperature_measurement.c
│ ├── iotmi_cdmb_zigbee_door_lock.c
│ ├── iotmi_cdmb_zigbee_window_covering.c
│ ├── iotmi_cdmb_zigbee_sensors.c # IAS Zone, Occupancy
│ ├── iotmi_cdmb_power_configuration.c
│ └── iotmi_cdmb_zigbee_basic_information.c
├── test/unit/ # GoogleTest unit tests
└── external/ # External dependencies (cdmb_sim, cJSON)
Key Data Structures
Before writing code, understand these core structs (defined in include/iotmi_cdmb_zigbee_common_types.h):
zigbeeTask_t
Represents a single request from the cloud. Created by zigbeeParseRequest() from the incoming JSON.
typedef struct zigbeeTask {
uint16_t nodeId; // Target Zigbee node (short address)
uint8_t ep; // Target endpoint on the node
uint16_t clusterId; // Matter cluster ID for this operation
uint16_t commandId; // Command ID (0xff01=write, 0xff02=read, or cluster command)
cJSON *params; // Command parameters as JSON
char *traceId; // For request tracing
char *commandRecvTime; // Timestamp when command was received
zigbeeTaskType_t type; // REQ_SET(0), REQ_GET(1), or REQ_COMMAND(2)
} zigbeeTask_t;
clusterMapTableEntry_t
The central dispatch table entry. Maps a Matter/cloud cluster to its Zigbee implementation.
typedef struct clusterMapTableEntry {
const char *name; // AWS cluster name (e.g., "aws.OnOff")
uint16_t matterId; // Matter cluster ID (e.g., 0x0006)
commandHandler_t commandHandler; // Custom command handler (NULL = use generic)
readHandler_t readHandler; // Custom read handler (NULL = use generic)
zigbeeClusterInfo_t *clusterInfo; // Pointer to cluster data definitions
} clusterMapTableEntry_t;
Key insight: When commandHandler and readHandler are NULL, the framework uses its built-in generic handlers that work entirely from the data in clusterInfo. This is why most clusters need zero custom code.
zigbeeClusterInfo_t
The complete definition of a cluster's attributes, commands, and event handling. This is the struct you define in your cluster data file.
typedef struct zigbeeClusterInfo {
uint16_t zigbeeClusterId; // ZCL cluster ID (e.g., 0x0006)
uint8_t commandCount; // Number of commands
zigbeeCommandInfo_t commands[MAX_COMMAND_COUNT]; // Up to 20 commands
uint16_t attributeCount; // Number of attributes
zigbeeAttributeZclInfo_t attributes[MAX_ATTRIBUTE_COUNT]; // Up to 55 attributes
zigbeeClusterSpecificEventHandler_t clusterSpecificEventHandler; // Optional (NULL for most)
} zigbeeClusterInfo_t;
zigbeeAttributeZclInfo_t
Maps a single attribute between Matter and Zigbee. The framework uses this for bidirectional translation.
typedef struct zigbeeAttributeZclInfo {
uint16_t matterId; // Matter attribute ID (used in cloud JSON)
uint16_t zigbeeId; // Zigbee ZCL attribute ID (on the wire)
uint8_t type; // ZCL data type (e.g., BOOLEAN=0x10, UINT8=0x20)
zigbeeAttributeEnumMapInfo_t *enumMapInfo; // Optional enum value mapping (NULL if not enum)
zigbeeBitMapHandler_t bitmapHandler; // Optional bitmap decoder (NULL if not bitmap)
} zigbeeAttributeZclInfo_t;
zigbeeCommandInfo_t
Maps a single cluster command between Matter and Zigbee.
typedef struct zigbeeCommandInfo {
uint8_t matterId; // Matter command ID
uint8_t zigbeeId; // Zigbee ZCL command ID
uint8_t inputCount; // Number of input parameters
zigbeeCommandInput_t *inputs; // Array of input parameter definitions
commandPayloadHandler_t payloadHandler; // Optional custom payload builder (NULL = generic)
} zigbeeCommandInfo_t;
zigbeeCommandInput_t
Defines a single input parameter for a cluster command.
typedef struct zigbeeCommandInput {
char *inputName; // JSON key name (e.g., "0", "1")
uint8_t inputType; // ZCL data type
zigbeeAttributeEnumMapInfo_t *enumMapInfo; // Optional enum mapping (NULL if not enum)
} zigbeeCommandInput_t;
zigbeeAttributeEnumMapInfo_t / zigbeeAttributeEnumMapElement_t
Translates between string names and numeric ZCL values for enum-type attributes and command inputs.
typedef struct zigbeeAttributeEnumMapElement {
char *elementName; // String name (e.g., "On", "Off", "Locked")
uint16_t elementValue; // Numeric ZCL value (e.g., 0, 1, 2)
} zigbeeAttributeEnumMapElement_t;
typedef struct zigbeeAttributeEnumMapInfo {
uint16_t elementCount; // Number of elements
zigbeeAttributeEnumMapElement_t *enumMap; // Array of name-value pairs
} zigbeeAttributeEnumMapInfo_t;
Step-by-Step: Adding a New Cluster
We use a hypothetical Pressure Measurement cluster as a running example. This is a simple, data-only cluster — the most common case. Adapt the names and logic to your actual cluster.
Step 1: Create the Cluster Data File
New file: src/clusters/iotmi_cdmb_zigbee_pressure_measurement.c
This is where you define the zigbeeClusterInfo_t struct that describes your cluster's attributes and commands. For most clusters, this is the only file you create — no header file needed.
1a. Include the required headers:
1b. Define enum maps (if your cluster has enum-type attributes or command inputs):
For each enum-type attribute, define an array of name-value pairs and wrap it in a zigbeeAttributeEnumMapInfo_t:
/* Not needed for Pressure Measurement (no enum attributes) */
/* But here's what it looks like for reference (from On/Off cluster): */
/*
static zigbeeAttributeEnumMapElement_t onOffEnumMap[] = {
{ "Off", 0 },
{ "On", 1 },
};
static zigbeeAttributeEnumMapInfo_t onOffEnumMapInfo = {
.elementCount = 2,
.enumMap = onOffEnumMap,
};
*/
1c. Define command inputs (if your cluster has commands with parameters):
/* Not needed for Pressure Measurement (read-only, no commands) */
/* But here's what it looks like for reference (from Level Control): */
/*
static zigbeeCommandInput_t moveToLevelCmdInputs[] = {
{ .inputName = "0", .inputType = IOTMI_ZIGBEE_ZCL_INT8U_ATTRIBUTE_TYPE, .enumMapInfo = NULL },
{ .inputName = "1", .inputType = IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE, .enumMapInfo = NULL },
};
*/
1d. Define the zigbeeClusterInfo_t struct:
zigbeeClusterInfo_t zigbeePressureMeasurementInfo = {
.zigbeeClusterId = 0x0403, /* ZCL Pressure Measurement cluster ID */
.attributeCount = 3,
.attributes = {
[0] = {
.matterId = 0x0000, /* Matter: MeasuredValue */
.zigbeeId = 0x0000, /* ZCL: MeasuredValue */
.type = IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE,
.enumMapInfo = NULL,
.bitmapHandler = NULL,
},
[1] = {
.matterId = 0x0001, /* Matter: MinMeasuredValue */
.zigbeeId = 0x0001, /* ZCL: MinMeasuredValue */
.type = IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE,
.enumMapInfo = NULL,
.bitmapHandler = NULL,
},
[2] = {
.matterId = 0x0002, /* Matter: MaxMeasuredValue */
.zigbeeId = 0x0002, /* ZCL: MaxMeasuredValue */
.type = IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE,
.enumMapInfo = NULL,
.bitmapHandler = NULL,
},
},
.commandCount = 0,
.commands = {},
.clusterSpecificEventHandler = NULL,
};
How to fill in each field:
| Field | Where to find the value |
|---|---|
zigbeeClusterId |
ZCL specification or iotmi_zigbee_zcl_id.h |
| --- | --- |
attributes[].matterId |
Matter specification or cloud data model |
attributes[].zigbeeId |
ZCL specification or iotmi_zigbee_zcl_id.h |
attributes[].type |
ZCL specification; use constants from iotmiZigbee_zclAttributeType_t enum |
attributes[].enumMapInfo |
Define if the attribute is an enum type; NULL otherwise |
attributes[].bitmapHandler |
Implement if the attribute is a bitmap that needs decomposition; NULL otherwise |
commands[].matterId |
Matter specification or cloud data model |
commands[].zigbeeId |
ZCL specification |
commands[].inputs |
Define if the command takes parameters; NULL otherwise |
commands[].payloadHandler |
Implement if the command has non-standard payload format; NULL otherwise |
clusterSpecificEventHandler |
Implement if the cluster receives cluster-specific ZCL responses; NULL otherwise |
Common ZCL attribute types:
| Type | Constant | Size |
|---|---|---|
| Boolean | IOTMI_ZIGBEE_ZCL_BOOLEAN_ATTRIBUTE_TYPE (0x10) |
1 byte |
| --- | --- | --- |
| uint8 | IOTMI_ZIGBEE_ZCL_INT8U_ATTRIBUTE_TYPE (0x20) |
1 byte |
| uint16 | IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE (0x21) |
2 bytes |
| int16 | IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE (0x29) |
2 bytes |
| enum8 | IOTMI_ZIGBEE_ZCL_ENUM8_ATTRIBUTE_TYPE (0x30) |
1 byte |
| enum16 | IOTMI_ZIGBEE_ZCL_ENUM16_ATTRIBUTE_TYPE (0x31) |
2 bytes |
| bitmap8 | IOTMI_ZIGBEE_ZCL_BITMAP8_ATTRIBUTE_TYPE (0x18) |
1 byte |
| bitmap16 | IOTMI_ZIGBEE_ZCL_BITMAP16_ATTRIBUTE_TYPE (0x19) |
2 bytes |
| char string | IOTMI_ZIGBEE_ZCL_CHAR_STRING_ATTRIBUTE_TYPE (0x42) |
variable |
| octet string | IOTMI_ZIGBEE_ZCL_OCTET_STRING_ATTRIBUTE_TYPE (0x41) |
variable |
Step 2: Register the Extern Declaration in the Core File
File: src/iotmi_cdmb_zigbee_core.c (~line 56)
Add an extern declaration for your zigbeeClusterInfo_t so the dispatch table can reference it:
Add this alongside the existing extern declarations:
extern zigbeeClusterInfo_t zigbeebinarySwitchInfo;
extern zigbeeClusterInfo_t zigbeeLevelControlInfo;
// ... existing declarations ...
extern zigbeeClusterInfo_t zigbeeBasicInformationClusterInfo;
extern zigbeeClusterInfo_t zigbeePressureMeasurementInfo; /* ← ADD THIS */
Step 3: Add Entry to the Cluster Map Table
File: src/iotmi_cdmb_zigbee_core.c (~line 74)
Add an entry to clusterMapTable[]:
clusterMapTableEntry_t clusterMapTable[ CLUSTER_MAP_TABLE_SIZE ] = {
{ "aws.OnOff", 0x0006, NULL, NULL, &zigbeebinarySwitchInfo },
// ... existing entries ...
{ "aws.BasicInformationCluster", 0x0028, NULL, zigbeeBasicInformationClusterReadHandler, &zigbeeBasicInformationClusterInfo },
{ "aws.PressureMeasurement", 0x0403, NULL, NULL, &zigbeePressureMeasurementInfo }, /* ← ADD THIS */
};
Entry format: { "aws.<ClusterName>", <MatterClusterId>, <commandHandler>, <readHandler>, &<clusterInfoStruct> }
- Set
commandHandlertoNULLunless you need custom command handling - Set
readHandlertoNULLunless you need custom read handling - The
namestring must match the cloud data model's cluster name exactly
Step 4: Increment CLUSTER_MAP_TABLE_SIZE
File: include/iotmi_cdmb_zigbee_common_types.h (~line 28)
Step 5: Configure Attribute Reporting (If Needed)
Most clusters that report state changes need ZCL attribute reporting configured. Skip this step only for clusters that are purely command-driven or read-only-on-demand.
File: src/iotmi_cdmb_zigbee_reporting.c
5a. Add extern declaration (~line 24):
5b. Add entry to reportClusterConfigs[] (~line 44):
const size_t zigbeeReportClusterConfigCount = 12; /* was 11, incremented by 1 */
const zigbeeReportClusterConfig_t reportClusterConfigs[] = {
// ... existing entries ...
{&zigbeeWindowCoveringInfo, (uint16_t[]){3, 4}, 2},
{&zigbeePressureMeasurementInfo, (uint16_t[]){0}, 1}, /* ← ADD THIS */
};
Entry format: {&<clusterInfoStruct>, (uint16_t[]){<zigbeeAttrId1>, <zigbeeAttrId2>, ...}, <count>}
- The attribute IDs here are Zigbee ZCL attribute IDs (not Matter IDs)
- These are the attributes the device will periodically report
countmust match the number of attribute IDs in the array
5c. (Optional) Add custom reporting intervals:
If your cluster needs non-default reporting intervals, add a case to the switch in zigbeeConfigureReporting() (~line 130):
switch (cluster.zigbeeClusterId) {
case IOTMI_ZIGBEE_ZCL_TEMP_MEASUREMENT_CLUSTER_ID:
case IOTMI_ZIGBEE_ZCL_RELATIVE_HUMIDITY_MEASUREMENT_CLUSTER_ID:
case 0x0403: /* Pressure Measurement — same intervals as temperature */
bytesAdded = zigbeeAddAttributeToReportRequest(
..., 30, 0x0384, 100); /* min=30s, max=900s, change=100 */
break;
default:
bytesAdded = zigbeeAddAttributeToReportRequest(
..., 1, 0x0384, 0); /* min=1s, max=900s, change=0 */
}
Default intervals are: min=1s, max=900s (15 min), reportable change=0 (report on any change).
Step 6: Add Device Type (If Needed)
Only needed if your cluster introduces a new Zigbee device type that isn't already in the table. Skip this step if the device type is already supported.
File: src/iotmi_cdmb_zigbee_onboarding.c (~line 46)
deviceTypeEntry_t deviceTypeTable[DEVICE_TYPE_TABLE_SIZE] = {
// ... existing entries ...
{0x000A, "Door lock"},
{0x0305, "Pressure Sensor"}, /* ← ADD THIS (if new device type) */
};
Also update include/iotmi_cdmb_zigbee_common_types.h:
Step 7: Update Build Files
File: CMakeLists.txt (at the plugin root)
Add your new cluster source file to the add_library() or source file list:
If you have unit tests, also add them to the test CMakeLists.txt.
Step 8: Build and Verify
Build the SDK following the instructions in the SDK root README to verify your changes compile correctly.
Advanced: Custom Handlers (When Data-Only Isn't Enough)
Most clusters work with just the data definitions from Step 1. However, some clusters need custom handler functions. Here's when and how to implement them.
When You Need Custom Handlers
| Scenario | What to Implement | Example |
|---|---|---|
| Command has non-standard ZCL payload (e.g., PIN codes, octet strings) | commandPayloadHandler_t on the command |
Door Lock (PIN code in lock/unlock) |
| --- | --- | --- |
| Cluster receives cluster-specific ZCL responses (not just global attribute reports) | clusterSpecificEventHandler on the cluster info |
Door Lock (operation event status) |
| Read needs to come from a different source (not ZCL Read Attributes) | Custom readHandler on the map table entry |
Basic Information (reads from node info, not ZCL) |
| Bitmap attribute needs to be decomposed into multiple JSON fields | zigbeeBitMapHandler_t on the attribute |
Thermostat (running state bitmap) |
Custom Payload Handler
Set the payloadHandler field on a zigbeeCommandInfo_t entry when the command's ZCL payload doesn't follow the standard "sequence of typed values" format.
Signature:
zigbeeStatusCodes_t myPayloadHandler(
cJSON *params, // Input: JSON parameters from cloud
uint8_t *payload, // Output: raw ZCL payload buffer to fill
uint16_t *payloadLength // Output: length of payload written
);
Example from Door Lock (PIN code payload):
static zigbeeStatusCodes_t zigbeeDoorLockCmdPayloadHandler(
cJSON *params, uint8_t *payload, uint16_t *payloadLength)
{
/* Extract PIN code from JSON params */
cJSON *pinCodeItem = cJSON_GetObjectItem(params, "0");
if (pinCodeItem == NULL || !cJSON_IsString(pinCodeItem)) {
return ZIGBEE_STATUS_INVALID_PARAMETER;
}
char *pinCode = pinCodeItem->valuestring;
uint8_t pinCodeLength = strlen(pinCode);
/* Build ZCL payload: [length byte][PIN code bytes] */
payload[0] = pinCodeLength;
memcpy(&payload[1], pinCode, pinCodeLength);
*payloadLength = 1 + pinCodeLength;
return ZIGBEE_STATUS_OK;
}
Then reference it in the command definition:
.commands[0] = {
.matterId = 0x00,
.zigbeeId = 0x00, /* Lock Door */
.inputCount = 1,
.inputs = lockDoorCmdInputs,
.payloadHandler = zigbeeDoorLockCmdPayloadHandler, /* ← custom handler */
},
Custom Cluster-Specific Event Handler
Set the clusterSpecificEventHandler field on zigbeeClusterInfo_t when the cluster receives ZCL frames that are cluster-specific (not global Read/Report Attributes responses).
Signature:
zigbeeStatusCodes_t myEventHandler(
iotmiZigbee_cmdEvent_t *event, // The raw ZCL event from middleware
uint8_t commandIDIndex, // Index of command ID in the ZCL frame
cJSON *attributesResponse, // Output: JSON attributes to report to cloud
cJSON *cmdsResponse // Output: JSON commands response (rarely used)
);
Example from Door Lock (operation event notification):
static zigbeeStatusCodes_t zigbeeDoorLockEventHandler(
iotmiZigbee_cmdEvent_t *event, uint8_t commandIDIndex,
cJSON *attributesResponse, cJSON *cmdsResponse)
{
uint8_t *payload = event->cmd->payload;
uint8_t commandId = payload[commandIDIndex];
if (commandId == 0x01) { /* Lock Door Response */
uint8_t status = payload[commandIDIndex + 1];
/* Translate status to Matter attribute and add to attributesResponse */
cJSON_AddStringToObject(attributesResponse, "0",
status == 0 ? "Locked" : "Unlocked");
}
return ZIGBEE_STATUS_OK;
}
Then set it in the cluster info:
zigbeeClusterInfo_t zigbeeDoorLockInfo = {
.zigbeeClusterId = 0x0101,
// ... attributes and commands ...
.clusterSpecificEventHandler = zigbeeDoorLockEventHandler, /* ← custom handler */
};
Custom Read Handler
Set a custom readHandler on the clusterMapTableEntry_t when the cluster's attributes can't be read via standard ZCL Read Attributes (e.g., they come from a different source).
Signature:
Example from Basic Information (reads from cached node info instead of ZCL):
zigbeeStatusCodes_t zigbeeBasicInformationClusterReadHandler(
zigbeeTask_t *task, zigbeeClusterInfo_t *clusterInfo)
{
/* Read from cached node info instead of sending ZCL Read Attributes */
iotmiZigbee_nodeInfo_t *nodeInfo = findNodeInfo(task->nodeId);
if (nodeInfo == NULL) {
return ZIGBEE_STATUS_RT_NOT_FOUND;
}
/* Build response JSON from cached data */
// ...
return ZIGBEE_STATUS_OK;
}
Register in the map table with a non-NULL readHandler:
{ "aws.BasicInformationCluster", 0x0028, NULL, zigbeeBasicInformationClusterReadHandler, &zigbeeBasicInformationClusterInfo },
Custom Bitmap Handler
Set the bitmapHandler field on a zigbeeAttributeZclInfo_t when a bitmap attribute needs to be decomposed into multiple JSON fields.
Signature:
zigbeeStatusCodes_t myBitmapHandler(
uint32_t bitmap, // The raw bitmap value from ZCL
cJSON *attributesResponse // Output: JSON to add decomposed fields to
);
Complete Example: Simple Cluster (Binary Switch / On/Off)
This is the simplest possible cluster — a real implementation from the codebase. It has 5 attributes, 6 commands, no custom handlers.
/* src/clusters/iotmi_cdmb_zigbee_binary_switch.c */
#include <iotmi_zigbee/iotmi_zigbee_types.h>
#include "iotmi_cdmb_zigbee_common_types.h"
/* Enum map for On/Off boolean-like attributes */
static zigbeeAttributeEnumMapElement_t zigbeeBinarySwitchOnOffEnumMap[] = {
{ "Off", 0 },
{ "On", 1 },
};
static zigbeeAttributeEnumMapInfo_t zigbeeBinarySwitchOnOffEnumMapInfo = {
.elementCount = 2,
.enumMap = zigbeeBinarySwitchOnOffEnumMap,
};
/* Command inputs for OnWithTimedOff (command with parameters) */
static zigbeeCommandInput_t zigbeeBinarySwitchOnWithTimedOffCmdInputs[] = {
{ .inputName = "0", .inputType = IOTMI_ZIGBEE_ZCL_INT8U_ATTRIBUTE_TYPE, .enumMapInfo = NULL },
{ .inputName = "1", .inputType = IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE, .enumMapInfo = NULL },
{ .inputName = "2", .inputType = IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE, .enumMapInfo = NULL },
};
/* The cluster info struct — this is all you need */
zigbeeClusterInfo_t zigbeebinarySwitchInfo = {
.zigbeeClusterId = IOTMI_ZIGBEE_ZCL_ON_OFF_CLUSTER_ID, /* 0x0006 */
.attributeCount = 5,
.attributes = {
[0] = { .matterId = 0x0000, .zigbeeId = IOTMI_ZIGBEE_ZCL_ON_OFF_ATTRIBUTE_ID,
.type = IOTMI_ZIGBEE_ZCL_BOOLEAN_ATTRIBUTE_TYPE,
.enumMapInfo = &zigbeeBinarySwitchOnOffEnumMapInfo },
[1] = { .matterId = 0x4000, .zigbeeId = 0x4000,
.type = IOTMI_ZIGBEE_ZCL_BOOLEAN_ATTRIBUTE_TYPE,
.enumMapInfo = &zigbeeBinarySwitchOnOffEnumMapInfo },
[2] = { .matterId = 0x4001, .zigbeeId = 0x4001,
.type = IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE },
[3] = { .matterId = 0x4002, .zigbeeId = 0x4002,
.type = IOTMI_ZIGBEE_ZCL_INT16U_ATTRIBUTE_TYPE },
[4] = { .matterId = 0x4003, .zigbeeId = 0x4003,
.type = IOTMI_ZIGBEE_ZCL_ENUM8_ATTRIBUTE_TYPE },
},
.commandCount = 6,
.commands = {
[0] = { .matterId = 0x00, .zigbeeId = IOTMI_ZIGBEE_ZCL_OFF_COMMAND_ID,
.inputCount = 0, .inputs = NULL, .payloadHandler = NULL },
[1] = { .matterId = 0x01, .zigbeeId = IOTMI_ZIGBEE_ZCL_ON_COMMAND_ID,
.inputCount = 0, .inputs = NULL, .payloadHandler = NULL },
[2] = { .matterId = 0x02, .zigbeeId = IOTMI_ZIGBEE_ZCL_TOGGLE_COMMAND_ID,
.inputCount = 0, .inputs = NULL, .payloadHandler = NULL },
[3] = { .matterId = 0x40, .zigbeeId = 0x40,
.inputCount = 0, .inputs = NULL, .payloadHandler = NULL },
[4] = { .matterId = 0x41, .zigbeeId = 0x41,
.inputCount = 0, .inputs = NULL, .payloadHandler = NULL },
[5] = { .matterId = 0x42, .zigbeeId = 0x42,
.inputCount = 3, .inputs = zigbeeBinarySwitchOnWithTimedOffCmdInputs,
.payloadHandler = NULL },
},
.clusterSpecificEventHandler = NULL,
};
Registration in iotmi_cdmb_zigbee_core.c:
extern zigbeeClusterInfo_t zigbeebinarySwitchInfo;
// ...
{ "aws.OnOff", 0x0006, NULL, NULL, &zigbeebinarySwitchInfo },
That's it. No Send function, no HandleReport function, no Control function, no Map function. The framework handles everything.
Complete Example: Complex Cluster (Thermostat)
A more complex cluster with many attributes, enum maps, and commands with parameters — but still no custom handler functions.
/* src/clusters/iotmi_cdmb_zigbee_thermostat.c (simplified for clarity) */
#include <iotmi_zigbee/iotmi_zigbee_types.h>
#include "iotmi_cdmb_zigbee_common_types.h"
/* Enum maps for thermostat modes */
static zigbeeAttributeEnumMapElement_t zigbeeThermostatSystemModeEnumMap[] = {
{ "Off", 0x00 },
{ "Auto", 0x01 },
{ "Cool", 0x03 },
{ "Heat", 0x04 },
{ "EmergencyHeat", 0x05 },
{ "Precooling", 0x06 },
{ "FanOnly", 0x07 },
};
static zigbeeAttributeEnumMapInfo_t zigbeeThermostatSystemModeEnumMapInfo = {
.elementCount = 7,
.enumMap = zigbeeThermostatSystemModeEnumMap,
};
/* Command inputs for SetpointRaiseLower */
static zigbeeCommandInput_t zigbeeThermostatSetpointRaiseLowerCmdInputs[] = {
{ .inputName = "0", .inputType = IOTMI_ZIGBEE_ZCL_ENUM8_ATTRIBUTE_TYPE,
.enumMapInfo = NULL },
{ .inputName = "1", .inputType = IOTMI_ZIGBEE_ZCL_INT8S_ATTRIBUTE_TYPE,
.enumMapInfo = NULL },
};
zigbeeClusterInfo_t zigbeethermostatInfo = {
.zigbeeClusterId = 0x0201,
.attributeCount = 12,
.attributes = {
[0] = { .matterId = 0x0000, .zigbeeId = 0x0000,
.type = IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE }, /* LocalTemperature */
[1] = { .matterId = 0x0003, .zigbeeId = 0x0003,
.type = IOTMI_ZIGBEE_ZCL_INT16S_ATTRIBUTE_TYPE }, /* AbsMinHeatSetpoint */
/* ... more attributes ... */
[11] = { .matterId = 0x001C, .zigbeeId = 0x001C,
.type = IOTMI_ZIGBEE_ZCL_ENUM8_ATTRIBUTE_TYPE,
.enumMapInfo = &zigbeeThermostatSystemModeEnumMapInfo }, /* SystemMode */
},
.commandCount = 1,
.commands = {
[0] = { .matterId = 0x00, .zigbeeId = 0x00,
.inputCount = 2, .inputs = zigbeeThermostatSetpointRaiseLowerCmdInputs,
.payloadHandler = NULL }, /* SetpointRaiseLower */
},
.clusterSpecificEventHandler = NULL,
};
Key observations:
- 12 attributes, 1 command — still zero custom code
- Enum attributes use
enumMapInfoto translate between string names and ZCL values - The framework automatically handles reading all 12 attributes, writing writable ones, and executing the SetpointRaiseLower command
Registration Checklist
When adding a new cluster, you must update these files. Use this checklist:
| # | File | What to Add | Required? |
|---|---|---|---|
| 1 | src/clusters/iotmi_cdmb_zigbee_<name>.c |
New cluster data file with zigbeeClusterInfo_t |
Always |
| --- | --- | --- | --- |
| 2 | src/iotmi_cdmb_zigbee_core.c (~line 56) |
extern zigbeeClusterInfo_t zigbee<Name>Info; |
Always |
| 3 | src/iotmi_cdmb_zigbee_core.c (~line 74) |
Entry in clusterMapTable[] |
Always |
| 4 | include/iotmi_cdmb_zigbee_common_types.h (~line 28) |
Increment CLUSTER_MAP_TABLE_SIZE |
Always |
| 5 | src/iotmi_cdmb_zigbee_reporting.c (~line 24) |
extern zigbeeClusterInfo_t zigbee<Name>Info; |
If reporting needed |
| 6 | src/iotmi_cdmb_zigbee_reporting.c (~line 44) |
Entry in reportClusterConfigs[] + increment count |
If reporting needed |
| 7 | src/iotmi_cdmb_zigbee_onboarding.c (~line 46) |
Entry in deviceTypeTable[] |
If new device type |
| 8 | include/iotmi_cdmb_zigbee_common_types.h (~line 29) |
Increment DEVICE_TYPE_TABLE_SIZE |
If new device type |
| 9 | CMakeLists.txt |
Add .c file to source list |
Always |
Tips and Common Pitfalls
- Most clusters need zero custom code. If you find yourself writing handler functions, double-check whether the generic framework can handle your case. Only Door Lock, Basic Information, and IAS Zone have custom handlers in the current codebase.
- Attribute IDs may differ between Zigbee and Matter. Always check both specifications. For example, Zigbee Power Configuration attribute
BatteryPercentageRemainingis ZCL ID0x0021but maps to Matter Power Source attribute ID0x000C. - Command input names are string indices. Use
"0","1","2"etc. asinputNamevalues. These correspond to the parameter positions in the cloud JSON request. - Enum maps must cover all values the device might report. If a device reports an enum value not in your map, the framework will skip that attribute in the response.
- The
zigbeeReportClusterConfigCountmust match the actual array size. This is a manual count — if it's wrong, some clusters won't get reporting configured. - ZCL attribute types must match the spec exactly. The framework uses the type to determine how many bytes to read/write from the ZCL frame. A wrong type will cause data corruption.
- Use
iotmi_zigbee_zcl_id.hfor ZCL constants. This header defines cluster IDs, attribute IDs, command IDs, and data type constants. Use the named constants instead of magic numbers where available. - Test with a real device. The data tables are easy to get wrong (transposed IDs, wrong types). Always verify with actual Zigbee hardware.
- One cluster info struct per file. Follow the convention of one
.cfile per cluster insrc/clusters/. The struct is defined as a global variable (not static) so it can be referenced viaexternfrom other files.
Reference: Currently Supported Clusters
| Cluster | Zigbee ID | Maps To (Cloud) | Matter ID | Source File |
|---|---|---|---|---|
| On/Off | 0x0006 | On/Off | 0x0006 | iotmi_cdmb_zigbee_binary_switch.c |
| --- | --- | --- | --- | --- |
| Level Control | 0x0008 | Level Control | 0x0008 | iotmi_cdmb_zigbee_level_control.c |
| Power Configuration | 0x0001 | Power Source | 0x002F | iotmi_cdmb_power_configuration.c |
| IAS Zone | 0x0500 | Boolean State | 0x0045 | iotmi_cdmb_zigbee_sensors.c |
| Door Lock | 0x0101 | Door Lock | 0x0101 | iotmi_cdmb_zigbee_door_lock.c |
| Window Covering | 0x0102 | Window Covering | 0x0102 | iotmi_cdmb_zigbee_window_covering.c |
| Thermostat | 0x0201 | Thermostat | 0x0201 | iotmi_cdmb_zigbee_thermostat.c |
| Fan Control | 0x0202 | Fan Control | 0x0202 | iotmi_cdmb_zigbee_fan_control.c |
| Color Control | 0x0300 | Color Control | 0x0300 | iotmi_cdmb_zigbee_color_control.c |
| Temperature Measurement | 0x0402 | Temperature Measurement | 0x0402 | iotmi_cdmb_zigbee_temperature_measurement.c |
| Humidity Measurement | 0x0405 | Humidity Measurement | 0x0405 | iotmi_cdmb_zigbee_sensors.c |
| Occupancy Sensing | 0x0406 | Occupancy Sensing | 0x0406 | iotmi_cdmb_zigbee_sensors.c |
| Basic Information | 0x0000 | Basic Information | 0x0028 | iotmi_cdmb_zigbee_basic_information.c |
Clusters with Custom Handlers
| Cluster | Custom Handler | Why |
|---|---|---|
| Door Lock | commandPayloadHandler + clusterSpecificEventHandler |
PIN code payloads, lock operation event responses |
| --- | --- | --- |
| Basic Information | Custom readHandler |
Reads from cached node info, not ZCL Read Attributes |
| IAS Zone | clusterSpecificEventHandler |
Zone status change notifications are cluster-specific frames |