@minguyen68/node-red-contrib-matter
v0.1.0
Published
Generic Matter protocol controller for Node-RED — commission and control any Matter device (locks, lights, sensors) over IP/Thread
Maintainers
Readme
node-red-contrib-matter

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 (@project-chip/matter.js).
Features
- 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
- Decommission devices cleanly from the fabric, or force-remove when offline
- Works on Raspberry Pi (Ethernet or WiFi) and any Node.js-capable Linux/macOS host
- Thread devices reachable via a Thread Border Router (HomePod, Apple TV, OTBR)
Prerequisites
| Requirement | Version | |---|---| | Node.js | ≥ 20.0.0 | | Node-RED | ≥ 3.0.0 |
Installation
From npm
cd ~/.node-red
npm install @minguyen68/node-red-contrib-matterFrom GitHub
cd ~/.node-red
npm install github:minhtuannguyen/node-red-contrib-matterFrom a local clone
git 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 noderedNodes
matter-controller (config node)
Shared 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.
matter-commission
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
matter-discover
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.
matter-command
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 } |
matter-read
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.
matter-subscribe
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"
}matter-decommission
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 |
Device Registry
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.
How it works
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.
Registry file
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", ...]
}
]
}
]
}
}
}Registry lifecycle
| 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 |
Troubleshooting the dropdowns
- "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.
Example: Nuki Smart Lock
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;Thread devices (HomePod, Apple TV as Border Router)
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.
Development
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-matterLicense
Apache 2.0
