Skip to content

Guide: Adding a New Matter Cluster to the CDMB Plugin

Introduction

This guide walks you through adding support for a new Matter cluster to the IotManagedIntegrationsDeviceSDK-MatterPlugin 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
  • Why translation is still needed even though the cloud data model IS Matter
  • The chip-tool subprocess architecture and how commands flow through it
  • Every file you need to create or modify, with concrete examples

Key difference from Z-Wave and Zigbee CDMB plugins: The cloud data model IS Matter, so there is no protocol-to-Matter translation. However, the plugin still performs significant work: converting between IoTMI JSON format and chip-tool CLI arguments, mapping enum/bitmap values between string names and numeric values, managing Matter subscriptions, and handling device lifecycle. All cluster-specific logic lives in a single file (matter_action_converter.cpp) rather than per-cluster files. Language difference: This plugin is written in C++ (not C like Z-Wave/Zigbee). It uses std::string, std::vector, std::unordered_map, and other STL containers.


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

What is a Matter Cluster?

A Matter cluster is a standardized set of attributes, commands, and events defined by the Matter specification (formerly CHIP/Project Connected Home over IP). 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
  • Color Control (0x0300) — color-capable lights

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")
  • Events — notifications from the device (e.g., "StateChange", "DoorLockAlarm")

Why Translation Is Still Needed

Even though the cloud data model IS Matter, the plugin still performs significant translation work:

What Cloud Format Device Format (chip-tool) Translation Needed
Enum values String names ("Locked", "Heat") Numeric values (1, 4) String ↔ numeric mapping
--- --- --- ---
Bitmap values JSON objects ({"CoolSetpoint": true}) Integer bitmasks (0x02) Object ↔ bitmask conversion
Command format JSON with camelCase names CLI args with dash-case names Format conversion
Attribute reports chip-tool text output IoTMI JSON format Text parsing → JSON
Attribute IDs Numeric IDs Numeric IDs (same) ID-to-name lookup for JSON keys

Real-world example: When the cloud sends "lock the door", it arrives as:

{"capabilityId": "matter.DoorLock@1.4", "actionName": "LockDoor", "parameters": {"0": "123456"}}

The plugin must convert this to chip-tool CLI arguments:

doorlock lock-door <nodeId> <endpointId> --timedInteractionTimeoutMs 1000 --PINCode 123456

And when the device reports its lock state, chip-tool outputs:

LockState: 1

The plugin must convert this back to IoTMI JSON:

{"LockState": "Locked"}

How Data Flows

Cloud (IoTMI JSON — Matter cluster commands/attributes)
  CDMB Plugin (this code — converts between IoTMI JSON and chip-tool CLI)
  chip-tool (subprocess — handles Matter protocol encoding/decoding)
  Matter Device (physical device communication via Thread/Wi-Fi/BLE)

There are two main data flows: Control path (cloud → device):

  1. Cloud sends an IoTMI control task with JSON payload
  2. controlTaskCallback() parses the JSON into action requests
  3. convertActionToCommand() dispatches based on cluster name:

    • UpdateState (0xff01): processUpdateStateAction() — converts attribute writes to chip-tool write commands
    • ReadState (0xff02): processReadStateAction() — converts to chip-tool read commands
    • Regular commands: processRegularAction() — converts cluster commands to chip-tool cluster-specific commands
  4. ChiptoolManager submits the command to chip-tool's stdin

Report path (device → cloud):

  1. chip-tool outputs attribute reports on stdout (from subscriptions or reads)
  2. ChiptoolParser parses the text into MatterPluginResponse structs
  3. DeviceManager routes responses to the correct DeviceEndpointClusterAttribute
  4. convertAttributeToReportString() converts chip-tool text values to IoTMI JSON format (enum names, bitmap objects, etc.)
  5. DeviceJsonReporter generates the JSON report and publishes to cloud

Key insight: The plugin does NOT implement the Matter protocol stack. chip-tool handles all Matter encoding/decoding, TLV serialization, secure sessions, etc. The plugin's job is to bridge between IoTMI's JSON world and chip-tool's CLI world.


Project Structure

IotManagedIntegrationsDeviceSDK-MatterPlugin/
├── CMakeLists.txt                              # Top-level build config
├── MatterPlugin.cmake                          # CMake module for parent SDK integration
├── include/
│   ├── matter_plugin_core.h                   # Core plugin class (lifecycle, callbacks)
│   ├── matter_plugin_config.h                 # Configuration (chip-tool path, subscriptions)
│   ├── matter_plugin_msg.h                    # MatterPluginCommand / MatterPluginResponse
│   ├── matter_plugin_msg_queue.h              # Thread-safe message queue
│   ├── matter_plugin_log.h                    # Logging macros
│   ├── matter_plugin_exceptions.h             # Exception types
│   ├── conversion/
│   │   └── matter_action_converter.h          # ★ Action/attribute conversion (THE dispatch layer)
│   ├── chiptool/
│   │   ├── chiptool_manager.h                 # chip-tool process management
│   │   ├── chiptool_proc.h                    # Subprocess wrapper
│   │   ├── chiptool_parser.h                  # chip-tool output parser
│   │   └── chiptool_response_parser.h         # Value parsing utilities
│   ├── device/
│   │   ├── matter_plugin_device.h             # Device model
│   │   ├── matter_plugin_endpoint.h           # Endpoint model
│   │   ├── matter_plugin_cluster.h            # Cluster/Attribute model
│   │   ├── matter_plugin_device_manager.h     # Device collection manager
│   │   ├── matter_plugin_device_json_reporter.h # JSON report generation
│   │   └── matter_plugin_device_list_storage.h  # Device persistence
│   ├── iotmi_msg/
│   │   ├── iotmi_msg.h                        # IoTMI message parsing
│   │   ├── iotmi_msg_constants.h              # Message field constants
│   │   └── matter_plugin_device_address_utils.h # Device address format
│   ├── zzz_generated/
│   │   ├── iotmi/
│   │   │   └── matter_cluster_definitions.h   # ★ Auto-generated cluster registry (88 clusters)
│   │   ├── clusters/                          # ★ ZAP-generated cluster ID headers (141 dirs)
│   │   │   ├── OnOff/
│   │   │   │   ├── AttributeIds.h
│   │   │   │   ├── ClusterId.h
│   │   │   │   ├── CommandIds.h
│   │   │   │   ├── EventIds.h
│   │   │   │   └── Ids.h
│   │   │   ├── DoorLock/
│   │   │   └── ...                            # 141 cluster directories
│   │   ├── app-common/zap-generated/ids/      # Common ZAP-generated IDs
│   │   └── chip-tool/zap-generated/cluster/   # chip-tool cluster definitions
│   ├── datamodel/lib/core/
│   │   ├── DataModelTypes.h                   # Matter type definitions
│   │   └── CHIPVendorIdentifiers.hpp          # Vendor IDs
│   └── util/
│       ├── base64.h                           # Base64 utilities
│       └── sha256_verifier.h                  # chip-tool binary verification
├── src/
│   ├── CMakeLists.txt                         # Source build config (20 source files)
│   └── matter_plugin/
│       ├── matter_plugin_main.cpp             # Entry point
│       ├── matter_plugin_core.cpp             # Core lifecycle and callbacks
│       ├── matter_plugin_config.cpp           # Configuration parsing
│       ├── matter_plugin_msg.cpp              # Command/response structs
│       ├── conversion/
│       │   └── matter_action_converter.cpp    # ★ ALL cluster-specific dispatch logic
│       ├── chiptool/
│       │   ├── chiptool_manager.cpp           # chip-tool process management
│       │   ├── chiptool_proc.cpp              # Subprocess wrapper
│       │   ├── chiptool_parser.cpp            # Output parser
│       │   └── chiptool_response_parser.cpp   # Value parsing utilities
│       ├── device/
│       │   ├── matter_plugin_device.cpp        # Device model
│       │   ├── matter_plugin_endpoint.cpp      # Endpoint model
│       │   ├── matter_plugin_cluster.cpp       # Cluster/Attribute model
│       │   ├── matter_plugin_device_manager.cpp
│       │   ├── matter_plugin_device_json_reporter.cpp
│       │   └── matter_plugin_device_list_storage.cpp
│       ├── iotmi_msg/
│       │   ├── iotmi_msg.cpp
│       │   └── matter_plugin_device_address_utils.cpp
│       └── zzz_generated/iotmi/
│           └── matter_cluster_definitions.cpp  # ★ Auto-generated cluster registry
├── test/                                       # GoogleTest unit tests (18 test files)
└── tools/
    ├── generate_cpp_schemas.py                 # Generates cluster definitions from JSON schemas
    └── retrieve_schemas.py                     # Retrieves Matter capability schemas

Key Data Structures

MatterPluginCommand

A command to send to chip-tool. Simply a vector of string arguments.

struct MatterPluginCommand {
    std::vector<std::string> cmdArgs;  // e.g., {"onoff", "on", "12345", "1"}
    std::string toCommandString() const;  // Joins args with spaces
};

MatterPluginResponse

A response parsed from chip-tool's stdout output.

struct MatterPluginResponse {
    uint64_t nodeId;        // Matter node ID
    uint16_t endpointID;    // Endpoint number
    uint32_t clusterID;     // Cluster ID (e.g., 0x0006 for On/Off)
    uint32_t attributeID;   // Attribute ID within the cluster
    uint32_t dataVersion;   // Data version for the attribute
    std::string data;       // Raw string value from chip-tool output
};

ClusterInfo (auto-generated)

Metadata about a cluster, used for capability reporting. Defined in matter_cluster_definitions.h.

struct ClusterInfo {
    std::string name;           // e.g., "On/Off"
    std::string id;             // e.g., "matter.OnOff@1.4"
    std::string extrinsicId;    // e.g., "0x0006"
    std::string version;        // e.g., "1.4"
    std::string title;          // Human-readable title
    std::string description;    // Human-readable description
    std::unordered_set<std::string> properties;  // Attribute names
    std::unordered_set<std::string> actions;     // Command names
    std::unordered_set<std::string> events;      // Event names
};

ClusterRegistry (auto-generated)

Static registry providing cluster lookup by ID. Used for capability discovery and reporting.

class ClusterRegistry {
public:
    static const ClusterInfo* getCluster(const std::string& extrinsicId);
    static const ClusterInfo* getClusterByFullId(const std::string& fullId);
    static const std::vector<ClusterInfo>& getAllClusters();
};

FieldEnumMapping / FieldBitMapping

Mapping tables for converting between string names and numeric values. Used in matter_action_converter.cpp.

struct FieldEnumMapping {
    const char* fieldName;   // String name (e.g., "Locked")
    uint32_t enumValue;      // Numeric value (e.g., 1)
};
// Terminated by {UNKNOWN_ENUM_VALUE, <default>} sentinel

struct FieldBitMapping {
    const char* fieldName;   // String name (e.g., "CoolSetpoint")
    uint32_t bitMask;        // Bitmask (e.g., 0x02)
};
// Terminated by {nullptr, 0} sentinel

Device / Endpoint / Cluster / Attribute Hierarchy

The runtime device model mirrors the Matter data model directly:

class Device {
    uint64_t m_nodeId;
    DeviceState m_state;  // INVALID → INIT → PROVISIONED
    std::unordered_map<uint16_t, Endpoint> m_endpoints;
    // ... timestamps, managedThingId, etc.
};

class Endpoint {
    uint16_t m_endpointId;
    std::unordered_map<uint32_t, Cluster> m_clusters;
};

class Cluster {
    uint32_t m_clusterId;
    std::unordered_map<uint32_t, Attribute> m_attributes;
};

struct Attribute {
    uint32_t attributeId;
    std::string value;
    std::chrono::steady_clock::time_point lastUpdated;
};

Note: The device model is generic — it works with any cluster without modification. You do NOT need to change any device model files when adding a new cluster.


Step-by-Step: Adding a New Cluster

We use a hypothetical Window Covering cluster (0x0102) as a running example. Adapt the names and logic to your actual cluster.

Step 1: Add ZAP-Generated Cluster ID Headers

New directory: include/zzz_generated/clusters/WindowCovering/ Create 5 header files with the cluster's Matter specification IDs. These provide the chip::app::Clusters::WindowCovering::* constants used throughout the code.

ClusterId.h:

#pragma once
#include <cstdint>

namespace chip {
namespace app {
namespace Clusters {
namespace WindowCovering {

static constexpr uint32_t Id = 0x00000102;

} // namespace WindowCovering
} // namespace Clusters
} // namespace app
} // namespace chip

AttributeIds.h:

#pragma once
#include <cstdint>

namespace chip {
namespace app {
namespace Clusters {
namespace WindowCovering {
namespace Attributes {

namespace Type {
static constexpr uint32_t Id = 0x00000000;
} // namespace Type

namespace CurrentPositionLiftPercentage {
static constexpr uint32_t Id = 0x00000008;
} // namespace CurrentPositionLiftPercentage

namespace CurrentPositionTiltPercentage {
static constexpr uint32_t Id = 0x00000009;
} // namespace CurrentPositionTiltPercentage

namespace OperationalStatus {
static constexpr uint32_t Id = 0x0000000A;
} // namespace OperationalStatus

namespace TargetPositionLiftPercent100ths {
static constexpr uint32_t Id = 0x0000000B;
} // namespace TargetPositionLiftPercent100ths

namespace TargetPositionTiltPercent100ths {
static constexpr uint32_t Id = 0x0000000C;
} // namespace TargetPositionTiltPercent100ths

namespace ConfigStatus {
static constexpr uint32_t Id = 0x00000007;
} // namespace ConfigStatus

namespace Mode {
static constexpr uint32_t Id = 0x00000017;
} // namespace Mode

} // namespace Attributes
} // namespace WindowCovering
} // namespace Clusters
} // namespace app
} // namespace chip

CommandIds.h:

#pragma once
#include <cstdint>

namespace chip {
namespace app {
namespace Clusters {
namespace WindowCovering {
namespace Commands {

namespace UpOrOpen {
static constexpr uint32_t Id = 0x00000000;
} // namespace UpOrOpen

namespace DownOrClose {
static constexpr uint32_t Id = 0x00000001;
} // namespace DownOrClose

namespace StopMotion {
static constexpr uint32_t Id = 0x00000002;
} // namespace StopMotion

namespace GoToLiftPercentage {
static constexpr uint32_t Id = 0x00000005;
} // namespace GoToLiftPercentage

namespace GoToTiltPercentage {
static constexpr uint32_t Id = 0x00000008;
} // namespace GoToTiltPercentage

} // namespace Commands
} // namespace WindowCovering
} // namespace Clusters
} // namespace app
} // namespace chip

EventIds.h:

#pragma once
// No events for this cluster (or add as needed)

Ids.h:

#pragma once
#include "AttributeIds.h"
#include "ClusterId.h"
#include "CommandIds.h"
#include "EventIds.h"

Tip: Look at existing cluster directories (e.g., OnOff/, DoorLock/) as templates. The IDs must match the Matter specification exactly.

Step 2: Define Enum and Bitmap Mapping Tables

File: src/matter_plugin/conversion/matter_action_converter.cpp (top of file, ~lines 30–600) Add mapping tables in the cluster's namespace for all enum and bitmap types used by the cluster's attributes and commands.

namespace chip {
namespace app {
namespace Clusters {
namespace WindowCovering {
namespace Mappings {

/* Enum mapping: WindowCoveringType */
static const FieldEnumMapping g_TypeEnumMappings[] = {
    { "Rollershade",           0x00 },
    { "Rollershade2Motor",     0x01 },
    { "RollershadeExterior",   0x02 },
    { "RollershadeExterior2Motor", 0x03 },
    { "Drapery",               0x04 },
    { "Awning",                0x05 },
    { "Shutter",               0x06 },
    { "TiltBlindTiltOnly",     0x07 },
    { "TiltBlindLiftAndTilt",  0x08 },
    { "ProjectorScreen",       0x09 },
    { UNKNOWN_ENUM_VALUE,      0xFF },
};

/* Bitmap mapping: ConfigStatus */
static const FieldBitMapping g_ConfigStatusBitMappings[] = {
    { "Operational",       0x01 },
    { "OnlineReserved",    0x02 },
    { "LiftMovementReversed", 0x04 },
    { "LiftPositionAware", 0x08 },
    { "TiltPositionAware", 0x10 },
    { "LiftEncoderControlled", 0x20 },
    { "TiltEncoderControlled", 0x40 },
    { nullptr, 0 },  /* sentinel */
};

/* Bitmap mapping: Mode */
static const FieldBitMapping g_ModeBitMappings[] = {
    { "MotorDirectionReversed", 0x01 },
    { "CalibrationMode",        0x02 },
    { "MaintenanceMode",        0x04 },
    { "LEDFeedback",            0x08 },
    { nullptr, 0 },
};

/* Bitmap mapping: OperationalStatus */
static const FieldBitMapping g_OperationalStatusBitMappings[] = {
    { "Global",  0x03 },
    { "Lift",    0x0C },
    { "Tilt",    0x30 },
    { nullptr, 0 },
};

} // namespace Mappings
} // namespace WindowCovering
} // namespace Clusters
} // namespace app
} // namespace chip

Enum mapping rules:

  • Each entry maps a string name to a numeric value
  • Terminate with { UNKNOWN_ENUM_VALUE, <default_value> } sentinel
  • The string names must match the cloud data model exactly

Bitmap mapping rules:

  • Each entry maps a field name to a bitmask
  • Terminate with { nullptr, 0 } sentinel
  • The field names must match the cloud data model exactly

Step 3: Add UpdateState Handler (Writable Attributes)

File: src/matter_plugin/conversion/matter_action_converter.cpp Find the processUpdateStateAction() function and add an else if block for your cluster. This handles attribute write commands from the cloud (command ID 0xff01).

static bool processUpdateStateAction(
    uint64_t nodeId, uint16_t endpointId,
    const std::string& clusterName, cJSON* parameters,
    std::vector<MatterPluginCommand>& commands)
{
    // ... existing clusters ...

    else if( clusterName == "windowcovering" )
    {
        cJSON* param = parameters->child;
        while( param != nullptr )
        {
            MatterPluginCommand cmd;
            cmd.cmdArgs = { "windowcovering", "write" };

            std::string paramName( param->string );

            if( paramName == "Mode" )
            {
                cmd.cmdArgs.push_back( "mode" );
                cmd.cmdArgs.push_back( std::to_string(
                    parseBitmapFromJson( param,
                        WindowCovering::Mappings::g_ModeBitMappings ) ) );
            }
            else
            {
                param = param->next;
                continue;  /* Skip unknown attributes */
            }

            cmd.cmdArgs.push_back( std::to_string( nodeId ) );
            cmd.cmdArgs.push_back( std::to_string( endpointId ) );
            commands.push_back( cmd );
            param = param->next;
        }
        return true;
    }

    return false;
}

Pattern: For each writable attribute:

  1. Check the parameter name
  2. Build a chip-tool write command: {<cluster>, "write", <attribute-name>, <value>, <nodeId>, <endpointId>}
  3. Convert enum/bitmap values using the mapping tables
  4. Push the command to the output vector

Step 4: Add Regular Command Handler (Cluster Commands)

File: src/matter_plugin/conversion/matter_action_converter.cpp Find the processRegularAction() function and add an else if block for your cluster. This handles cluster-specific commands (not read/write).

static bool processRegularAction(
    uint64_t nodeId, uint16_t endpointId,
    const std::string& clusterName, const std::string& convertedActionName,
    cJSON* parameters, std::vector<MatterPluginCommand>& commands)
{
    // ... existing clusters ...

    else if( clusterName == "windowcovering" )
    {
        MatterPluginCommand cmd;
        cmd.cmdArgs = { "windowcovering", convertedActionName };

        if( convertedActionName == "up-or-open" )
        {
            /* No additional arguments */
        }
        else if( convertedActionName == "down-or-close" )
        {
            /* No additional arguments */
        }
        else if( convertedActionName == "stop-motion" )
        {
            /* No additional arguments */
        }
        else if( convertedActionName == "go-to-lift-percentage" )
        {
            cJSON* liftPercent = cJSON_GetObjectItem( parameters, "0" );
            if( liftPercent )
            {
                cmd.cmdArgs.push_back( std::to_string( liftPercent->valueint ) );
            }
        }
        else if( convertedActionName == "go-to-tilt-percentage" )
        {
            cJSON* tiltPercent = cJSON_GetObjectItem( parameters, "0" );
            if( tiltPercent )
            {
                cmd.cmdArgs.push_back( std::to_string( tiltPercent->valueint ) );
            }
        }
        else
        {
            return false;  /* Unknown command */
        }

        cmd.cmdArgs.push_back( std::to_string( nodeId ) );
        cmd.cmdArgs.push_back( std::to_string( endpointId ) );
        commands.push_back( cmd );
        return true;
    }

    return false;
}

Pattern: For each cluster command:

  1. Start with {<cluster>, <command-name>}
  2. Extract parameters from JSON and append as string arguments
  3. Append <nodeId> and <endpointId> at the end
  4. Push the command to the output vector

Command name convention: chip-tool uses dash-case (e.g., go-to-lift-percentage), while the cloud uses PascalCase (e.g., GoToLiftPercentage). The convertedActionName parameter is already converted to dash-case by the framework.

Step 5: Add Attribute Report Handler

File: src/matter_plugin/conversion/matter_action_converter.cpp Find the convertAttributeToReportString() function and add a case for your cluster in the outer switch. This converts chip-tool's text output back to IoTMI JSON format.

bool convertAttributeToReportString(
    uint32_t clusterId, uint32_t attributeId,
    const std::string& attributeValue, std::string& reportString)
{
    using namespace chip::app::Clusters;

    switch( clusterId )
    {
        // ... existing clusters ...

        case WindowCovering::Id:
            switch( attributeId )
            {
                case WindowCovering::Attributes::Type::Id:
                    return parseEnumPropertyValue( attributeValue,
                        WindowCovering::Mappings::g_TypeEnumMappings,
                        reportString );

                case WindowCovering::Attributes::CurrentPositionLiftPercentage::Id:
                case WindowCovering::Attributes::CurrentPositionTiltPercentage::Id:
                case WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id:
                case WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id:
                    return parseNumericPropertyValue( attributeValue, reportString );

                case WindowCovering::Attributes::ConfigStatus::Id:
                    return parseBitmapPropertyValue( attributeValue,
                        WindowCovering::Mappings::g_ConfigStatusBitMappings,
                        reportString );

                case WindowCovering::Attributes::OperationalStatus::Id:
                    return parseBitmapPropertyValue( attributeValue,
                        WindowCovering::Mappings::g_OperationalStatusBitMappings,
                        reportString );

                case WindowCovering::Attributes::Mode::Id:
                    return parseBitmapPropertyValue( attributeValue,
                        WindowCovering::Mappings::g_ModeBitMappings,
                        reportString );

                default:
                    return parseNumericPropertyValue( attributeValue, reportString );
            }

        // ... more clusters ...
    }

    return false;
}

Pattern: For each attribute, dispatch to the appropriate parser:

Attribute Type    Parser Function    Example
Boolean    parseBoolPropertyValue()    OnOff → "true" / "false"
Numeric (uint8, uint16, int16, etc.)    parseNumericPropertyValue()    CurrentLevel → "128"
Enum    parseEnumPropertyValue()    LockState → "Locked"
Bitmap    parseBitmapPropertyValue()    ConfigStatus → {"Operational": true, ...}
String    parseStringPropertyValue()    NodeLabel → "Living Room"

Step 6: Add Cluster ID Header Include

File: src/matter_plugin/conversion/matter_action_converter.cpp (top of file, includes section) Add the include for your cluster's ID headers:

#include <zzz_generated/clusters/WindowCovering/Ids.h>

Step 7: Regenerate Cluster Definitions (Auto-Generated)

The ClusterRegistry in matter_cluster_definitions.h/.cpp is auto-generated from Matter capability JSON schemas. To add your cluster:

  1. Ensure the cluster's JSON schema exists in the schema source (retrieved via tools/retrieve_schemas.py)
  2. Run the code generation tool:
python3 tools/generate_cpp_schemas.py

This regenerates:

  • include/zzz_generated/iotmi/matter_cluster_definitions.h — cluster namespace constants
  • src/matter_plugin/zzz_generated/iotmi/matter_cluster_definitions.cpp — ClusterRegistry initialization

The generated code provides:

  • ClusterRegistry::getCluster("0x0102") — lookup by extrinsic ID
  • Clusters::Window_Covering::NAME, ID, EXTRINSIC_ID, VERSION — namespace constants
  • Clusters::Window_Covering::PROPERTY_*, ACTION_*, EVENT_* — property/action/event name constants

Note: If the cluster already exists in the schema source (most standard Matter clusters do), you only need to run the generation tool. The 88 clusters in the current registry cover most of the Matter specification.

Step 8: Update Build Files (If Needed)

The source file matter_action_converter.cpp is already in the build. You only need to update src/CMakeLists.txt if you add new source files (which is rare — most clusters only modify matter_action_converter.cpp). If you added new header directories under zzz_generated/clusters/, ensure the include path covers them (it typically does via a wildcard or parent directory include).

Step 9: Build and Verify

Build the SDK following the instructions in the SDK root README to verify your changes compile correctly.


Complete Example: Simple Cluster (OnOff)

The OnOff cluster is one of the simplest. Here's how it's implemented across the registration points: Enum/Bitmap mappings:

namespace chip::app::Clusters::OnOff::Mappings {

static const FieldEnumMapping g_StartUpOnOffEnumMappings[] = {
    { "Off",    0x00 },
    { "On",     0x01 },
    { "Toggle", 0x02 },
    { UNKNOWN_ENUM_VALUE, 3 },
};

static const FieldBitMapping g_FeatureBitMappings[] = {
    { "Lighting",    0x01 },
    { "DeadFrontBehavior", 0x02 },
    { "OffOnly",     0x04 },
    { nullptr, 0 },
};

} // namespace

UpdateState handler (writable attributes):

else if( clusterName == "onoff" )
{
    cJSON* param = parameters->child;
    while( param != nullptr )
    {
        MatterPluginCommand cmd;
        cmd.cmdArgs = { "onoff", "write" };
        std::string paramName( param->string );

        if( paramName == "OnTime" )
        {
            cmd.cmdArgs.push_back( "on-time" );
            cmd.cmdArgs.push_back( std::to_string( param->valueint ) );
        }
        else if( paramName == "OffWaitTime" )
        {
            cmd.cmdArgs.push_back( "off-wait-time" );
            cmd.cmdArgs.push_back( std::to_string( param->valueint ) );
        }
        else if( paramName == "StartUpOnOff" )
        {
            cmd.cmdArgs.push_back( "start-up-on-off" );
            cmd.cmdArgs.push_back( std::to_string(
                findEnumValue( param->valuestring,
                    OnOff::Mappings::g_StartUpOnOffEnumMappings ) ) );
        }
        else { param = param->next; continue; }

        cmd.cmdArgs.push_back( std::to_string( nodeId ) );
        cmd.cmdArgs.push_back( std::to_string( endpointId ) );
        commands.push_back( cmd );
        param = param->next;
    }
    return true;
}

Regular command handler:

else if( clusterName == "onoff" )
{
    MatterPluginCommand cmd;
    cmd.cmdArgs = { "onoff", convertedActionName };

    if( convertedActionName == "off" ||
        convertedActionName == "on" ||
        convertedActionName == "toggle" ||
        convertedActionName == "on-with-recall-global-scene" )
    {
        /* No additional arguments */
    }
    else if( convertedActionName == "off-with-effect" )
    {
        cJSON* effectId = cJSON_GetObjectItem( parameters, "0" );
        cJSON* effectVariant = cJSON_GetObjectItem( parameters, "1" );
        if( effectId ) cmd.cmdArgs.push_back( std::to_string( effectId->valueint ) );
        if( effectVariant ) cmd.cmdArgs.push_back( std::to_string( effectVariant->valueint ) );
    }
    else if( convertedActionName == "on-with-timed-off" )
    {
        cJSON* onOffControl = cJSON_GetObjectItem( parameters, "0" );
        cJSON* onTime = cJSON_GetObjectItem( parameters, "1" );
        cJSON* offWaitTime = cJSON_GetObjectItem( parameters, "2" );
        if( onOffControl ) cmd.cmdArgs.push_back( std::to_string( onOffControl->valueint ) );
        if( onTime ) cmd.cmdArgs.push_back( std::to_string( onTime->valueint ) );
        if( offWaitTime ) cmd.cmdArgs.push_back( std::to_string( offWaitTime->valueint ) );
    }
    else { return false; }

    cmd.cmdArgs.push_back( std::to_string( nodeId ) );
    cmd.cmdArgs.push_back( std::to_string( endpointId ) );
    commands.push_back( cmd );
    return true;
}

Attribute report handler:

case OnOff::Id:
    switch( attributeId )
    {
        case OnOff::Attributes::OnOff::Id:
            return parseBoolPropertyValue( attributeValue, reportString );
        case OnOff::Attributes::GlobalSceneControl::Id:
            return parseBoolPropertyValue( attributeValue, reportString );
        case OnOff::Attributes::OnTime::Id:
        case OnOff::Attributes::OffWaitTime::Id:
            return parseNumericPropertyValue( attributeValue, reportString );
        case OnOff::Attributes::StartUpOnOff::Id:
            return parseEnumPropertyValue( attributeValue,
                OnOff::Mappings::g_StartUpOnOffEnumMappings, reportString );
        default:
            return parseNumericPropertyValue( attributeValue, reportString );
    }

Key observations:

  • Simple commands (off, on, toggle) need no parameters — just the cluster and command name
  • Writable attributes are converted to chip-tool write commands with dash-case attribute names
  • Enum attributes use findEnumValue() (cloud→device) and parseEnumPropertyValue() (device→cloud)
  • Boolean attributes use parseBoolPropertyValue() for reporting

Registration Checklist

When adding a new cluster, you must update these files. Use this checklist:

# File What to Add Required?
1 include/zzz_generated/clusters/<Name>/ 5 header files (ClusterId.h, AttributeIds.h, CommandIds.h, EventIds.h, Ids.h) Always
--- --- --- ---
2 matter_action_converter.cpp (top) #include for new cluster Ids.h Always
3 matter_action_converter.cpp (top) Enum/bitmap mapping tables in cluster namespace If cluster has enum/bitmap types
4 matter_action_converter.cpp processUpdateStateAction() else if block for writable attributes If cluster has writable attributes
5 matter_action_converter.cpp processRegularAction() else if block for cluster commands If cluster has commands
6 matter_action_converter.cpp convertAttributeToReportString() case block for attribute reporting Always
7 Run tools/generate_cpp_schemas.py Regenerate cluster definitions Always (if schema exists)

Note: Unlike Z-Wave (7+ registration points across 4 files) and Zigbee (7 registration points across 4 files), the Matter plugin concentrates all hand-written changes in a single file (matter_action_converter.cpp) at 4 locations.


Tips and Common Pitfalls

  • chip-tool uses dash-case for everything. Cluster names, command names, and attribute names are all dash-case in chip-tool (e.g., level-control, move-to-level, on-time). The cloud uses PascalCase. The framework converts action names automatically, but you must use dash-case for attribute names in write commands.
  • Command parameters use positional indices. Cloud sends parameters as {"0": value, "1": value}. Extract them with cJSON_GetObjectItem(parameters, "0").
  • Enum mapping tables must have a sentinel. Always terminate FieldEnumMapping arrays with { UNKNOWN_ENUM_VALUE, <default> } and FieldBitMapping arrays with { nullptr, 0 }.
  • The ClusterRegistry is for capability reporting only. It does NOT drive command dispatch. Even if a cluster is in the registry (88 clusters), it won't handle commands unless you add the dispatch code in matter_action_converter.cpp.
  • Read-only clusters still need attribute report handling. Even if a cluster has no writable attributes and no commands (like BooleanState), you still need to add a case in convertAttributeToReportString() so the plugin can report attribute values from subscriptions.
  • Some commands need --timedInteractionTimeoutMs. Matter commands that modify security-sensitive state (e.g., DoorLock) require timed interactions. Add --timedInteractionTimeoutMs 1000 to the command args when needed.
  • Test with actual chip-tool output. The text format of chip-tool output can vary. Always verify your convertAttributeToReportString() parsing against real chip-tool output.
  • The device model is generic. You do NOT need to modify Device, Endpoint, Cluster, or Attribute classes. They work with any cluster automatically.
  • Use existing clusters as templates. OnOff is the simplest, LevelControl is medium complexity, DoorLock is the most complex. Copy the pattern that matches your cluster's complexity.

Reference: Currently Supported Clusters

Clusters with Full Command Dispatch

These clusters have complete support in matter_action_converter.cpp for UpdateState, regular commands, and attribute reporting:

Cluster ID Commands Writable Attrs Enum/Bitmap Types
Identify 0x0003 identify, trigger-effect identifyTime 3 enums
--- --- --- --- ---
On/Off 0x0006 off, on, toggle, off-with-effect, on-with-recall-global-scene, on-with-timed-off onTime, offWaitTime, startUpOnOff 6 enums/bitmaps
Level Control 0x0008 move-to-level, move, step, stop, + with-on-off variants, move-to-closest-frequency 7 writable attrs 4 enums/bitmaps
Boolean State 0x0045 (none — read-only) (none) (none)
Door Lock 0x0101 lock-door, unlock-door, unlock-with-timeout, + schedule/user/credential management 19 writable attrs 30+ enums/bitmaps
Thermostat 0x0201 setpoint-raise-lower, set/get/clear-weekly-schedule, + preset/schedule management 25+ writable attrs 20+ enums/bitmaps
Color Control 0x0300 19 commands (move-to-hue, move-color, etc.) 13 writable attrs 10+ enums/bitmaps

Clusters in Registry Only (Capability Reporting, No Command Dispatch)

The ClusterRegistry contains 88 cluster definitions for capability reporting. Clusters not listed above are recognized for capability reports but do NOT have command dispatch support. Adding command dispatch requires the steps in this guide.

Architecture Comparison

Aspect Z-Wave Plugin Zigbee Plugin Matter Plugin
Language C C C++
--- --- --- ---
Protocol translation Z-Wave CC → Matter cluster Zigbee ZCL → Matter cluster Matter → Matter (format only)
Cluster files One .c per CC One .c per cluster Single file for all clusters
Dispatch mechanism 6 dispatch tables + switch/case 1 dispatch table (data-driven) if/else-if chains + switch/case
Custom functions per cluster 4 (Send, HandleReport, Control, Map) 0 for most (data-only) 0 separate functions (inline in if/else)
Registration points 7 across 4 files 7 across 4 files 4 in 1 file + 2 auto-generated
Auto-generated code None None ClusterRegistry (88 clusters) + ZAP headers
Device communication Direct Z-Wave radio Direct Zigbee radio chip-tool subprocess