A generic Matter protocol controller for Node-RED.
Commission and control any Matter device — smart locks, lights, sensors — directly from Node-RED flows over IP or Thread.
Built on matter.js.
- Commission any Matter device into your own fabric (multi-admin alongside Apple Home, Google Home, etc.)
- Device registry — after commissioning, devices are auto-discovered and stored; all other nodes show a device dropdown instead of requiring manual ID entry
- Send commands to any cluster and endpoint (lock/unlock, on/off, scenes, …)
- Read attributes on demand (lock state, brightness, temperature, …)
- Subscribe to real-time attribute changes and events
- Synchronize time on devices that support the TimeSynchronization cluster (0x0038) — e.g. IKEA ALPSTUGA
- Decommission devices cleanly from the fabric, or force-remove when offline
- Thread devices reachable via a Thread Border Router (HomePod, Apple TV, OTBR)
| Requirement | Version |
|---|---|
| Node.js | ≥ 20.0.0 |
| Node-RED | ≥ 3.0.0 |
cd ~/.node-red
npm install @minguyen68/node-red-contrib-mattercd ~/.node-red
npm install github:minhtuannguyen/node-red-contrib-mattergit clone https://github.com/minhtuannguyen/node-red-contrib-matter.git
cd ~/.node-red
npm install ../node-red-contrib-matterNote: The compiled
dist/folder is included in the repository, so no TypeScript compiler is needed on the target machine.
Then restart Node-RED:
node-red-restart
# or
sudo systemctl restart noderedShared configuration node. Manages the Matter controller lifecycle — one per Node-RED instance.
| Property | Description | Default |
|---|---|---|
| Storage Path | Directory where fabric credentials and device registry are persisted | ~/.node-red-matter |
| UDP Port | Matter controller port | 5540 |
| Log Level | matter.js log verbosity | Info |
The controller also serves an internal HTTP endpoint (GET /matter-nodes/:id/registry) used by the edit dialogs of all other nodes to populate their device dropdowns. This requires the controller node to be deployed before opening any other node's edit dialog.
Commission a Matter device into the Node-RED controller fabric.
After successful commissioning the device is automatically registered in the device registry so it immediately appears in the dropdowns of the command, read, subscribe, and decommission nodes.
Input msg.payload:
| Field | Type | Description |
|---|---|---|
pairingCode |
string | 11-digit manual pairing code (hyphens optional). Overrides node config. |
knownAddress |
string | (optional) IPv6/IPv4 address to skip mDNS discovery. Useful for Thread devices. |
Output msg.payload:
{ "nodeId": "<commissioned-node-id>", "endpoints": [...] }Before commissioning, open a commissioning window from your primary controller:
Apple Home → device → ⚙️ → Turn on Pairing Mode
Discovers all endpoints, clusters, attributes, commands, and events of a commissioned device and saves the result to the device registry. Run this node once after commissioning (or any time you want to refresh the registry) so the dropdowns in the other nodes reflect the device's current structure.
Input: any message (triggers the discovery).
Override nodeId at runtime via msg.nodeId.
Output msg.payload:
{
"nodeId": "<commissioned-node-id>",
"endpoints": [
{
"endpointId": 0,
"clusters": [
{
"clusterId": 40,
"clusterIdHex": "0028",
"clusterName": "BasicInformation",
"attributes": ["vendorName", "productName", "serialNumber", ...],
"commands": [],
"events": []
}
]
},
{
"endpointId": 1,
"clusters": [
{
"clusterId": 257,
"clusterIdHex": "0101",
"clusterName": "DoorLock",
"attributes": ["lockState", "lockType", "doorState", ...],
"commands": ["lockDoor", "unlockDoor", "unlockWithTimeout", ...],
"events": ["doorLockAlarm", "lockOperation", ...]
}
]
}
]
}Connect the output to a Debug node set to "complete msg object" to inspect the full structure.
Tip: If the device dropdown in another node shows "select a controller first" or is empty after commissioning, run this node with the device's
nodeId, then re-open the edit dialog.
Send a command to a cluster on a commissioned device.
The edit dialog shows cascading dropdowns (Device → Endpoint → Cluster → Command) populated from the device registry. The selected values fill the underlying text fields; all fields can also be overridden at runtime via msg:
| Field | Description | Example |
|---|---|---|
nodeId |
Commissioned node ID | 7825526669137635300 |
endpointId |
Endpoint number | 1 |
clusterId |
Cluster ID (hex) | 0101 (DoorLock) |
commandName |
Command name (camelCase) | lockDoor, unlockDoor |
payload |
Command arguments object | { "timeout": 30 } |
Read an attribute value from a commissioned device on any input message.
The edit dialog shows cascading dropdowns (Device → Endpoint → Cluster → Attribute).
| Field | Description | Example |
|---|---|---|
nodeId |
Commissioned node ID | 7825526669137635300 |
endpointId |
Endpoint number | 1 |
clusterId |
Cluster ID (hex) | 0101 |
attributeName |
Attribute name (camelCase) | lockState |
Output msg.payload: the raw attribute value.
Subscribes to real-time attribute changes and/or events from a device. Starts automatically on deploy, no input needed.
The edit dialog shows cascading dropdowns (Device → Endpoint → Cluster → Attribute / Event). All filter fields are optional — leave blank to receive everything from the device.
| Field | Description | Example |
|---|---|---|
nodeId |
Commissioned node ID (required) | 7825526669137635300 |
endpointId |
(optional) Filter by endpoint | 1 |
clusterId |
(optional) Filter by cluster (hex) | 0101 |
attributeName |
(optional) Filter by attribute | lockState |
eventName |
(optional) Filter by event | doorLockAlarm |
Output msg.payload — attribute change:
{
"type": "attribute",
"nodeId": "7825526669137635300",
"endpointId": 1,
"clusterId": 257,
"attributeName": "lockState",
"value": 1,
"timestamp": "2026-05-01T17:00:00.000Z"
}Output msg.payload — event:
{
"type": "event",
"nodeId": "7825526669137635300",
"endpointId": 1,
"clusterId": 257,
"eventName": "doorLockAlarm",
"events": [...],
"timestamp": "2026-05-01T17:00:00.000Z"
}Synchronizes the UTC time and timezone on a commissioned Matter device that supports the TimeSynchronization cluster (0x0038). Compatible devices include the IKEA ALPSTUGA air quality sensors.
Any incoming message triggers a sync. The node executes three commands in sequence:
SetTimeZone— sends the host UTC offset (DST already included) withvalidAt = 0(valid from Matter epoch start, 2000-01-01). This resets the device's internal time-source state so it is required to accept the subsequentSetUTCTimeper Matter spec §11.17.9.1. Silently skipped on devices that do not support the TimeZone feature.SetDSTOffset []— clears the DST table (DST is already folded into the UTC offset above). Silently skipped on devices without the TimeZone feature.SetUTCTime— sends the current time as Unix epoch-microseconds withgranularity = MicrosecondsGranularityandtimeSource = Admin. matter.js converts to Matter epoch (2000-01-01) internally before encoding on the wire.
The node creates no timers. Use an Inject node set to repeat for scheduled synchronization.
Input: any message (triggers the sync).
| Runtime override | Type | Description |
|---|---|---|
msg.nodeId |
string | Decimal Matter node ID — overrides the device configured in the node properties. |
Output msg.payload:
{ "synced": true, "syncedAt": "2026-06-10T12:34:56.789Z" }| Field | Type | Description |
|---|---|---|
synced |
boolean | Always true on success. |
syncedAt |
string | ISO-8601 UTC timestamp of when the sync completed. |
msg.topic: "time-sync"
Device dropdown: the edit dialog only lists devices that have the TimeSynchronization cluster (0x0038) in their discovery data. Devices that have not been discovered yet are also listed (no cluster data to filter on); they will fail at runtime if the cluster is absent.
Known compatible devices:
| Device | Cluster | Notes |
|---|---|---|
| IKEA ALPSTUGA | 0x0038 | Requires SetTimeZone before SetUTCTime |
Decommission (unpair) a Matter device from the controller fabric and remove it from the device registry.
The edit dialog shows a Device dropdown populated from the registry and a Force checkbox.
Input msg.payload:
| Field | Type | Description |
|---|---|---|
nodeId |
string | Decimal node ID of the device to remove. Overrides node config. |
force |
boolean | When true, skips the fabric-level RemoveFabric command and only erases local storage. Use when the device is offline or already factory-reset. Default: false. |
Output msg.payload:
{ "ok": true, "nodeId": "7825526669137635300", "force": false }| Mode | Behaviour |
|---|---|
| Normal (force = false) | Sends RemoveFabric to the device, then erases local storage and registry entry |
| Force (force = true) | Erases local storage and registry entry only — device must be manually factory-reset |
The device registry is the central store that connects the matter-commission node to all other nodes. Instead of manually typing node IDs, cluster IDs, and attribute names, every node's edit dialog loads the registry and shows human-readable cascading dropdowns.
commission → [auto-discover in background] → registry.json
↓
command / read / subscribe / decommission / discover
edit dialogs load registry → device dropdown
- Commission a device with
matter-commission. Immediately after the pairing handshake completes, the node triggers a background discovery that writes the device's label, node ID, and full cluster/attribute/command structure to the registry. - Open any other node's edit dialog. The dialog fetches the registry from the controller's HTTP endpoint and populates a Device dropdown. Selecting a device pre-fills the node ID. Nodes that need more detail (command, read, subscribe) show further cascading dropdowns: Endpoint → Cluster → Attribute / Command / Event.
- Refresh the registry at any time by triggering a
matter-discovernode. This is useful if the background discovery after commissioning was slow (e.g. the device rebooted after joining the fabric) or if the device firmware updated and exposed new clusters. - Decommission with
matter-decommission— this removes the device from both the Matter fabric and the registry.
Persisted at <storagePath>/node-red-matter/registry.json
Default: ~/.node-red-matter/node-red-matter/registry.json
{
"7825526669137635300": {
"label": "Nuki Smart Lock",
"nodeId": "7825526669137635300",
"discoveredAt": "2026-05-01T12:00:00.000Z",
"discovery": {
"nodeId": "7825526669137635300",
"endpoints": [
{
"endpointId": 1,
"clusters": [
{
"clusterId": 257,
"clusterIdHex": "0101",
"clusterName": "DoorLock",
"attributes": ["lockState", "lockType", ...],
"commands": ["lockDoor", "unlockDoor", ...],
"events": ["doorLockAlarm", "lockOperation", ...]
}
]
}
]
}
}
}| Event | Registry change |
|---|---|
matter-commission completes |
Entry added (background discovery, may take a few seconds) |
matter-discover is triggered |
Entry updated with latest cluster structure |
matter-decommission runs |
Entry removed |
| Node-RED restart | Registry loaded from disk — no re-discovery needed |
- "select a controller first" — no controller is selected or the controller config node is not deployed yet.
- "deploy the flow first, then re-open this dialog" — the controller node has not been deployed. Click Deploy then re-open the edit dialog.
- Device not in dropdown after commissioning — the background discovery may still be in progress (the device reboots after joining the fabric). Wait 10–15 seconds, then trigger
matter-discoverwithmsg.nodeIdset to the commissioned node ID, and re-open the dialog. - Dropdown shows a device but no endpoints — the registry entry has no
discoverydata yet. Runmatter-discoverto populate it.
Import examples/nuki-lock.json into Node-RED for a ready-made flow covering:
- Commission the lock (one-time)
- Discover and register in the device registry
- Lock / Unlock / Unlock with timeout
- Read
lockStateon demand - Subscribe to
lockStatechanges anddoorLockAlarmevents
DoorLock cluster reference:
| Property | Value |
|---|---|
| Cluster ID | 0101 |
| Endpoint | 1 |
| Commands | lockDoor, unlockDoor, unlockWithTimeout |
lockState values |
0 = NotFullyLocked, 1 = Locked, 2 = Unlocked, 3 = Unlatched |
Battery level (PowerSource cluster):
| Property | Value |
|---|---|
| Cluster ID | 002F |
| Endpoint | 1 |
| Attribute | batPercentRemaining |
| Raw value | 0–200 (divide by 2 for %) — e.g. 170 → 85% |
batChargeLevel |
0 = OK, 1 = Warning, 2 = Critical |
Add a Function node after matter-read to convert the raw value:
msg.payload = msg.payload / 2; // e.g. 170 → 85
return msg;If your device is Thread-only (e.g. Nuki Smart Lock connected via HomePod), standard mDNS commissioning discovery won't bridge from Thread to your WiFi/Ethernet network. Pass the device's IPv6 address directly:
{
"pairingCode": "1234-567-8901",
"knownAddress": "fd00::1234"
}Find the IPv6 address using avahi-browse -rt _matterc._udp (Linux) or dns-sd -B _matterc._udp local (macOS) while the commissioning window is open.
git clone https://github.com/minhtuannguyen/node-red-contrib-matter.git
cd node-red-contrib-matter
npm install
npm run build # compile TypeScript → dist/
npm run dev # watch modeInstall as a live symlink into Node-RED for development:
cd ~/.node-red/node_modules
ln -s /path/to/node-red-contrib-matter node-red-contrib-matterApache 2.0
