https://github.com/minhtuannguyen/node-red-contrib-matter
A generic Matter protocol controller for Node-RED.
https://github.com/minhtuannguyen/node-red-contrib-matter
home-automation matter nodered typescript
Last synced: 3 months ago
JSON representation
A generic Matter protocol controller for Node-RED.
- Host: GitHub
- URL: https://github.com/minhtuannguyen/node-red-contrib-matter
- Owner: minhtuannguyen
- License: apache-2.0
- Created: 2026-05-01T13:29:28.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-05-01T18:07:37.000Z (3 months ago)
- Last Synced: 2026-05-01T18:25:48.986Z (3 months ago)
- Topics: home-automation, matter, nodered, typescript
- Language: TypeScript
- Homepage:
- Size: 60.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# node-red-contrib-matter

A generic [Matter](https://csa-iot.org/all-solutions/matter/) protocol controller for [Node-RED](https://nodered.org/).
Commission and control **any Matter device** — smart locks, lights, sensors — directly from Node-RED flows over IP or Thread.
Built on [matter.js](https://github.com/project-chip/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 GitHub
```bash
cd ~/.node-red
npm install github:minhtuannguyen/node-red-contrib-matter
```
### From a local clone
```bash
git clone https://github.com/minhtuannguyen/node-red-contrib-matter.git
cd ~/.node-red
npm install ../node-red-contrib-matter
```
> **Note:** The compiled `dist/` folder is included in the repository, so no TypeScript compiler is needed on the target machine.
Then restart Node-RED:
```bash
node-red-restart
# or
sudo systemctl restart nodered
```
---
## Nodes
### `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`:**
```json
{ "nodeId": "", "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`:**
```json
{
"nodeId": "",
"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:**
```json
{
"type": "attribute",
"nodeId": "7825526669137635300",
"endpointId": 1,
"clusterId": 257,
"attributeName": "lockState",
"value": 1,
"timestamp": "2026-05-01T17:00:00.000Z"
}
```
**Output `msg.payload` — event:**
```json
{
"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`:**
```json
{ "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
```
1. **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.
2. **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.
3. **Refresh the registry** at any time by triggering a `matter-discover` node. 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.
4. **Decommission** with `matter-decommission` — this removes the device from both the Matter fabric and the registry.
### Registry file
Persisted at `/node-red-matter/registry.json`
Default: `~/.node-red-matter/node-red-matter/registry.json`
```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-discover` with `msg.nodeId` set to the commissioned node ID, and re-open the dialog.
- **Dropdown shows a device but no endpoints** — the registry entry has no `discovery` data yet. Run `matter-discover` to populate it.
---
## Example: Nuki Smart Lock
Import `examples/nuki-lock.json` into Node-RED for a ready-made flow covering:
1. Commission the lock (one-time)
2. Discover and register in the device registry
3. Lock / Unlock / Unlock with timeout
4. Read `lockState` on demand
5. Subscribe to `lockState` changes and `doorLockAlarm` events
**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:
```javascript
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:
```json
{
"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
```bash
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 mode
```
Install as a live symlink into Node-RED for development:
```bash
cd ~/.node-red/node_modules
ln -s /path/to/node-red-contrib-matter node-red-contrib-matter
```
---
## License
Apache 2.0