Skip to content

Guide: Adding a New Z-Wave Command Class to the CDMB Plugin

Introduction

This guide walks you through adding support for a new Z-Wave device (via its Command Class) to the IotManagedIntegrationsHubSDK-Zwave-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 Z-Wave device to the cloud and back
  • Every file you need to create or modify, with concrete examples

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 Z-Wave implementation of that bridge.

What is a Z-Wave Command Class?

A Z-Wave Command Class (CC) is a standardized set of commands that a Z-Wave device supports. Think of it like an interface or API contract. For example:

  • COMMAND_CLASS_SWITCH_BINARY (0x25) — on/off switches
  • COMMAND_CLASS_DOOR_LOCK (0x62) — door locks
  • COMMAND_CLASS_BATTERY (0x80) — battery-powered devices
  • COMMAND_CLASS_METER (0x32) — energy meters

Each CC defines commands like SET, GET, and REPORT with specific data formats defined in the Z-Wave specification.

What is a 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 (device state), commands (actions you can perform), and events (notifications from the device).

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 Z-Wave command for the specific device.

Here are some examples of how Z-Wave Command Classes map to Matter clusters:

Z-Wave Command Class CC ID Matter Cluster Cluster ID What It Controls
Binary Switch 0x25 On/Off 0x0006 Turning devices on or off
--- --- --- --- ---
Multilevel Switch 0x26 Level Control 0x0008 Dimming lights, adjusting fan speed
Door Lock 0x62 Door Lock 0x0101 Locking and unlocking doors
Battery 0x80 Power Source 0x002F Reporting battery level

Key relationships to understand:

  • One-to-one mapping — Most CCs map to a single cluster (e.g., Binary Switch → On/Off)
  • One CC → multiple clusters — Some CCs are complex enough to map to more than one cluster. For example, Barrier Operator maps to both On/Off and Level Control because a garage door can be open/closed (On/Off) and also partially open (Level Control)
  • Multiple CCs → one cluster — Different CCs can map to the same cluster. For example, both Binary Switch and Barrier Operator map to the On/Off cluster, since both support on/off behavior

The core job of this plugin is to map Z-Wave Command Classes to Matter clusters so the cloud can interact with Z-Wave devices using a protocol-agnostic model.

How Data Flows

`Cloud (Cluster commands/attributes)
  CDMB Plugin (this code — translates between clusters and CCs)
  Z-Wave Shim Layer (iotshdzw/ — abstracts the Z-Wave controller)
  Z-Wave Radio (physical device communication)
`

There are two main data flows:

Control path (cloud → device):

  1. Cloud sends a cluster command (e.g., "turn on the light")
  2. IotmiCdmbZWave_ExecuteTask() dispatches to the correct CC handler
  3. Your Control function translates the cluster command to a Z-Wave CC command
  4. Your Send function constructs the raw Z-Wave frame and sends it

Report path (device → cloud):

  1. Z-Wave device sends a report (e.g., "switch is now ON")
  2. IotmiCdmbZWave_HandleCommandClassReport() dispatches to the correct handler
  3. Your HandleReport function translates the Z-Wave report into cluster attributes (as cJSON)
  4. The CDMB core sends the cluster update to the cloud

Project Structure

`IotManagedIntegrationsHubSDK-Zwave-CDMB-Plugin/
├── CMakeLists.txt                  # Top-level build config
├── include/
│   ├── iotshd_cdmb_zwave.h        # Core data structures (nodes, endpoints, tasks, maps)
│   ├── iotshd_cdmb_zwave_svc.h    # Function declarations for all CC handlers
│   ├── iotshd_cdmb.h              # CDMB core interface definitions
│   ├── iotshd_cluster_common.h    # Cluster IDs, commands, attributes constants
│   ├── iotshdzw/                  # Z-Wave shim layer headers
│   │   ├── iotshd_zwave_common_cc.h         # CC ID definitions (0x25, 0x62, etc.)
│   │   ├── iotshd_zwave_common_cc_switch.h  # Switch CC command/report structs
│   │   ├── iotshd_zwave_common_cc_battery.h # Battery CC structs
│   │   └── ...                              # One header per CC family
│   └── custom_schema/             # Custom cluster schema headers
├── src/
│   ├── CMakeLists.txt             # Source build config (lists all .c files)
│   ├── iotshd_cdmb_zwave_svc.c   # Main service file — dispatch tables and orchestration
│   ├── iotshd_cdmb_zwave_data.c  # Static cluster↔CC mapping data
│   ├── command_classes/           # One .c file per command class implementation
│   │   ├── iotmi_cdmb_zwave_switch_binary.c
│   │   ├── iotmi_cdmb_zwave_door_lock.c
│   │   └── ...
│   ├── iotshdzw/                  # Z-Wave shim layer implementation
│   └── cluster_schemas/           # JSON cluster schema files
├── test/unit/                     # GoogleTest unit tests
│   ├── CMakeLists.txt
│   ├── iotshd_cdmb_zwave_mw_mock.h   # Mock for the Z-Wave middleware
│   ├── iotshd_cdmb_zwave_mw_mock.cc
│   └── iotmi_cdmb_zwave_switch_binary_test.cc
└── external/                      # External dependencies
`

Key Data Structures

Before writing code, understand these core structs (defined in include/iotshd_cdmb_zwave.h):

ZWaveTask

Represents a single command to execute. Passed to your Control function.

`typedef struct IotmiCdmbZWaveTask {
    uint16_t node_id;          // Target Z-Wave node
    uint8_t  ep_id;            // Target endpoint on the node
    uint16_t cluster_id;       // Which cluster the command is for
    uint16_t command_id;       // Which cluster command to execute
    cJSON    *params;          // Command parameters as JSON
    char     *trace_id;        // For request tracing
    // ... other fields for synchronization
} ZWaveTask;
`

ClusterCommandClassMapItem

Static mapping between a cluster and a CC. Defined in iotshd_cdmb_zwave_data.c.

`typedef struct {
    uint16_t cluster_id;           // e.g., CLUSTER_ON_OFF
    uint16_t cluster_id_prefix;    // Upper 16 bits for 32-bit cluster IDs
    uint8_t  revision;             // Cluster revision
    uint16_t cc_id;                // e.g., COMMAND_CLASS_SWITCH_BINARY
    ClusterCommandInfo command_info;
    clusterAttributeInfo_t attribute_info;
    clusterEventInfo_t event_info;
} ClusterCommandClassMapItem;
`

cdmb_zw_rt_t

Runtime mapping instance for a specific node/endpoint. Created by your Map function during onboarding.

`typedef struct {
    uint16_t rt_id;            // Cluster ID
    ClusterCommandClassMapItem *rt_info;  // Pointer to the static map
    uint8_t  command_enabled[CDMB_CLUSTER_MAX_COMMAND_COUNT];
    uint8_t  attribute_enabled[CDMB_CLUSTER_MAX_ATTRIBUTE_COUNT];
    uint32_t home_id;
    uint16_t node_id;
    uint8_t  ep_id;
    uint16_t cc_id;
    uint16_t device_class_id;
} cdmb_zw_rt_t;
`

ZWaveCommandClassClusterContent

Passed to your HandleReport function. Contains the cluster mappings and space to write translated content.

`typedef struct {
    cdmb_zw_rt_t cluster_info[CDMB_ZWAVE_MAX_RTS_PER_CC];
    cJSON *cluster_content[CDMB_ZWAVE_MAX_RTS_PER_CC];
    uint8_t cluster_cnt;
} ZWaveCommandClassClusterContent;
`

Step-by-Step: Adding a New Command Class

We use a hypothetical Thermostat Operating State CC as a running example. Adapt the names and logic to your actual CC.

Step 1: Define the CC in the Shim Layer Header

If your CC does not already have a header in include/iotshdzw/, create one.

New file: include/iotshdzw/iotshd_zwave_common_cc_thermostat.h

Define:

  • Command enum (SET, GET, REPORT, GET_CACHE)
  • Data structures for each command and report (matching the Z-Wave spec frame layout)

Use iotshd_zwave_common_cc_switch.h as a template. The structs must match the Z-Wave spec byte layout because they are cast directly from/to raw command buffers.

Also add the CC ID to include/iotshdzw/iotshd_zwave_common_cc.h:

`#define COMMAND_CLASS_THERMOSTAT_OPERATING_STATE 0x42
`

Step 2: Define Cluster Constants

File: include/iotshd_cluster_common.h

When to do this step: Check whether your Z-Wave CC maps to a Matter cluster that already exists in the codebase. If it does (e.g., your CC maps to the existing CLUSTER_ON_OFF), you can skip this step entirely.

When you need a new cluster: If no existing cluster represents your CC's functionality, you need to define a new one. This typically happens with Z-Wave-specific features that don't have a direct Matter equivalent (e.g., Z-Wave Door Lock Logging has no standard Matter cluster).

For a new cluster, you define three things:

  1. Cluster ID and revision — A unique identifier and version number
  2. Commands — The actions the cloud can send to the device (e.g., "set thermostat mode")
  3. Attributes — The state values the device reports to the cloud (e.g., "current operating state", "system mode")
`/* 1. Cluster identity */
#define CLUSTER_THERMOSTAT 0x0201
#define CLUSTER_THERMOSTAT_REVISION 1

/* 2. Commands the cloud can send to the device */
typedef enum {
    THERMOSTAT_COMMAND_SET_MODE = 0x00,
} ClusterThermostatCommand;

/* 3. Attributes the device reports to the cloud */
typedef enum {
    THERMOSTAT_ATTRIBUTE_OPERATING_STATE = 0x0000,
    THERMOSTAT_ATTRIBUTE_SYSTEM_MODE = 0x001C,
} ClusterThermostatAttribute;

/* String versions of attribute IDs — used as JSON keys in reports */
#define THERMOSTAT_ATTRIBUTE_OPERATING_STATE_STRING "0"
#define THERMOSTAT_ATTRIBUTE_SYSTEM_MODE_STRING "28"
`

Why the string defines? When the device sends a report, the plugin builds a JSON object like {"ats": {"0": value, "28": value}} where the keys are the attribute IDs as strings. These _STRING defines ensure consistency between the enum values and the JSON keys used in HandleReport.

Step 3: Define the Static Cluster-CC Mapping

File: src/iotshd_cdmb_zwave_data.c

This is where you formally connect your Z-Wave CC to its corresponding Matter cluster. Think of this as filling out a "translation card" that tells the plugin: "When the cloud talks about this cluster, translate it to that Z-Wave CC, and here are the specific commands and attributes involved."

Add a ClusterCommandClassMapItem:

`ClusterCommandClassMapItem kThermostatOperatingStateMap = {
    .cluster_id = CLUSTER_THERMOSTAT,           // The Matter cluster this CC maps to
    .cluster_id_prefix = 0,                      // 0 for standard clusters; non-zero for custom Z-Wave-specific clusters
    .revision = CLUSTER_THERMOSTAT_REVISION,     // Must match the revision defined in Step 2
    .cc_id = COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,  // The Z-Wave CC ID from Step 1
    .command_info = {
        .command_id = {THERMOSTAT_COMMAND_SET_MODE},    // Which cluster commands this CC supports
        .command_count = 1                              // How many commands listed above
    },
    .attribute_info = {
        .attribute_id = {
            [0] = THERMOSTAT_ATTRIBUTE_OPERATING_STATE, // Attributes the device can report
            [1] = THERMOSTAT_ATTRIBUTE_SYSTEM_MODE
        },
        .attribute_count = 2                            // How many attributes listed above
    }
};
`

How this mapping gets used at runtime:

  1. Cloud → Device (control): The cloud sends a command targeting CLUSTER_THERMOSTAT. The plugin looks up this map, finds the corresponding COMMAND_CLASS_THERMOSTAT_OPERATING_STATE, and routes to your Control function.
  2. Device → Cloud (report): The Z-Wave device sends a report for CC 0x42. The plugin looks up this map, finds it maps to CLUSTER_THERMOSTAT, and your HandleReport function translates the Z-Wave data into the cluster's attribute format.

Declare as extern in include/iotshd_cdmb_zwave.h so other files can reference it:

`extern ClusterCommandClassMapItem kThermostatOperatingStateMap;
`

Note on cluster_id_prefix: For standard Matter clusters (On/Off, Level Control, etc.), set this to 0. If your CC maps to a custom Z-Wave-specific cluster that doesn't exist in the Matter spec, you'll use a non-zero prefix to create a 32-bit cluster ID. See the Door Lock implementation for an example.

Step 4: Implement the Command Class Handler

New file: src/command_classes/iotmi_cdmb_zwave_thermostat.c

You need 4 functions:

4a. Send Function — Constructs and sends Z-Wave commands

`iotmi_statusCode_t IotmiCdmbZWave_SendThermostatCommand(
    uint16_t node_id, uint8_t ep_id,
    ZWaveThermostatCmd cmd, void *data, ZWaveTask *task)
{
    // 1. Validate parameters
    // 2. Build the command buffer struct (matching Z-Wave spec layout)
    // 3. Set up iotmiZWaveMgr_commandClassSend_t
    // 4. For GET commands: set task->wait_for_report = 1
    // 5. Call IotmiCdmbZWave_SendCommandClassCommand()
}
`

4b. Handle Report — Translates Z-Wave reports to cluster attributes

`void IotmiCdmbZWave_HandleThermostatReport(
    const void *report_data, uint32_t report_data_len,
    ZWaveCommandClassClusterContent *cc_cluster_content)
{
    // 1. Validate inputs and report_data_len
    // 2. Cast report_data to your report struct
    // 3. Loop over cc_cluster_content->cluster_info[]
    // 4. Create cJSON objects: {"ats": {"0": value, "28": value}}
    // 5. Store in cc_cluster_content->cluster_content[i]
}
`

4c. Control Function — Receives cluster commands, dispatches to Send

`iotmi_statusCode_t IotmiCdmbZWave_ControlThermostat(ZWaveTask *task)
{
    // 1. Validate task
    // 2. Check task->cluster_id matches your cluster
    // 3. Switch on task->command_id:
    //    - SET commands: extract params, call Send with SET
    //    - CLUSTER_COMMAND_READ_STATE (0xff02): call Send with GET
    //    - CLUSTER_COMMAND_UPDATE_STATE (0xff01): extract params, call Send
    // 4. Return status
}
`

4d. Map Function — Creates runtime mapping during device onboarding

`void IotmiCdmbZWave_MapThermostatToCluster(
    uint32_t home_id, uint16_t node_id, uint8_t ep_id,
    cdmb_zw_rt_t rt_data[], uint8_t *rt_cnt)
{
    // 1. Validate inputs
    // 2. Fill rt_data[*rt_cnt] with cluster/CC mapping
    // 3. Set command_enabled[] and attribute_enabled[]
    // 4. Increment *rt_cnt
}
`

Tip: Use iotmi_cdmb_zwave_switch_binary.c as your reference — it is the simplest existing implementation.

Step 5: Declare Functions in the Service Header

File: include/iotshd_cdmb_zwave_svc.h

`#include "iotshdzw/iotshd_zwave_common_cc_thermostat.h"

typedef struct {
    ZWaveThermostatCmd type;
    union {
        iotmiZWave_thermostatSet_t set;
        iotmiZWave_thermostatGet_t get;
    } buf;
} ZWaveThermostatCmdBuf;

iotmi_statusCode_t IotmiCdmbZWave_ControlThermostat(ZWaveTask *task);

void IotmiCdmbZWave_HandleThermostatReport(
    const void *report_data, uint32_t report_data_len,
    ZWaveCommandClassClusterContent *cc_cluster_content);

void IotmiCdmbZWave_MapThermostatToCluster(
    uint32_t home_id, uint16_t node_id, uint8_t ep_id,
    cdmb_zw_rt_t rt_data[], uint8_t *rt_cnt);
`

Step 6: Register in the Service File

File: src/iotshd_cdmb_zwave_svc.c

You need to touch 6 places. Search for existing CCs to find exact locations.

6a. Report Handler Table (~line 119) — Add to kCommandClassReportHandlerTable[]:

`{COMMAND_CLASS_THERMOSTAT_OPERATING_STATE, IOTMI_THERMOSTAT_REPORT,
 IotmiCdmbZWave_HandleThermostatReport},
`

6b. Command Class Name Table (~line 196) — Add to COMMAND_CLASS_TABLE[]:

`{COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
 "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE"},
`

6c. Cached Command Option (~line 549) — Add case to GetCommandCachedOption():

`case COMMAND_CLASS_THERMOSTAT_OPERATING_STATE:
    if (cmd == IOTMI_THERMOSTAT_GET_CACHE) { option = true; }
    break;
`

6d. Report Command Mapping (~line 625) — Add case to GetReportCommand():

`case COMMAND_CLASS_THERMOSTAT_OPERATING_STATE:
    if (cmd == IOTMI_THERMOSTAT_GET || cmd == IOTMI_THERMOSTAT_GET_CACHE)
        return IOTMI_THERMOSTAT_REPORT;
    break;
`

6e. Command Dispatch (~line 1460) — Add to ExecuteTask():

`else if (task->cluster_id == CLUSTER_THERMOSTAT) {
    if (IotmiCdmbZWave_CheckClusterMappingOnEndpoint(
            task->node_id, task->ep_id,
            COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
            task->cluster_id, task->type_id)) {
        return IotmiCdmbZWave_ControlThermostat(task);
    }
}
`

6f. Mapping Dispatch (~line 1852) — Add to mappers[] in MapCommandClassToCluster():

`{COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
 IotmiCdmbZWave_MapThermostatToCluster, "THERMOSTAT"},
`

Step 7: Update Build Files

src/CMakeLists.txt — Add to add_library():

`${CMAKE_CURRENT_SOURCE_DIR}/command_classes/iotmi_cdmb_zwave_thermostat.c
`

test/unit/CMakeLists.txt — Add to UNIT_TEST_SRC:

`${CMAKE_CURRENT_SOURCE_DIR}/../../src/command_classes/iotmi_cdmb_zwave_thermostat.c
${CMAKE_CURRENT_SOURCE_DIR}/iotmi_cdmb_zwave_thermostat_test.cc
`

Step 8: Write Unit Tests

New file: test/unit/iotmi_cdmb_zwave_thermostat_test.cc

Use GoogleTest/GoogleMock. Test:

  • Send function: valid SET/GET, invalid parameters, unsupported commands
  • Handle Report: valid reports, malformed data, wrong length, edge cases
  • Control function: each command_id, unsupported commands, null task
  • Map function: correct mapping, full array, null inputs

Use iotmi_cdmb_zwave_switch_binary_test.cc as your template.

Step 9: Custom Cluster Schema (When No Matter Cluster Exists)

When is this needed? Some Z-Wave Command Classes have no equivalent in the Matter specification. For example, Z-Wave Door Lock has features (bolt state, latch state, auto-relock timers) that go far beyond what the standard Matter Door Lock cluster supports. Similarly, Z-Wave Notification subtypes like Home Security or Access Control have no Matter equivalent at all.

In these cases, you need to create a custom cluster — a Z-Wave-specific cluster that the cloud can still understand, even though it's not part of the Matter standard.

How custom clusters differ from standard clusters:

A Standard Cluster Custom Cluster
cluster_id Matter-defined (e.g., 0x0006) Z-Wave CC-derived (e.g., 0x0062 for Door Lock)
--- --- ---
cluster_id_prefix 0 Non-zero (e.g., 0x0F01)
Full 32-bit ID Just the lower 16 bits (prefix << 16) | cluster_id (e.g., 0x0F010062)
Schema Cloud already knows it You must provide a JSON schema so the cloud knows the shape
Header location iotshd_cluster_common.h Dedicated header in include/ (e.g., iotmi_zwave_doorlock_cluster.h)

The 0x0F01 prefix is the convention used for Z-Wave-specific custom clusters. The full 32-bit cluster ID is formed by combining the prefix and the cluster ID: (0x0F01 << 16) | 0x0062 = 0x0F010062.

Here's the complete process, using Z-Wave Door Lock as a real example:

9a. Create the cluster header file

New file: include/iotmi_zwave_doorlock_cluster.h

Define the custom cluster ID, prefix, revision, commands, and attributes — just like Step 2, but in a dedicated header:

`// Custom cluster identity — uses non-zero prefix
#define ZWAVE_DOORLOCK_CLUSTER_ID      0x0062
#define ZWAVE_DOORLOCK_CLUSTER_PREFIX  0x0F01   // Z-Wave custom cluster prefix
#define ZWAVE_DOORLOCK_CLUSTER_REVISION 1

// Commands the cloud can send
typedef enum {
    ZWAVE_DOORLOCK_COMMAND_SET_OPERATION = 0x0001,
    ZWAVE_DOORLOCK_COMMAND_GET_OPERATION = 0x0002,
    ZWAVE_DOORLOCK_COMMAND_SET_CONFIGURATION = 0x0004,
} ZWaveDoorLockCommandEnum;

// Attributes the device reports
typedef enum {
    ZWAVE_DOORLOCK_ATTRIBUTE_LOCK_MODE = 0x0001,
    ZWAVE_DOORLOCK_ATTRIBUTE_DOOR_STATE = 0x0004,
    ZWAVE_DOORLOCK_ATTRIBUTE_BOLT_STATE = 0x0021,
    ZWAVE_DOORLOCK_ATTRIBUTE_LATCH_STATE = 0x0022,
    // ... more attributes as needed
} ZWaveDoorLockAttributeEnum;
`

9b. Create the JSON schema file

New file: src/cluster_schemas/zwave_doorlock.json

The JSON schema tells the cloud the shape of your custom cluster — what attributes it has, what commands it supports, and what data types they use. This is how the cloud learns to "speak" your custom cluster's language.

The schema follows a specific structure:

`{
    "$id": "/schema-versions/capability/zwave.doorlock@1.0",
    "$ref": "/schema-versions/definition/aws.capability@1.0",
    "name": "ZWave Door Lock",
    "extrinsicId": "0x0F010062",
    "extrinsicVersion": "1",
    "title": "Z-Wave Door Lock Capability",
    "description": "Z-Wave specific door lock interface",
    "unevaluatedProperties": false,
    "properties": {
        "LockMode": {
            "extrinsicId": "0x0001",
            "value": {"$ref": "#/$defs/ZWaveDoorLockMode"},
            "extrinsicProperties": {"side": "server"},
            "mutable": true,
            "reportable": true
        },
        "DoorState": {
            "extrinsicId": "0x0004",
            "value": {"$ref": "#/$defs/ZWaveDoorState"},
            "extrinsicProperties": {"side": "server"},
            "mutable": false,
            "reportable": true
        }
    },
    "actions": [
        {
            "name": "SetOperation",
            "extrinsicId": "0x0001",
            "request": {
                "parameters": {
                    "Mode": {
                        "extrinsicId": "0",
                        "value": {"$ref": "#/$defs/ZWaveDoorLockMode"}
                    }
                }
            }
        }
    ],
    "$defs": {
        "ZWaveDoorLockMode": {
            "type": "string",
            "enum": ["Unsecured", "Secured", "Unknown"],
            "extrinsicIdMap": {
                "Unsecured": "0x00",
                "Secured": "0xFF",
                "Unknown": "0xFE"
            }
        },
        "ZWaveDoorState": {
            "type": "string",
            "enum": ["Open", "Closed"],
            "extrinsicIdMap": {"Open": "0x00", "Closed": "0x01"}
        }
    }
}
`

Key fields in the JSON schema:

  • extrinsicId at the top level — The full 32-bit cluster ID as a hex string (e.g., "0x0F010062")
  • properties — Each attribute the device can report. The extrinsicId must match the attribute enum values from your header
  • mutabletrue if the cloud can write this attribute (e.g., setting lock mode), false if read-only (e.g., door state)
  • reportabletrue if the device sends this attribute in reports
  • actions — Commands the cloud can send. The extrinsicId must match the command enum values
  • $defs — Reusable type definitions for enum values. The extrinsicIdMap maps human-readable names to Z-Wave byte values

9c. Create the embedded schema header (for notification-style clusters)

For some custom clusters, the schema is embedded directly as a C string macro in a header file under include/custom_schema/. This is used when the schema needs to be compiled into the binary rather than loaded from a JSON file.

New file: include/custom_schema/zwave_my_custom_schema.h

`#define ZWAVE_MY_CUSTOM_SCHEMA_JSON \
  "{\"$id\":\"/schema-versions/capability/" \
  "zwave.mycustom@1.0\"," \
  "\"$ref\":\"/schema-versions/definition/aws.capability@1.0\"," \
  "\"name\":\"ZWave My Custom\"," \
  "\"extrinsicId\":\"0x0F01XXXX\"," \
  "\"extrinsicVersion\":\"1\"," \
  "\"properties\":{" \
  "\"MyAttribute\":{" \
  "\"extrinsicId\":\"0x0000\"," \
  "\"value\":{\"type\":\"boolean\"}," \
  "\"extrinsicProperties\":{\"side\":\"server\"}," \
  "\"mutable\":false," \
  "\"reportable\":true" \
  "}" \
  "}" \
  "}"
`

Then include this in your cluster header:

`#include "custom_schema/zwave_my_custom_schema.h"
`

9d. Update the static mapping to use the custom prefix

In src/iotshd_cdmb_zwave_data.c, set the non-zero cluster_id_prefix:

`ClusterCommandClassMapItem kDoorLockDoorLockMap = {
    .cluster_id = ZWAVE_DOORLOCK_CLUSTER_ID,          // 0x0062
    .cluster_id_prefix = ZWAVE_DOORLOCK_CLUSTER_PREFIX, // 0x0F01 — marks this as custom
    .revision = ZWAVE_DOORLOCK_CLUSTER_REVISION,
    .cc_id = COMMAND_CLASS_DOOR_LOCK,
    // ... commands and attributes
};
`

9e. Set the prefix in your Map function

In your Map function (Step 4d), make sure to set rt_id_prefix:

`rt_data[*rt_cnt].rt_id = ZWAVE_DOORLOCK_CLUSTER_ID;
rt_data[*rt_cnt].rt_id_prefix = kDoorLockDoorLockMap.cluster_id_prefix;
rt_data[*rt_cnt].rt_info = &kDoorLockDoorLockMap;
`

The framework combines rt_id_prefix and rt_id into the full 32-bit cluster ID (0x0F010062) when communicating with the cloud.

Existing custom cluster examples to reference:

Custom Cluster Header File JSON Schema Prefix
Z-Wave Door Lock iotmi_zwave_doorlock_cluster.h zwave_doorlock.json 0x0F01
--- --- --- ---
Z-Wave Door Lock Logging iotmi_zwave_doorlock_cluster.h zwave_doorlocklogging.json 0x0F01
Z-Wave User Code iotmi_zwave_usercode_cluster.h zwave_usercode.json 0x0F01
Z-Wave Notification (Home Security) iotmi_zwave_notification_cluster.h zwave_notificationhomesecurity.json 0x0F01
Z-Wave Notification (Access Control) iotmi_zwave_notification_cluster.h zwave_notificationaccesscontrol.json 0x0F01
Z-Wave Notification (Power Mgmt) iotmi_zwave_notification_cluster.h zwave_notificationpowermanagement.json 0x0F01

Step 10: Build and Verify

Follow the readme in SDK root path to build the SDK

Tips and Common Pitfalls

  • Always check the Z-Wave spec for the exact byte layout of commands and reports. The structs must match exactly.
  • Handle CLUSTER_COMMAND_READ_STATE (0xff02) and CLUSTER_COMMAND_UPDATE_STATE (0xff01) — these are special command IDs used by the cloud. Your Control function must handle them.
  • cJSON memory management — The CDMB core takes ownership of cJSON objects created in HandleReport. Do not free them yourself, but do clean up on error paths.
  • The wait_for_report flag — Set task->wait_for_report = 1 for GET commands so the framework waits for the device report.
  • One CC can map to multiple clusters — e.g., Barrier Operator maps to both On/Off and Level Control. Your Map function can add multiple rt_data[] entries.
  • One cluster can be served by multiple CCs — The dispatch uses CheckClusterMappingOnEndpoint() to find the right CC.
  • Use the simplest CC as your templateswitch_binary.c for simple CCs, battery.c or door_lock.c for complex ones.

Reference: Currently Supported Command Classes

Command Class Zwave CC ID Maps To Cluster(s) Source File
Switch Binary 0x25 On/Off iotmi_cdmb_zwave_switch_binary.c
--- --- --- ---
Switch Multilevel 0x26 Level Control iotmi_cdmb_zwave_switch_multilevel.c
Sensor Multilevel 0x31 Temperature Measurement iotmi_cdmb_zwave_sensor_multilevel.c
Meter 0x32 Electrical Energy/Power iotmi_cdmb_zwave_meter.c
Switch Color 0x33 Color Control iotmi_cdmb_zwave_switch_color.c
Door Lock Logging 0x4C Z-Wave Door Lock Logging iotmi_cdmb_zwave_door_lock_logging.c
Door Lock 0x62 Z-Wave Door Lock iotmi_cdmb_zwave_door_lock.c
User Code 0x63 Z-Wave User Code iotmi_cdmb_zwave_user_code.c
Barrier Operator 0x66 On/Off + Level Control iotmi_cdmb_zwave_barrier_operator.c
Notification 0x71 Boolean State, Smoke/CO Alarm iotmi_cdmb_zwave_notification.c
Sound Switch 0x79 Level Control iotmi_cdmb_zwave_switch_sound.c
Battery 0x80 Power Source iotmi_cdmb_zwave_battery.c