Skip to content

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 a zigbeeTask_t
  • zigbeeExecuteTask() looks up the cluster in clusterMapTable[]
  • 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 custom readHandler) — builds a ZCL Read Attributes frame
    • Command: zigbeeClusterCommandHandler() (or custom commandHandler) — translates Matter command ID to Zigbee command ID using the cluster's command table, builds a ZCL cluster-specific command frame

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 clusterSpecificEventHandler if 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:

#include <iotmi_zigbee/iotmi_zigbee_types.h>
#include "iotmi_cdmb_zigbee_common_types.h"

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:

extern zigbeeClusterInfo_t zigbeePressureMeasurementInfo;

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 commandHandler to NULL unless you need custom command handling
  • Set readHandler to NULL unless you need custom read handling
  • The name string 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)

#define CLUSTER_MAP_TABLE_SIZE 14  /* was 13, incremented by 1 */

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):

extern zigbeeClusterInfo_t zigbeePressureMeasurementInfo;

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
  • count must 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:

#define DEVICE_TYPE_TABLE_SIZE 13  /* was 12, incremented by 1 */

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:

src/clusters/iotmi_cdmb_zigbee_pressure_measurement.c

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:

zigbeeStatusCodes_t myReadHandler(
zigbeeTask_t *task,
zigbeeClusterInfo_t *clusterInfo);

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 enumMapInfo to 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 BatteryPercentageRemaining is ZCL ID 0x0021 but maps to Matter Power Source attribute ID 0x000C.
  • Command input names are string indices. Use "0", "1", "2" etc. as inputName values. 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 zigbeeReportClusterConfigCount must 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.h for 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 .c file per cluster in src/clusters/. The struct is defined as a global variable (not static) so it can be referenced via extern from 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