@airnexus/node-red-contrib-matter-airnexus 0.2.4-airnexus.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +327 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/icons/matter-device-icon.svg +3 -0
- package/matter-bridge.html +263 -0
- package/matter-bridge.js +475 -0
- package/matter-device.html +138 -0
- package/matter-device.js +880 -0
- package/matter-pairing.html +54 -0
- package/matter-pairing.js +275 -0
- package/package.json +41 -0
- package/utils.js +30 -0
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Node-RED Matter Dynamic - Architecture Documentation
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This project implements dynamic Matter device nodes for Node-RED, allowing users to create Matter devices by specifying only the device type in JSON configuration, without hardcoding device-specific characteristics.
|
|
6
|
+
|
|
7
|
+
## Key Components
|
|
8
|
+
|
|
9
|
+
### 1. Matter Bridge (`matter-bridge.js` / `matter-bridge.html`)
|
|
10
|
+
- **Purpose**: Creates a Matter Bridge that hosts multiple dynamic devices
|
|
11
|
+
- **Key Features**:
|
|
12
|
+
- Manages Matter server lifecycle
|
|
13
|
+
- Handles device registration via `registerChild()` function
|
|
14
|
+
- Provides HTTP endpoints for commissioning QR codes
|
|
15
|
+
- Auto-starts when all registered devices are ready
|
|
16
|
+
|
|
17
|
+
### 2. Matter Device (`matter-device.js` / `matter-device.html`)
|
|
18
|
+
- **Purpose**: Generic node that can represent any Matter device type
|
|
19
|
+
- **Configuration**: Simple JSON specifying only `deviceType` (e.g., "OnOffLightDevice")
|
|
20
|
+
- **Key Features**:
|
|
21
|
+
- Dynamically subscribes to all device events
|
|
22
|
+
- Maps Matter.js cluster structure for input/output
|
|
23
|
+
- No hardcoded device-specific logic
|
|
24
|
+
|
|
25
|
+
## Architecture Design
|
|
26
|
+
|
|
27
|
+
### CRITICAL DESIGN PRINCIPLE: NO HARDCODED DEVICE-SPECIFIC LOGIC
|
|
28
|
+
**This system MUST remain completely generic. NEVER hardcode behaviors, attributes, or logic for specific device types. All device capabilities must be discovered and handled dynamically at runtime.**
|
|
29
|
+
|
|
30
|
+
### Device Creation Flow
|
|
31
|
+
```
|
|
32
|
+
1. User configures device with JSON: {"deviceType": "OnOffLightDevice"}
|
|
33
|
+
2. Device node waits for bridge to be ready
|
|
34
|
+
3. Creates Matter.js Endpoint with specified device type
|
|
35
|
+
4. Registers with bridge using registerChild()
|
|
36
|
+
5. Bridge adds device to aggregator
|
|
37
|
+
6. On server ready, device subscribes to all events dynamically
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Event System
|
|
41
|
+
- **Input**: Expects Matter.js cluster format (e.g., `{onOff: {onOff: true}}`)
|
|
42
|
+
- **Output**: Emits changes in same format when device state changes
|
|
43
|
+
- **Dynamic Subscription**: Iterates through all clusters and subscribes to `$Changed` events
|
|
44
|
+
|
|
45
|
+
### Key Differences from Static Approach
|
|
46
|
+
- No separate node for each device type
|
|
47
|
+
- No hardcoded cluster names or attributes
|
|
48
|
+
- All device capabilities discovered at runtime
|
|
49
|
+
- Single codebase handles all Matter device types
|
|
50
|
+
|
|
51
|
+
## Current Issues & Solutions
|
|
52
|
+
|
|
53
|
+
### ✅ Issue 4: Video Player Commands Not Implemented (RESOLVED)
|
|
54
|
+
- **Symptom**: Matter.js logs "Throws unimplemented exception" for play/pause/stop/sendKey
|
|
55
|
+
- **Root Cause**: Behaviors with commands require method implementations
|
|
56
|
+
- **Solution**: Dynamically patch ALL behavior prototypes at module load time:
|
|
57
|
+
- Iterate through all behaviors that have cluster.commands
|
|
58
|
+
- Add command method implementations before devices are loaded
|
|
59
|
+
- Each method sends command to Node-RED and returns appropriate response
|
|
60
|
+
```javascript
|
|
61
|
+
// Patch behaviors before loading devices
|
|
62
|
+
Object.values(matterBehaviors).forEach(BehaviorClass => {
|
|
63
|
+
if (BehaviorClass?.cluster?.commands) {
|
|
64
|
+
Object.entries(BehaviorClass.cluster.commands).forEach(([cmd, def]) => {
|
|
65
|
+
BehaviorClass.prototype[cmd] = async function(request) {
|
|
66
|
+
// Send to Node-RED
|
|
67
|
+
// Return success
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
- **Note**: Video player devices are part of Matter 1.4 spec but may not be supported by all controllers yet.
|
|
74
|
+
|
|
75
|
+
### ✅ Issue 1: Events Not Firing (RESOLVED)
|
|
76
|
+
- **Symptom**: Matter.js logs showed commands but Node-RED didn't emit messages
|
|
77
|
+
- **Root Cause**: Event properties ending with `$Changed` are not enumerable in Matter.js
|
|
78
|
+
- **Solution**: Subscribe to events based on state attributes instead of iterating event properties
|
|
79
|
+
```javascript
|
|
80
|
+
// Instead of iterating events, use state attributes:
|
|
81
|
+
if (node.device.state && node.device.state[clusterName]) {
|
|
82
|
+
Object.keys(node.device.state[clusterName]).forEach(attributeName => {
|
|
83
|
+
const eventName = `${attributeName}$Changed`;
|
|
84
|
+
if (clusterEvents[eventName]) {
|
|
85
|
+
clusterEvents[eventName].on(handler);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### ✅ Issue 2: Multiple Event Emissions (RESOLVED)
|
|
92
|
+
- **Symptom**: Each HomeKit action triggered 3 identical messages
|
|
93
|
+
- **Root Cause**: `serverReady` event emitted multiple times when devices register
|
|
94
|
+
- **Solution**: Added flag to prevent multiple subscriptions
|
|
95
|
+
```javascript
|
|
96
|
+
if (node.eventsSubscribed) {
|
|
97
|
+
return; // Prevent duplicate subscriptions
|
|
98
|
+
}
|
|
99
|
+
node.eventsSubscribed = true;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Code Structure
|
|
103
|
+
|
|
104
|
+
### Bridge Registration
|
|
105
|
+
```javascript
|
|
106
|
+
// Bridge exposes function
|
|
107
|
+
node.registerChild = function(child) {
|
|
108
|
+
// Add to registered devices
|
|
109
|
+
// Add device to aggregator
|
|
110
|
+
// Start server when ready
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Device calls it
|
|
114
|
+
node.bridge.registerChild(node);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Dynamic Event Subscription
|
|
118
|
+
```javascript
|
|
119
|
+
// Events ending with $Changed are not enumerable, so we use state attributes
|
|
120
|
+
Object.keys(node.device.events).forEach(clusterName => {
|
|
121
|
+
if (node.device.state && node.device.state[clusterName]) {
|
|
122
|
+
Object.keys(node.device.state[clusterName]).forEach(attributeName => {
|
|
123
|
+
const eventName = `${attributeName}$Changed`;
|
|
124
|
+
if (node.device.events[clusterName][eventName]) {
|
|
125
|
+
// Subscribe to state change
|
|
126
|
+
node.device.events[clusterName][eventName].on((value, oldValue, context) => {
|
|
127
|
+
const msg = { payload: {} };
|
|
128
|
+
msg.payload[clusterName] = {};
|
|
129
|
+
msg.payload[clusterName][attributeName] = value;
|
|
130
|
+
node.send(msg);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Event Cleanup
|
|
139
|
+
Proper cleanup on node close to prevent memory leaks:
|
|
140
|
+
```javascript
|
|
141
|
+
// Store handlers for cleanup
|
|
142
|
+
node.eventHandlers[`${clusterName}.${eventName}`] = handler;
|
|
143
|
+
|
|
144
|
+
// On close, remove all event listeners
|
|
145
|
+
for (const [handlerKey, handler] of Object.entries(node.eventHandlers)) {
|
|
146
|
+
const [clusterName, eventName] = handlerKey.split('.');
|
|
147
|
+
await node.device.events[clusterName][eventName].off(handler);
|
|
148
|
+
}
|
|
149
|
+
node.removeAllListeners('serverReady');
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Payload Format Examples
|
|
153
|
+
|
|
154
|
+
### Light Control
|
|
155
|
+
```javascript
|
|
156
|
+
// Input to turn on
|
|
157
|
+
msg.payload = {onOff: {onOff: true}}
|
|
158
|
+
|
|
159
|
+
// Output when state changes
|
|
160
|
+
msg.payload = {onOff: {onOff: false}}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Temperature Sensor
|
|
164
|
+
```javascript
|
|
165
|
+
// Input to set temperature (21.5°C)
|
|
166
|
+
msg.payload = {temperatureMeasurement: {measuredValue: 2150}}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Key Implementation Details
|
|
170
|
+
|
|
171
|
+
### Event Discovery
|
|
172
|
+
Matter.js events ending with `$Changed` are not enumerable properties. The solution uses the device state to discover available attributes and then checks for corresponding events:
|
|
173
|
+
|
|
174
|
+
1. Iterate through `node.device.state` clusters
|
|
175
|
+
2. For each attribute in the state, check if `${attribute}$Changed` event exists
|
|
176
|
+
3. Subscribe only to existing events
|
|
177
|
+
|
|
178
|
+
### Performance Considerations
|
|
179
|
+
- Single event subscription per attribute (prevented by flag)
|
|
180
|
+
- Proper cleanup prevents memory leaks
|
|
181
|
+
- 2-second delay after `serverReady` ensures Matter.js initialization
|
|
182
|
+
|
|
183
|
+
### ✅ Issue 3: Matter.js Validation Errors Crash Node-RED (RESOLVED)
|
|
184
|
+
- **Symptom**: Node-RED crashes when creating devices with missing mandatory attributes
|
|
185
|
+
- **Root Cause**: Unhandled Promise rejection from Matter.js validation
|
|
186
|
+
- **Solution**: Added unhandled rejection handler in bridge to catch Matter.js errors
|
|
187
|
+
```javascript
|
|
188
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
189
|
+
if (reason && reason.message && reason.message.includes('Behaviors have errors')) {
|
|
190
|
+
// Handle gracefully, mark device as failed
|
|
191
|
+
// Prevent Node-RED crash
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Command Handling Architecture
|
|
197
|
+
|
|
198
|
+
### Dynamic Command Interception
|
|
199
|
+
The system dynamically intercepts ALL commands for ANY device type without hardcoding:
|
|
200
|
+
1. At module load time, patches all behavior prototypes that have commands
|
|
201
|
+
2. Each command method sends message to Node-RED when invoked
|
|
202
|
+
3. Returns appropriate Matter response based on command schema
|
|
203
|
+
4. Works for all current and future device types
|
|
204
|
+
|
|
205
|
+
### Two-Output System
|
|
206
|
+
Device nodes have two outputs for different message types:
|
|
207
|
+
- **Output 1**: State change events (when attributes change)
|
|
208
|
+
- **Output 2**: Commands received from Matter controllers
|
|
209
|
+
|
|
210
|
+
This separation allows:
|
|
211
|
+
- Backward compatibility with existing flows using events
|
|
212
|
+
- Clear distinction between state changes and commands
|
|
213
|
+
- Easy routing of different message types
|
|
214
|
+
|
|
215
|
+
Example messages:
|
|
216
|
+
```javascript
|
|
217
|
+
// Output 1 - Event
|
|
218
|
+
{ onOff: { onOff: true } }
|
|
219
|
+
|
|
220
|
+
// Output 2 - Command
|
|
221
|
+
{ command: "on", cluster: "OnOff", data: undefined }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Controller Support Status
|
|
225
|
+
- **Fully Supported**: Lights, Switches, Sensors, Thermostats, Locks
|
|
226
|
+
- **Limited Support**:
|
|
227
|
+
- Video Players (Matter 1.4) - Not recognized by HomeKit/Tuya yet
|
|
228
|
+
- Advanced features may require specific controller support
|
|
229
|
+
|
|
230
|
+
## Enhanced Devices Architecture
|
|
231
|
+
|
|
232
|
+
### Overview
|
|
233
|
+
The system supports creating enhanced devices by adding additional behaviors/clusters to a primary device type using the `additionalBehaviors` configuration.
|
|
234
|
+
|
|
235
|
+
### Enhanced Device Implementation
|
|
236
|
+
Adds extra functionality to a primary device type:
|
|
237
|
+
```javascript
|
|
238
|
+
{
|
|
239
|
+
"deviceType": "ThermostatDevice",
|
|
240
|
+
"additionalBehaviors": ["PowerSourceServer", "RelativeHumidityMeasurementServer"]
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### How It Works
|
|
245
|
+
1. **Primary Device Type**: The main device type (e.g., ThermostatDevice)
|
|
246
|
+
2. **Additional Behaviors**: Extra clusters added to the same endpoint
|
|
247
|
+
3. **Behavior Aggregation**: All behaviors are merged into a single endpoint
|
|
248
|
+
4. **Single Endpoint**: Everything runs on one BridgedNodeEndpoint
|
|
249
|
+
|
|
250
|
+
### Example: Thermostat with Battery and Humidity
|
|
251
|
+
```javascript
|
|
252
|
+
// Configuration
|
|
253
|
+
{
|
|
254
|
+
"deviceType": "ThermostatDevice",
|
|
255
|
+
"additionalBehaviors": ["PowerSourceServer", "RelativeHumidityMeasurementServer"],
|
|
256
|
+
"behaviorFeatures": {
|
|
257
|
+
"Thermostat": ["Heating", "Cooling"],
|
|
258
|
+
"PowerSource": ["Battery", "Replaceable"],
|
|
259
|
+
"RelativeHumidityMeasurement": ["Percentage"]
|
|
260
|
+
},
|
|
261
|
+
"initialState": {
|
|
262
|
+
"thermostat": {
|
|
263
|
+
"localTemperature": 2000,
|
|
264
|
+
"systemMode": 4,
|
|
265
|
+
"occupiedHeatingSetpoint": 2000
|
|
266
|
+
},
|
|
267
|
+
"powerSource": {
|
|
268
|
+
"batPercentRemaining": 100,
|
|
269
|
+
"batChargeLevel": 1
|
|
270
|
+
},
|
|
271
|
+
"relativeHumidityMeasurement": {
|
|
272
|
+
"measuredValue": 5000
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Results in single endpoint with:
|
|
278
|
+
// - ThermostatServer (with Heating/Cooling features)
|
|
279
|
+
// - PowerSourceServer (with Battery features)
|
|
280
|
+
// - RelativeHumidityMeasurementServer
|
|
281
|
+
// - BridgedDeviceBasicInformationServer
|
|
282
|
+
// - IdentifyServer
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Benefits
|
|
286
|
+
- **Simple**: One endpoint manages all functionality
|
|
287
|
+
- **Compatible**: Works with all Matter controllers
|
|
288
|
+
- **Efficient**: No complex child endpoint management
|
|
289
|
+
- **Flexible**: Add only the behaviors you need
|
|
290
|
+
|
|
291
|
+
### Event Handling
|
|
292
|
+
All events come from the same endpoint:
|
|
293
|
+
```javascript
|
|
294
|
+
// Thermostat event
|
|
295
|
+
{ payload: { thermostat: { localTemperature: 2150 } } }
|
|
296
|
+
|
|
297
|
+
// Battery event
|
|
298
|
+
{ payload: { powerSource: { batPercentRemaining: 90 } } }
|
|
299
|
+
|
|
300
|
+
// Humidity event
|
|
301
|
+
{ payload: { relativeHumidityMeasurement: { measuredValue: 6000 } } }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Next Steps
|
|
305
|
+
|
|
306
|
+
1. **Add More Examples**: Create comprehensive examples for all device types
|
|
307
|
+
2. **Error Handling**: Better error messages for invalid configurations
|
|
308
|
+
- **Future Enhancement**: Parse Matter.js validation warnings to show specific missing attributes
|
|
309
|
+
- Example: Extract `fanModeSequence`, `percentCurrent` from validation logs
|
|
310
|
+
- Goal: Show user-friendly list of missing mandatory attributes
|
|
311
|
+
3. **Documentation**: Expand user documentation with more device examples
|
|
312
|
+
4. **Testing**: Add automated tests for various device types including composite devices
|
|
313
|
+
5. **Generic Command Handler**: Extend dynamic command handling to all clusters with commands
|
|
314
|
+
|
|
315
|
+
## Dependencies
|
|
316
|
+
|
|
317
|
+
- `@matter/main`: Core Matter.js implementation
|
|
318
|
+
- Node-RED >= 3.0.0
|
|
319
|
+
- Node.js >= 18.0.0
|
|
320
|
+
|
|
321
|
+
## Testing
|
|
322
|
+
|
|
323
|
+
Current test flow in `examples/flows.json` includes:
|
|
324
|
+
- OnOffLightDevice
|
|
325
|
+
- TemperatureSensorDevice
|
|
326
|
+
|
|
327
|
+
Both should respond to HomeKit commands and emit state changes to Node-RED.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 faxioman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
AirNexus Node-RED Matter Dynamic
|
|
2
|
+
|
|
3
|
+
Dynamic Matter bridge + dynamic devices for Node-RED. Create any Matter device by specifying the device type in JSON configuration — no need for device-specific nodes.
|
|
4
|
+
|
|
5
|
+
This AirNexus fork adds:
|
|
6
|
+
|
|
7
|
+
Matter Pairing node (get QR/manual codes, reopen pairing, factory reset commissioning)
|
|
8
|
+
|
|
9
|
+
Matter Device “hidden until enabled” mode (devices don’t appear in Alexa/HomeKit until you enable them by payload)
|
|
10
|
+
|
|
11
|
+
Dynamic rename by payload
|
|
12
|
+
|
|
13
|
+
Thermostat-friendly event handling (captures attribute writes even when controllers don’t send explicit commands)
|
|
14
|
+
|
|
15
|
+
Installation
|
|
16
|
+
npm install @airnexus/node-red-contrib-matter-airnexus
|
|
17
|
+
|
|
18
|
+
Requirements
|
|
19
|
+
|
|
20
|
+
Node.js >= 18.0.0
|
|
21
|
+
|
|
22
|
+
Node-RED >= 3.0.0
|
|
23
|
+
|
|
24
|
+
Nodes Included
|
|
25
|
+
1) Matter Dynamic Bridge (matter-dynamic-bridge)
|
|
26
|
+
|
|
27
|
+
Creates a Matter bridge and hosts all devices.
|
|
28
|
+
|
|
29
|
+
2) Matter Device (matter-device)
|
|
30
|
+
|
|
31
|
+
Creates a dynamic Matter endpoint from JSON config.
|
|
32
|
+
|
|
33
|
+
AirNexus behavior (Option 2 – hidden until enabled):
|
|
34
|
+
|
|
35
|
+
Device is created locally, but NOT registered into the bridge/aggregator until enabled
|
|
36
|
+
|
|
37
|
+
Once enabled, the device becomes visible in Alexa/HomeKit/Google
|
|
38
|
+
|
|
39
|
+
Disable sets reachable=false and suppresses outputs (device may remain listed in the controller until you remove it there)
|
|
40
|
+
|
|
41
|
+
3) Matter Pairing (matter-pairing)
|
|
42
|
+
|
|
43
|
+
Provides commissioning info (QR/manual) and supports “reset pairing” flows.
|
|
44
|
+
|
|
45
|
+
Quick Start
|
|
46
|
+
1) Create a Bridge
|
|
47
|
+
|
|
48
|
+
Add Matter Dynamic Bridge
|
|
49
|
+
|
|
50
|
+
Configure name/port/interface (default port 5540)
|
|
51
|
+
|
|
52
|
+
Deploy
|
|
53
|
+
|
|
54
|
+
2) Get Pairing Codes (recommended: use Matter Pairing node)
|
|
55
|
+
|
|
56
|
+
Add Matter Pairing
|
|
57
|
+
|
|
58
|
+
Select your bridge
|
|
59
|
+
|
|
60
|
+
Deploy
|
|
61
|
+
|
|
62
|
+
Inject get to output pairing info (QR code string + manual code)
|
|
63
|
+
|
|
64
|
+
3) Add Devices
|
|
65
|
+
|
|
66
|
+
Add Matter Device
|
|
67
|
+
|
|
68
|
+
Select the same bridge
|
|
69
|
+
|
|
70
|
+
Set your JSON device config (examples below)
|
|
71
|
+
|
|
72
|
+
Deploy
|
|
73
|
+
|
|
74
|
+
4) Enable a Device (AirNexus Option 2)
|
|
75
|
+
|
|
76
|
+
Devices will not show up in Alexa until you enable them:
|
|
77
|
+
|
|
78
|
+
Inject to the device:
|
|
79
|
+
|
|
80
|
+
topic: enable
|
|
81
|
+
|
|
82
|
+
payload can include a name
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
"topic": "enable",
|
|
88
|
+
"payload": { "name": "Lounge Thermostat" }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Matter Pairing Node
|
|
92
|
+
Inputs (commands)
|
|
93
|
+
|
|
94
|
+
Send an Inject into the Matter Pairing node:
|
|
95
|
+
|
|
96
|
+
Command How to send What it does
|
|
97
|
+
get msg.topic="get" Outputs current pairing info (if commissioned → state=commissioned)
|
|
98
|
+
pair msg.topic="pair" Ensures bridge is online and ready to commission (best effort)
|
|
99
|
+
reset msg.topic="reset" Factory reset commissioning (wipes bridge commissioning storage, regenerates codes)
|
|
100
|
+
unpair / delete msg.topic="unpair" Same as reset
|
|
101
|
+
disable msg.topic="disable" Best-effort stop advertising (takes server offline)
|
|
102
|
+
|
|
103
|
+
Optional payload:
|
|
104
|
+
|
|
105
|
+
{ "timeoutMins": 15, "includeSvg": true, "forceReset": true }
|
|
106
|
+
|
|
107
|
+
Output payload (example)
|
|
108
|
+
{
|
|
109
|
+
"bridgeId": "5318704ab571aeeb",
|
|
110
|
+
"pairingEnabled": true,
|
|
111
|
+
"pairingUntil": "2026-01-25T02:30:00.000Z",
|
|
112
|
+
"state": "ready",
|
|
113
|
+
"commissioned": false,
|
|
114
|
+
"qrPairingCode": "MT:....",
|
|
115
|
+
"manualPairingCode": "123-45-678",
|
|
116
|
+
"qrSvg": "<svg>...</svg>"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Matter Device Node (AirNexus Option 2: hidden until enabled)
|
|
120
|
+
Enable / Disable / Rename (by payload)
|
|
121
|
+
|
|
122
|
+
Enable
|
|
123
|
+
|
|
124
|
+
msg.topic = "enable";
|
|
125
|
+
msg.payload = { name: "Zone 1 Thermostat" }; // optional
|
|
126
|
+
return msg;
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Disable
|
|
130
|
+
|
|
131
|
+
msg.topic = "disable";
|
|
132
|
+
return msg;
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
Rename
|
|
136
|
+
|
|
137
|
+
msg.topic = "name";
|
|
138
|
+
msg.payload = "New Name Here";
|
|
139
|
+
return msg;
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
Config combined
|
|
143
|
+
|
|
144
|
+
msg.topic = "config";
|
|
145
|
+
msg.payload = { enabled: true, name: "Zone 2 Thermostat" };
|
|
146
|
+
return msg;
|
|
147
|
+
|
|
148
|
+
State query
|
|
149
|
+
msg.topic = "state";
|
|
150
|
+
return msg;
|
|
151
|
+
|
|
152
|
+
Inputs / Outputs (Matter Device)
|
|
153
|
+
|
|
154
|
+
The Matter Device node has 3 outputs:
|
|
155
|
+
|
|
156
|
+
Output 1 – Events / State
|
|
157
|
+
|
|
158
|
+
$Changed attribute events (when Matter.js emits them)
|
|
159
|
+
|
|
160
|
+
Thermostat writes often appear as interactionEnd snapshot diffs (even when no explicit commands are used)
|
|
161
|
+
|
|
162
|
+
Output 2 – Commands
|
|
163
|
+
|
|
164
|
+
Real Matter cluster commands (e.g. OnOff.on, LevelControl.moveToLevel)
|
|
165
|
+
|
|
166
|
+
Some controllers do attribute writes instead of commands (especially thermostats)
|
|
167
|
+
|
|
168
|
+
Output 3 – Debug / Diagnostics
|
|
169
|
+
|
|
170
|
+
subscription logs, init status, bridgeReset, write retries, sanitization info, etc.
|
|
171
|
+
|
|
172
|
+
Example Output 2 (command)
|
|
173
|
+
{
|
|
174
|
+
"command": "on",
|
|
175
|
+
"cluster": "OnOff",
|
|
176
|
+
"data": {}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Example Output 1 (thermostat diff from Alexa)
|
|
180
|
+
{
|
|
181
|
+
"thermostat": {
|
|
182
|
+
"systemMode": 3,
|
|
183
|
+
"occupiedCoolingSetpoint": 2000,
|
|
184
|
+
"occupiedHeatingSetpoint": 2000
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Command vs Attribute Writes (Thermostats)
|
|
189
|
+
|
|
190
|
+
Many Matter thermostats are controlled via attribute writes rather than explicit commands.
|
|
191
|
+
|
|
192
|
+
Typical behavior:
|
|
193
|
+
|
|
194
|
+
Changing setpoint in Alexa/HomeKit → Output 1 (state diff)
|
|
195
|
+
|
|
196
|
+
Using something like setpointRaiseLower (if controller uses it) → Output 2 (command)
|
|
197
|
+
|
|
198
|
+
Always monitor Output 1 for thermostat changes.
|
|
199
|
+
|
|
200
|
+
Configuration Examples
|
|
201
|
+
Simple On/Off Light
|
|
202
|
+
{
|
|
203
|
+
"deviceType": "OnOffLightDevice"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Thermostat (Heating + Cooling + Auto)
|
|
207
|
+
{
|
|
208
|
+
"deviceType": "ThermostatDevice",
|
|
209
|
+
"behaviorFeatures": {
|
|
210
|
+
"Thermostat": ["Heating", "Cooling", "AutoMode"]
|
|
211
|
+
},
|
|
212
|
+
"initialState": {
|
|
213
|
+
"thermostat": {
|
|
214
|
+
"controlSequenceOfOperation": 4,
|
|
215
|
+
"systemMode": 1,
|
|
216
|
+
"localTemperature": 2500,
|
|
217
|
+
"minSetpointDeadBand": 100,
|
|
218
|
+
"occupiedHeatingSetpoint": 2000,
|
|
219
|
+
"occupiedCoolingSetpoint": 2600,
|
|
220
|
+
"minHeatSetpointLimit": 500,
|
|
221
|
+
"maxHeatSetpointLimit": 3500,
|
|
222
|
+
"minCoolSetpointLimit": 500,
|
|
223
|
+
"maxCoolSetpointLimit": 3500
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Troubleshooting
|
|
229
|
+
Device doesn’t appear in Alexa/HomeKit
|
|
230
|
+
|
|
231
|
+
If you’re using AirNexus Option 2:
|
|
232
|
+
|
|
233
|
+
You must enable the Matter Device node:
|
|
234
|
+
|
|
235
|
+
msg.topic="enable"
|
|
236
|
+
|
|
237
|
+
optionally set name
|
|
238
|
+
|
|
239
|
+
Pairing issues / want to re-pair
|
|
240
|
+
|
|
241
|
+
Use Matter Pairing node:
|
|
242
|
+
|
|
243
|
+
reset to wipe commissioning + generate new QR/manual
|
|
244
|
+
|
|
245
|
+
Commands not appearing in Output 2
|
|
246
|
+
|
|
247
|
+
Expected for devices controlled by attribute writes (thermostats). Use Output 1 diffs.
|
|
248
|
+
|
|
249
|
+
License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
252
|
+
|
|
253
|
+
Acknowledgments
|
|
254
|
+
|
|
255
|
+
Inspired by and based on patterns from node-red-matter-bridge and Matter.js.
|
|
256
|
+
|
|
257
|
+
## Support
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
- Node-RED Forum: [Get help from the community](https://discourse.nodered.org)
|
|
261
|
+
|
|
262
|
+
## Contributing
|
|
263
|
+
|
|
264
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
|
269
|
+
|
|
270
|
+
## Acknowledgments
|
|
271
|
+
|
|
272
|
+
This project was heavily inspired by and based on the excellent work done in [node-red-matter-bridge](https://github.com/sammachin/node-red-matter-bridge) by Sam Machin. The architecture and implementation patterns from that project served as a fundamental guide for developing this dynamic Matter bridge implementation.
|
|
273
|
+
|
|
274
|
+
Built on top of the excellent [Matter.js](https://github.com/project-chip/matter.js) library.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 115 112">
|
|
2
|
+
<path d="M85.715 52.905c-7.996 2.19-15.164 7.406-19.636 15.152s-5.407 16.568-3.306 24.587l7.835-4.526a23.9 23.9 0 0 1 1.105-11.836l18.309 10.569 4.303-2.487v-4.967L76.016 68.829a23.92 23.92 0 0 1 9.699-6.879zm-57.108 0v9.045a23.91 23.91 0 0 1 9.699 6.879L20 79.398v4.967l4.303 2.487 18.306-10.569c1.39 3.868 1.726 7.938 1.108 11.836l7.832 4.526c2.101-8.02 1.167-16.841-3.306-24.587A32.52 32.52 0 0 0 28.607 52.905zM57.161 20l-4.303 2.484v21.138c-4.046-.731-7.736-2.476-10.804-4.961l-7.838 4.522c5.895 5.83 14 9.429 22.946 9.429s17.051-3.599 22.946-9.429l-7.835-4.522a23.92 23.92 0 0 1-10.807 4.961V22.484z" fill="#000000"/>
|
|
3
|
+
</svg>
|