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:
The plugin must convert this to chip-tool CLI arguments:
And when the device reports its lock state, chip-tool outputs:
The plugin must convert this back to IoTMI JSON:
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):
- Cloud sends an IoTMI control task with JSON payload
controlTaskCallback()parses the JSON into action requests-
convertActionToCommand()dispatches based on cluster name:- UpdateState (0xff01):
processUpdateStateAction()— converts attribute writes to chip-toolwritecommands - ReadState (0xff02):
processReadStateAction()— converts to chip-toolreadcommands - Regular commands:
processRegularAction()— converts cluster commands to chip-tool cluster-specific commands
- UpdateState (0xff01):
-
ChiptoolManagersubmits the command to chip-tool's stdin
Report path (device → cloud):
- chip-tool outputs attribute reports on stdout (from subscriptions or reads)
ChiptoolParserparses the text intoMatterPluginResponsestructsDeviceManagerroutes responses to the correctDevice→Endpoint→Cluster→AttributeconvertAttributeToReportString()converts chip-tool text values to IoTMI JSON format (enum names, bitmap objects, etc.)DeviceJsonReportergenerates 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:
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:
- Check the parameter name
- Build a chip-tool
writecommand:{<cluster>, "write", <attribute-name>, <value>, <nodeId>, <endpointId>} - Convert enum/bitmap values using the mapping tables
- 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:
- Start with
{<cluster>, <command-name>} - Extract parameters from JSON and append as string arguments
- Append
<nodeId>and<endpointId>at the end - 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:
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:
- Ensure the cluster's JSON schema exists in the schema source (retrieved via
tools/retrieve_schemas.py) - Run the code generation tool:
This regenerates:
include/zzz_generated/iotmi/matter_cluster_definitions.h— cluster namespace constantssrc/matter_plugin/zzz_generated/iotmi/matter_cluster_definitions.cpp— ClusterRegistry initialization
The generated code provides:
ClusterRegistry::getCluster("0x0102")— lookup by extrinsic IDClusters::Window_Covering::NAME,ID,EXTRINSIC_ID,VERSION— namespace constantsClusters::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
writecommands with dash-case attribute names - Enum attributes use
findEnumValue()(cloud→device) andparseEnumPropertyValue()(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 inwritecommands. - Command parameters use positional indices. Cloud sends parameters as
{"0": value, "1": value}. Extract them withcJSON_GetObjectItem(parameters, "0"). - Enum mapping tables must have a sentinel. Always terminate
FieldEnumMappingarrays with{ UNKNOWN_ENUM_VALUE, <default> }andFieldBitMappingarrays 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
caseinconvertAttributeToReportString()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 1000to 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, orAttributeclasses. 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 |