@homebridge-plugins/homebridge-matter 0.0.4 → 0.0.6
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/.claude/settings.local.json +4 -1
- package/CHANGELOG.md +15 -1
- package/config.schema.json +225 -1
- package/dist/platform.d.ts +20 -0
- package/dist/platform.js +1030 -435
- package/dist/platform.js.map +1 -1
- package/package.json +3 -3
package/dist/platform.js
CHANGED
|
@@ -53,13 +53,16 @@ export class ExampleHomebridgePlatform {
|
|
|
53
53
|
this.log.info('Matter cached accessories:', this.matterAccessories.size);
|
|
54
54
|
this.log.info('='.repeat(80));
|
|
55
55
|
this.log.info('Registering Matter accessories...');
|
|
56
|
+
// Remove disabled accessories that are cached
|
|
57
|
+
this.removeDisabledAccessories();
|
|
56
58
|
// Register each device type
|
|
57
59
|
this.registerLightingDevices();
|
|
58
60
|
this.registerSwitchesAndOutlets();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
this.registerSensors();
|
|
62
|
+
this.registerHVAC();
|
|
63
|
+
this.registerSecurity();
|
|
64
|
+
this.registerWindowCoverings();
|
|
65
|
+
this.registerAppliances();
|
|
63
66
|
this.log.info('Finished registering Matter accessories');
|
|
64
67
|
// You can read current state using the API
|
|
65
68
|
const onOffState = this.api.matter.getAccessoryState('matter-dimmable-light', this.api.matter.clusterNames.OnOff);
|
|
@@ -82,6 +85,53 @@ export class ExampleHomebridgePlatform {
|
|
|
82
85
|
// onOff: true,
|
|
83
86
|
// })
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Remove disabled accessories from cache
|
|
90
|
+
*/
|
|
91
|
+
removeDisabledAccessories() {
|
|
92
|
+
const accessoriesToRemove = [];
|
|
93
|
+
// Define mapping of config flags to UUIDs
|
|
94
|
+
const configMap = [
|
|
95
|
+
{ enabled: this.config.enableOnOffLight, uuid: this.api.matter.uuid.generate('matter-onoff-light'), name: 'On/Off Light' },
|
|
96
|
+
{ enabled: this.config.enableDimmableLight, uuid: this.api.matter.uuid.generate('matter-dimmable-light'), name: 'Dimmable Light' },
|
|
97
|
+
{ enabled: this.config.enableColourTemperatureLight, uuid: this.api.matter.uuid.generate('matter-colour-temp-light'), name: 'Colour Temperature Light' },
|
|
98
|
+
{ enabled: this.config.enableColourLight, uuid: this.api.matter.uuid.generate('matter-colour-light'), name: 'Colour Light' },
|
|
99
|
+
{ enabled: this.config.enableExtendedColourLight, uuid: this.api.matter.uuid.generate('matter-extended-colour-light'), name: 'Extended Colour Light' },
|
|
100
|
+
{ enabled: this.config.enableOnOffSwitch, uuid: this.api.matter.uuid.generate('matter-onoff-switch'), name: 'On/Off Switch' },
|
|
101
|
+
{ enabled: this.config.enableOnOffOutlet, uuid: this.api.matter.uuid.generate('matter-onoff-outlet'), name: 'On/Off Outlet' },
|
|
102
|
+
{ enabled: this.config.enableDimmableOutlet, uuid: this.api.matter.uuid.generate('matter-dimmable-outlet'), name: 'Dimmable Outlet' },
|
|
103
|
+
{ enabled: this.config.enableTemperatureSensor, uuid: this.api.hap.uuid.generate('matter-temperature-sensor'), name: 'Temperature Sensor' },
|
|
104
|
+
{ enabled: this.config.enableHumiditySensor, uuid: this.api.hap.uuid.generate('matter-humidity-sensor'), name: 'Humidity Sensor' },
|
|
105
|
+
{ enabled: this.config.enableLightSensor, uuid: this.api.hap.uuid.generate('matter-light-sensor'), name: 'Light Sensor' },
|
|
106
|
+
{ enabled: this.config.enableMotionSensor, uuid: this.api.hap.uuid.generate('matter-motion-sensor'), name: 'Motion Sensor' },
|
|
107
|
+
{ enabled: this.config.enableContactSensor, uuid: this.api.hap.uuid.generate('matter-contact-sensor'), name: 'Contact Sensor' },
|
|
108
|
+
{ enabled: this.config.enableLeakSensor, uuid: this.api.hap.uuid.generate('matter-leak-sensor'), name: 'Leak Sensor' },
|
|
109
|
+
{ enabled: this.config.enableSmokeSensor, uuid: this.api.hap.uuid.generate('matter-smoke-sensor'), name: 'Smoke Sensor' },
|
|
110
|
+
{ enabled: this.config.enableThermostat, uuid: this.api.matter.uuid.generate('matter-thermostat'), name: 'Thermostat' },
|
|
111
|
+
{ enabled: this.config.enableFan, uuid: this.api.matter.uuid.generate('matter-fan'), name: 'Fan' },
|
|
112
|
+
{ enabled: this.config.enableDoorLock, uuid: this.api.matter.uuid.generate('matter-door-lock'), name: 'Door Lock' },
|
|
113
|
+
{ enabled: this.config.enableGarageDoor, uuid: this.api.matter.uuid.generate('matter-garage-door'), name: 'Garage Door' },
|
|
114
|
+
{ enabled: this.config.enableWindowBlind, uuid: this.api.matter.uuid.generate('matter-window-blind'), name: 'Window Blind' },
|
|
115
|
+
{ enabled: this.config.enableVenetianBlind, uuid: this.api.matter.uuid.generate('matter-venetian-blind'), name: 'Venetian Blind' },
|
|
116
|
+
{ enabled: this.config.enableRobotVacuum, uuid: this.api.matter.uuid.generate('matter-robot-vacuum'), name: 'Robot Vacuum' },
|
|
117
|
+
];
|
|
118
|
+
// Check each config entry
|
|
119
|
+
for (const item of configMap) {
|
|
120
|
+
// If disabled and exists in cache, mark for removal
|
|
121
|
+
if (!item.enabled && this.matterAccessories.has(item.uuid)) {
|
|
122
|
+
accessoriesToRemove.push(item);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Remove disabled accessories
|
|
126
|
+
if (accessoriesToRemove.length > 0) {
|
|
127
|
+
this.log.info(`Removing ${accessoriesToRemove.length} disabled accessories...`);
|
|
128
|
+
for (const item of accessoriesToRemove) {
|
|
129
|
+
this.log.info(` - Removing: ${item.name}`);
|
|
130
|
+
this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [this.matterAccessories.get(item.uuid)]);
|
|
131
|
+
this.matterAccessories.delete(item.uuid);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
85
135
|
/**
|
|
86
136
|
* Lighting Devices
|
|
87
137
|
*/
|
|
@@ -93,242 +143,254 @@ export class ExampleHomebridgePlatform {
|
|
|
93
143
|
const uuidLightExtendedColour = this.api.matter.uuid.generate('matter-extended-colour-light');
|
|
94
144
|
// 1. On/Off Light
|
|
95
145
|
const accessories = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
onOff:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
if (this.config.enableOnOffLight) {
|
|
147
|
+
accessories.push({
|
|
148
|
+
uuid: uuidLightOnOff,
|
|
149
|
+
displayName: 'On/Off Light',
|
|
150
|
+
deviceType: this.api.matter.deviceTypes.OnOffLight,
|
|
151
|
+
serialNumber: 'LIGHT-001',
|
|
152
|
+
manufacturer: 'Matter Examples',
|
|
153
|
+
model: 'OnOffLight v1',
|
|
154
|
+
clusters: {
|
|
155
|
+
onOff: {
|
|
156
|
+
onOff: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
// These are called when the user controls the accessory via the Home app
|
|
160
|
+
handlers: {
|
|
161
|
+
onOff: {
|
|
162
|
+
on: async ( /* no args */) => {
|
|
163
|
+
this.log.info('[On/Off Light] ✓ Handler `on` called (user controlled via Home app)');
|
|
164
|
+
},
|
|
165
|
+
off: async ( /* no args */) => {
|
|
166
|
+
this.log.info('[On/Off Light] ✓ Handler `off` called (user controlled via Home app)');
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
120
172
|
// 2. Dimmable Light
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
onOff:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
if (this.config.enableDimmableLight) {
|
|
174
|
+
accessories.push({
|
|
175
|
+
uuid: uuidLightDimmable,
|
|
176
|
+
displayName: 'Dimmable Light',
|
|
177
|
+
deviceType: this.api.matter.deviceTypes.DimmableLight,
|
|
178
|
+
serialNumber: 'LIGHT-002',
|
|
179
|
+
manufacturer: 'Matter Examples',
|
|
180
|
+
model: 'DimmableLight v1',
|
|
181
|
+
clusters: {
|
|
182
|
+
onOff: {
|
|
183
|
+
onOff: false,
|
|
184
|
+
},
|
|
185
|
+
levelControl: {
|
|
186
|
+
currentLevel: 127,
|
|
187
|
+
minLevel: 1,
|
|
188
|
+
maxLevel: 254,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
// These are called when the user controls the accessory via the Home app
|
|
192
|
+
handlers: {
|
|
193
|
+
onOff: {
|
|
194
|
+
on: async () => {
|
|
195
|
+
this.log.info('[Dimmable Light] ✓ Handler `on` called (user controlled via Home app)');
|
|
196
|
+
},
|
|
197
|
+
off: async () => {
|
|
198
|
+
this.log.info('[Dimmable Light] ✓ Handler `off` called (user controlled via Home app)');
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
levelControl: {
|
|
202
|
+
moveToLevelWithOnOff: async (request) => {
|
|
203
|
+
const { level } = request;
|
|
204
|
+
this.log.info(`[Dimmable Light] ✓ Handler \`moveToLevel\` called with ${level} (${Math.round(level / 254 * 100)}%)`);
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
156
210
|
// 3. Colour Temperature Light
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
onOff:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
if (this.config.enableColourTemperatureLight) {
|
|
212
|
+
accessories.push({
|
|
213
|
+
uuid: uuidLightColourTemp,
|
|
214
|
+
displayName: 'Colour Temperature Light',
|
|
215
|
+
deviceType: this.api.matter.deviceTypes.ColorTemperatureLight,
|
|
216
|
+
serialNumber: 'LIGHT-003',
|
|
217
|
+
manufacturer: 'Matter Examples',
|
|
218
|
+
model: 'ColourTempLight v1',
|
|
219
|
+
clusters: {
|
|
220
|
+
onOff: {
|
|
221
|
+
onOff: false,
|
|
222
|
+
},
|
|
223
|
+
levelControl: {
|
|
224
|
+
currentLevel: 127,
|
|
225
|
+
minLevel: 1,
|
|
226
|
+
maxLevel: 254,
|
|
227
|
+
},
|
|
228
|
+
colorControl: {
|
|
229
|
+
colorMode: 2, // Colour temperature mode
|
|
230
|
+
colorTemperatureMireds: 250, // ~4000K
|
|
231
|
+
colorTempPhysicalMinMireds: 147, // 6800K (coolest)
|
|
232
|
+
colorTempPhysicalMaxMireds: 454, // 2200K (warmest)
|
|
233
|
+
coupleColorTempToLevelMinMireds: 147,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
// These are called when the user controls the accessory via the Home app
|
|
237
|
+
handlers: {
|
|
238
|
+
onOff: {
|
|
239
|
+
on: async ( /* no args */) => {
|
|
240
|
+
this.log.info('[Colour Temp Light] handler `on` called (user controlled via Home app)');
|
|
241
|
+
},
|
|
242
|
+
off: async ( /* no args */) => {
|
|
243
|
+
this.log.info('[Colour Temp Light] Turned `off` called (user controlled via Home app)');
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
levelControl: {
|
|
247
|
+
moveToLevelWithOnOff: async (request) => {
|
|
248
|
+
const { level } = request;
|
|
249
|
+
this.log.info(`[Colour Light] ✓ Handler \`moveToLevel\` called with ${level} (${Math.round(level / 254 * 100)}%)`);
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
colorControl: {
|
|
253
|
+
moveToColorTemperatureLogic: async (request) => {
|
|
254
|
+
const { targetMireds, transitionTime } = request;
|
|
255
|
+
const kelvin = Math.round(1000000 / targetMireds);
|
|
256
|
+
this.log.info(`[Colour Temp Light] ✓ Handler \`moveToColorTemperatureLogic\` called with ${targetMireds} mireds (~${kelvin}K), transition: ${transitionTime}s`);
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
206
262
|
// 4. Colour Light (Hue/Saturation ONLY - no CCT)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
onOff:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
if (this.config.enableColourLight) {
|
|
264
|
+
accessories.push({
|
|
265
|
+
uuid: uuidLightColour,
|
|
266
|
+
displayName: 'Colour Light (HS)',
|
|
267
|
+
deviceType: this.api.matter.deviceTypes.ExtendedColorLight,
|
|
268
|
+
serialNumber: 'LIGHT-004',
|
|
269
|
+
manufacturer: 'Matter Examples',
|
|
270
|
+
model: 'ColorLight v1',
|
|
271
|
+
clusters: {
|
|
272
|
+
onOff: {
|
|
273
|
+
onOff: false,
|
|
274
|
+
},
|
|
275
|
+
levelControl: {
|
|
276
|
+
currentLevel: 127,
|
|
277
|
+
minLevel: 1,
|
|
278
|
+
maxLevel: 254,
|
|
279
|
+
},
|
|
280
|
+
colorControl: {
|
|
281
|
+
colorMode: 0, // Hue/Saturation mode
|
|
282
|
+
currentHue: 0, // Red (0 degrees)
|
|
283
|
+
currentSaturation: 254, // Full saturation
|
|
284
|
+
currentX: 41942, // Also provide XY for compatibility
|
|
285
|
+
currentY: 21626,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
// These are called when the user controls the accessory via the Home app
|
|
289
|
+
handlers: {
|
|
290
|
+
onOff: {
|
|
291
|
+
on: async () => {
|
|
292
|
+
this.log.info('[Colour Light HS] ✓ Handler `on` called (user controlled via Home app)');
|
|
293
|
+
},
|
|
294
|
+
off: async () => {
|
|
295
|
+
this.log.info('[Colour Light HS] ✓ Handler `off` called (user controlled via Home app)');
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
levelControl: {
|
|
299
|
+
moveToLevelWithOnOff: async (request) => {
|
|
300
|
+
const { level } = request;
|
|
301
|
+
this.log.info(`[Colour Light HS] ✓ Handler \`moveToLevel\` called with ${level} (${Math.round(level / 254 * 100)}%)`);
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
colorControl: {
|
|
305
|
+
moveToColorLogic: async (request) => {
|
|
306
|
+
const { targetX, targetY, transitionTime } = request;
|
|
307
|
+
const xFloat = (targetX / 65535).toFixed(4);
|
|
308
|
+
const yFloat = (targetY / 65535).toFixed(4);
|
|
309
|
+
this.log.info(`[Colour Light HS] ✓ Handler \`moveToColorLogic\` called with x=${targetX} (~${xFloat}), y=${targetY} (~${yFloat}), transition: ${transitionTime}s`);
|
|
310
|
+
},
|
|
311
|
+
moveToHueAndSaturationLogic: async (request) => {
|
|
312
|
+
const { targetHue, targetSaturation, transitionTime } = request;
|
|
313
|
+
const hueDegrees = Math.round((targetHue / 254) * 360);
|
|
314
|
+
const saturationPercent = Math.round((targetSaturation / 254) * 100);
|
|
315
|
+
this.log.info(`[Colour Light HS] ✓ Handler \`moveToHueAndSaturationLogic\` called with hue=${targetHue} (~${hueDegrees}°), saturation=${targetSaturation} (~${saturationPercent}%), transition: ${transitionTime}s`);
|
|
316
|
+
},
|
|
317
|
+
// NOTE: No moveToColorTemperatureLogic handler - this light only supports color, not CCT
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
264
322
|
// 5. Extended Colour Light (Hue/Saturation + CCT)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
onOff:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
323
|
+
if (this.config.enableExtendedColourLight) {
|
|
324
|
+
accessories.push({
|
|
325
|
+
uuid: uuidLightExtendedColour,
|
|
326
|
+
displayName: 'Extended Colour Light (HS+CCT)',
|
|
327
|
+
deviceType: this.api.matter.deviceTypes.ExtendedColorLight,
|
|
328
|
+
serialNumber: 'LIGHT-005',
|
|
329
|
+
manufacturer: 'Matter Examples',
|
|
330
|
+
model: 'ExtendedColorLight v1',
|
|
331
|
+
clusters: {
|
|
332
|
+
onOff: {
|
|
333
|
+
onOff: false,
|
|
334
|
+
},
|
|
335
|
+
levelControl: {
|
|
336
|
+
currentLevel: 127,
|
|
337
|
+
minLevel: 1,
|
|
338
|
+
maxLevel: 254,
|
|
339
|
+
},
|
|
340
|
+
colorControl: {
|
|
341
|
+
colorMode: 0, // Hue/Saturation mode
|
|
342
|
+
currentHue: 0, // Red (0 degrees)
|
|
343
|
+
currentSaturation: 254, // Full saturation
|
|
344
|
+
currentX: 41942, // Also provide XY for compatibility
|
|
345
|
+
currentY: 21626,
|
|
346
|
+
colorTemperatureMireds: 250, // ~4000K (for CCT mode)
|
|
347
|
+
colorTempPhysicalMinMireds: 147, // 6800K (coolest)
|
|
348
|
+
colorTempPhysicalMaxMireds: 454, // 2200K (warmest)
|
|
349
|
+
coupleColorTempToLevelMinMireds: 147,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
// These are called when the user controls the accessory via the Home app
|
|
353
|
+
handlers: {
|
|
354
|
+
onOff: {
|
|
355
|
+
on: async () => {
|
|
356
|
+
this.log.info('[Extended Colour Light] ✓ Handler `on` called (user controlled via Home app)');
|
|
357
|
+
},
|
|
358
|
+
off: async () => {
|
|
359
|
+
this.log.info('[Extended Colour Light] ✓ Handler `off` called (user controlled via Home app)');
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
levelControl: {
|
|
363
|
+
moveToLevelWithOnOff: async (request) => {
|
|
364
|
+
const { level } = request;
|
|
365
|
+
this.log.info(`[Extended Colour Light] ✓ Handler \`moveToLevel\` called with ${level} (${Math.round(level / 254 * 100)}%)`);
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
colorControl: {
|
|
369
|
+
moveToColorLogic: async (request) => {
|
|
370
|
+
const { targetX, targetY, transitionTime } = request;
|
|
371
|
+
const xFloat = (targetX / 65535).toFixed(4);
|
|
372
|
+
const yFloat = (targetY / 65535).toFixed(4);
|
|
373
|
+
this.log.info(`[Extended Colour Light] ✓ Handler \`moveToColorLogic\` called with x=${targetX} (~${xFloat}), y=${targetY} (~${yFloat}), transition: ${transitionTime}s`);
|
|
374
|
+
},
|
|
375
|
+
moveToHueAndSaturationLogic: async (request) => {
|
|
376
|
+
const { targetHue, targetSaturation, transitionTime } = request;
|
|
377
|
+
const hueDegrees = Math.round((targetHue / 254) * 360);
|
|
378
|
+
const saturationPercent = Math.round((targetSaturation / 254) * 100);
|
|
379
|
+
this.log.info(`[Extended Colour Light] ✓ Handler \`moveToHueAndSaturationLogic\` called with hue=${targetHue} (~${hueDegrees}°), saturation=${targetSaturation} (~${saturationPercent}%), transition: ${transitionTime}s`);
|
|
380
|
+
},
|
|
381
|
+
moveToColorTemperatureLogic: async (request) => {
|
|
382
|
+
const { targetMireds, transitionTime } = request;
|
|
383
|
+
const kelvin = Math.round(1000000 / targetMireds);
|
|
384
|
+
this.log.info(`[Extended Colour Light] ✓ Handler \`moveToColorTemperatureLogic\` called with ${targetMireds} mireds (~${kelvin}K), transition: ${transitionTime}s`);
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
330
390
|
// Register all lighting accessories
|
|
331
|
-
|
|
391
|
+
if (accessories.length > 0) {
|
|
392
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
393
|
+
}
|
|
332
394
|
}
|
|
333
395
|
/**
|
|
334
396
|
* Switches and Outlets
|
|
@@ -339,92 +401,98 @@ export class ExampleHomebridgePlatform {
|
|
|
339
401
|
const uuidDimmableOutlet = this.api.matter.uuid.generate('matter-dimmable-outlet');
|
|
340
402
|
const accessories = [];
|
|
341
403
|
// 1. On/Off Switch
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
onOff:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
if (this.config.enableOnOffSwitch) {
|
|
405
|
+
accessories.push({
|
|
406
|
+
uuid: uuidSwitch,
|
|
407
|
+
displayName: 'On/Off Switch',
|
|
408
|
+
deviceType: this.api.matter.deviceTypes.OnOffSwitch,
|
|
409
|
+
serialNumber: 'SWITCH-001',
|
|
410
|
+
manufacturer: 'Matter Examples',
|
|
411
|
+
model: 'OnOffSwitch v1',
|
|
412
|
+
clusters: {
|
|
413
|
+
onOff: {
|
|
414
|
+
onOff: false,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
handlers: {
|
|
418
|
+
onOff: {
|
|
419
|
+
on: async () => {
|
|
420
|
+
this.log.info('[On/Off Switch] ✓ Handler `on` called (user controlled via Home app)');
|
|
421
|
+
},
|
|
422
|
+
off: async () => {
|
|
423
|
+
this.log.info('[On/Off Switch] ✓ Handler `off` called (user controlled via Home app)');
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
365
429
|
// 2. On/Off Outlet (Smart Plug)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
onOff:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
430
|
+
if (this.config.enableOnOffOutlet) {
|
|
431
|
+
accessories.push({
|
|
432
|
+
uuid: uuidOutlet,
|
|
433
|
+
displayName: 'On/Off Outlet',
|
|
434
|
+
deviceType: this.api.matter.deviceTypes.OnOffOutlet,
|
|
435
|
+
serialNumber: 'OUTLET-001',
|
|
436
|
+
manufacturer: 'Matter Examples',
|
|
437
|
+
model: 'OnOffOutlet v1',
|
|
438
|
+
clusters: {
|
|
439
|
+
onOff: {
|
|
440
|
+
onOff: false,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
handlers: {
|
|
444
|
+
onOff: {
|
|
445
|
+
on: async () => {
|
|
446
|
+
this.log.info('[On/Off Outlet] ✓ Handler `on` called (user controlled via Home app)');
|
|
447
|
+
},
|
|
448
|
+
off: async () => {
|
|
449
|
+
this.log.info('[On/Off Outlet] ✓ Handler `off` called (user controlled via Home app)');
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
}
|
|
389
455
|
// 3. Dimmable Outlet
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
onOff:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
456
|
+
if (this.config.enableDimmableOutlet) {
|
|
457
|
+
accessories.push({
|
|
458
|
+
uuid: uuidDimmableOutlet,
|
|
459
|
+
displayName: 'Dimmable Outlet',
|
|
460
|
+
deviceType: this.api.matter.deviceTypes.DimmableOutlet,
|
|
461
|
+
serialNumber: 'OUTLET-002',
|
|
462
|
+
manufacturer: 'Matter Examples',
|
|
463
|
+
model: 'DimmableOutlet v1',
|
|
464
|
+
clusters: {
|
|
465
|
+
onOff: {
|
|
466
|
+
onOff: false,
|
|
467
|
+
},
|
|
468
|
+
levelControl: {
|
|
469
|
+
currentLevel: 127,
|
|
470
|
+
minLevel: 1,
|
|
471
|
+
maxLevel: 254,
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
handlers: {
|
|
475
|
+
onOff: {
|
|
476
|
+
on: async () => {
|
|
477
|
+
this.log.info('[Dimmable Outlet] ✓ Handler `on` called (user controlled via Home app)');
|
|
478
|
+
},
|
|
479
|
+
off: async () => {
|
|
480
|
+
this.log.info('[Dimmable Outlet] ✓ Handler `off` called (user controlled via Home app)');
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
levelControl: {
|
|
484
|
+
moveToLevelWithOnOff: async (request) => {
|
|
485
|
+
const { level } = request;
|
|
486
|
+
this.log.info(`[Dimmable Outlet] ✓ Handler \`moveToLevel\` called with ${level} (${Math.round(level / 254 * 100)}%)`);
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
424
492
|
// Register all switch/outlet accessories
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
493
|
+
if (accessories.length > 0) {
|
|
494
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
495
|
+
}
|
|
428
496
|
}
|
|
429
497
|
/**
|
|
430
498
|
* Register Matter sensor accessories
|
|
@@ -435,130 +503,657 @@ export class ExampleHomebridgePlatform {
|
|
|
435
503
|
this.log.info('═'.repeat(80));
|
|
436
504
|
const accessories = [];
|
|
437
505
|
// 1. Temperature Sensor
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
506
|
+
if (this.config.enableTemperatureSensor) {
|
|
507
|
+
accessories.push({
|
|
508
|
+
uuid: this.api.hap.uuid.generate('matter-temperature-sensor'),
|
|
509
|
+
displayName: 'Temperature Sensor',
|
|
510
|
+
deviceType: this.api.matter.deviceTypes.TemperatureSensor,
|
|
511
|
+
serialNumber: 'TEMP-001',
|
|
512
|
+
manufacturer: 'Homebridge',
|
|
513
|
+
model: 'Temperature Sensor Example',
|
|
514
|
+
clusters: {
|
|
515
|
+
temperatureMeasurement: {
|
|
516
|
+
measuredValue: 2100, // 21.00°C (in hundredths of a degree Celsius)
|
|
517
|
+
minMeasuredValue: -5000, // -50°C
|
|
518
|
+
maxMeasuredValue: 10000, // 100°C
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
453
523
|
// 2. Humidity Sensor
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
524
|
+
if (this.config.enableHumiditySensor) {
|
|
525
|
+
accessories.push({
|
|
526
|
+
uuid: this.api.hap.uuid.generate('matter-humidity-sensor'),
|
|
527
|
+
displayName: 'Humidity Sensor',
|
|
528
|
+
deviceType: this.api.matter.deviceTypes.HumiditySensor,
|
|
529
|
+
serialNumber: 'HUM-001',
|
|
530
|
+
manufacturer: 'Homebridge',
|
|
531
|
+
model: 'Humidity Sensor Example',
|
|
532
|
+
clusters: {
|
|
533
|
+
relativeHumidityMeasurement: {
|
|
534
|
+
measuredValue: 5500, // 55% (in hundredths of a percent)
|
|
535
|
+
minMeasuredValue: 0,
|
|
536
|
+
maxMeasuredValue: 10000, // 100%
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
}
|
|
469
541
|
// 3. Light Sensor
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
542
|
+
if (this.config.enableLightSensor) {
|
|
543
|
+
accessories.push({
|
|
544
|
+
uuid: this.api.hap.uuid.generate('matter-light-sensor'),
|
|
545
|
+
displayName: 'Light Sensor',
|
|
546
|
+
deviceType: this.api.matter.deviceTypes.LightSensor,
|
|
547
|
+
serialNumber: 'LIGHT-001',
|
|
548
|
+
manufacturer: 'Homebridge',
|
|
549
|
+
model: 'Light Sensor Example',
|
|
550
|
+
clusters: {
|
|
551
|
+
illuminanceMeasurement: {
|
|
552
|
+
measuredValue: 5000, // 500 lux (in 10,000 * log10(lux) format)
|
|
553
|
+
minMeasuredValue: 1,
|
|
554
|
+
maxMeasuredValue: 65534,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}
|
|
485
559
|
// 4. Motion Sensor (Occupancy)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
560
|
+
if (this.config.enableMotionSensor) {
|
|
561
|
+
// Note: OccupancySensorDevice requires specifying features (PIR, Ultrasonic, or PhysicalContact)
|
|
562
|
+
const OccupancySensingServer = this.api.matter.deviceTypes.MotionSensor.requirements.OccupancySensingServer;
|
|
563
|
+
const MotionSensorWithPIR = this.api.matter.deviceTypes.MotionSensor.with(OccupancySensingServer.with('PassiveInfrared'));
|
|
564
|
+
accessories.push({
|
|
565
|
+
uuid: this.api.hap.uuid.generate('matter-motion-sensor'),
|
|
566
|
+
displayName: 'Motion Sensor',
|
|
567
|
+
deviceType: MotionSensorWithPIR,
|
|
568
|
+
serialNumber: 'MOTION-001',
|
|
569
|
+
manufacturer: 'Homebridge',
|
|
570
|
+
model: 'Motion Sensor Example',
|
|
571
|
+
clusters: {
|
|
572
|
+
occupancySensing: {
|
|
573
|
+
occupancy: {
|
|
574
|
+
occupied: false, // No motion detected
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
}
|
|
504
580
|
// 5. Contact Sensor
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
581
|
+
if (this.config.enableContactSensor) {
|
|
582
|
+
accessories.push({
|
|
583
|
+
uuid: this.api.hap.uuid.generate('matter-contact-sensor'),
|
|
584
|
+
displayName: 'Contact Sensor',
|
|
585
|
+
deviceType: this.api.matter.deviceTypes.ContactSensor,
|
|
586
|
+
serialNumber: 'CONTACT-001',
|
|
587
|
+
manufacturer: 'Homebridge',
|
|
588
|
+
model: 'Contact Sensor Example',
|
|
589
|
+
clusters: {
|
|
590
|
+
booleanState: {
|
|
591
|
+
stateValue: false, // Contact closed (false = closed, true = open)
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
518
596
|
// 6. Leak Sensor
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
597
|
+
if (this.config.enableLeakSensor) {
|
|
598
|
+
accessories.push({
|
|
599
|
+
uuid: this.api.hap.uuid.generate('matter-leak-sensor'),
|
|
600
|
+
displayName: 'Leak Sensor',
|
|
601
|
+
deviceType: this.api.matter.deviceTypes.LeakSensor,
|
|
602
|
+
serialNumber: 'LEAK-001',
|
|
603
|
+
manufacturer: 'Homebridge',
|
|
604
|
+
model: 'Leak Sensor Example',
|
|
605
|
+
clusters: {
|
|
606
|
+
booleanState: {
|
|
607
|
+
stateValue: false, // No leak detected (false = dry, true = leak)
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
}
|
|
532
612
|
// 7. Smoke Sensor
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
613
|
+
if (this.config.enableSmokeSensor) {
|
|
614
|
+
// Note: SmokeCoAlarmDevice requires specifying features (SmokeAlarm and/or CoAlarm)
|
|
615
|
+
const SmokeCoAlarmServer = this.api.matter.deviceTypes.SmokeSensor.requirements.SmokeCoAlarmServer;
|
|
616
|
+
const SmokeSensorWithBoth = this.api.matter.deviceTypes.SmokeSensor.with(SmokeCoAlarmServer.with('SmokeAlarm', 'CoAlarm'));
|
|
617
|
+
accessories.push({
|
|
618
|
+
uuid: this.api.hap.uuid.generate('matter-smoke-sensor'),
|
|
619
|
+
displayName: 'Smoke Sensor',
|
|
620
|
+
deviceType: SmokeSensorWithBoth,
|
|
621
|
+
serialNumber: 'SMOKE-001',
|
|
622
|
+
manufacturer: 'Homebridge',
|
|
623
|
+
model: 'Smoke Sensor Example',
|
|
624
|
+
clusters: {
|
|
625
|
+
smokeCoAlarm: {
|
|
626
|
+
smokeState: 0, // 0 = Normal, 1 = Warning, 2 = Critical
|
|
627
|
+
coState: 0, // 0 = Normal, 1 = Warning, 2 = Critical
|
|
628
|
+
batteryAlert: 0, // 0 = Normal
|
|
629
|
+
testInProgress: false,
|
|
630
|
+
hardwareFaultAlert: false,
|
|
631
|
+
endOfServiceAlert: 0, // 0 = Normal
|
|
632
|
+
interconnectSmokeAlarm: 0, // 0 = Normal
|
|
633
|
+
interconnectCoAlarm: 0, // 0 = Normal
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (accessories.length > 0) {
|
|
639
|
+
this.log.info(`✓ Registered ${accessories.length} sensor accessories`);
|
|
640
|
+
for (const acc of accessories) {
|
|
641
|
+
this.log.info(` - ${acc.displayName}`);
|
|
642
|
+
}
|
|
643
|
+
// Register all sensor accessories
|
|
644
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Register Matter HVAC accessories (Thermostats, Fans)
|
|
649
|
+
*/
|
|
650
|
+
registerHVAC() {
|
|
651
|
+
this.log.info('═'.repeat(80));
|
|
652
|
+
this.log.info('Registering Matter HVAC Devices');
|
|
653
|
+
this.log.info('═'.repeat(80));
|
|
654
|
+
const accessories = [];
|
|
655
|
+
// 1. Thermostat
|
|
656
|
+
if (this.config.enableThermostat) {
|
|
657
|
+
accessories.push({
|
|
658
|
+
uuid: this.api.matter.uuid.generate('matter-thermostat'),
|
|
659
|
+
displayName: 'Thermostat',
|
|
660
|
+
deviceType: this.api.matter.deviceTypes.Thermostat,
|
|
661
|
+
serialNumber: 'THERMO-001',
|
|
662
|
+
manufacturer: 'Matter Examples',
|
|
663
|
+
model: 'Thermostat v1',
|
|
664
|
+
clusters: {
|
|
665
|
+
thermostat: {
|
|
666
|
+
// Current temperature (in hundredths of degrees Celsius)
|
|
667
|
+
localTemperature: 2100, // 21.00°C
|
|
668
|
+
// Heating setpoint (target temperature in heat mode)
|
|
669
|
+
occupiedHeatingSetpoint: 2000, // 20.00°C
|
|
670
|
+
minHeatSetpointLimit: 700, // 7°C minimum
|
|
671
|
+
maxHeatSetpointLimit: 3000, // 30°C maximum
|
|
672
|
+
// Cooling setpoint (target temperature in cool mode)
|
|
673
|
+
occupiedCoolingSetpoint: 2400, // 24.00°C
|
|
674
|
+
minCoolSetpointLimit: 1600, // 16°C minimum
|
|
675
|
+
maxCoolSetpointLimit: 3200, // 32°C maximum
|
|
676
|
+
// System mode: 0=Off, 1=Auto, 3=Cool, 4=Heat
|
|
677
|
+
systemMode: 4, // Heat mode
|
|
678
|
+
// Control sequence: what modes are available (mandatory field)
|
|
679
|
+
// 4 = CoolingAndHeating (correct value when both Heating & Cooling features are present)
|
|
680
|
+
controlSequenceOfOperation: 4,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
handlers: {
|
|
684
|
+
thermostat: {
|
|
685
|
+
// Called when user changes heating setpoint
|
|
686
|
+
setOccupiedHeatingSetpoint: async (request) => {
|
|
687
|
+
const tempC = (request.targetSetpoint / 100).toFixed(1);
|
|
688
|
+
this.log.info(`[Thermostat] ✓ Handler \`setOccupiedHeatingSetpoint\` called: ${request.targetSetpoint} (${tempC}°C)`);
|
|
689
|
+
},
|
|
690
|
+
// Called when user changes cooling setpoint
|
|
691
|
+
setOccupiedCoolingSetpoint: async (request) => {
|
|
692
|
+
const tempC = (request.targetSetpoint / 100).toFixed(1);
|
|
693
|
+
this.log.info(`[Thermostat] ✓ Handler \`setOccupiedCoolingSetpoint\` called: ${request.targetSetpoint} (${tempC}°C)`);
|
|
694
|
+
},
|
|
695
|
+
// Called when user changes mode (Off, Auto, Cool, Heat)
|
|
696
|
+
setSystemMode: async (request) => {
|
|
697
|
+
const modes = ['Off', 'Auto', 'Reserved', 'Cool', 'Heat', 'Emergency Heating', 'Precooling', 'Fan Only'];
|
|
698
|
+
const modeName = modes[request.systemMode] || `Unknown (${request.systemMode})`;
|
|
699
|
+
this.log.info(`[Thermostat] ✓ Handler \`setSystemMode\` called: ${request.systemMode} (${modeName})`);
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
// 2. Fan
|
|
706
|
+
if (this.config.enableFan) {
|
|
707
|
+
accessories.push({
|
|
708
|
+
uuid: this.api.matter.uuid.generate('matter-fan'),
|
|
709
|
+
displayName: 'Fan',
|
|
710
|
+
deviceType: this.api.matter.deviceTypes.Fan,
|
|
711
|
+
serialNumber: 'FAN-001',
|
|
712
|
+
manufacturer: 'Matter Examples',
|
|
713
|
+
model: 'Fan v1',
|
|
714
|
+
clusters: {
|
|
715
|
+
fanControl: {
|
|
716
|
+
// Fan mode: 0=Off, 1=Low, 2=Medium, 3=High, 4=On, 5=Auto, 6=Smart
|
|
717
|
+
fanMode: 0, // Off
|
|
718
|
+
// Fan mode sequence: indicates which modes are supported
|
|
719
|
+
// 0=OffLowMedHigh, 1=OffLowHigh, 2=OffLowMedHighAuto, 3=OffLowHighAuto, 4=OffOnAuto, 5=OffOn
|
|
720
|
+
fanModeSequence: 0, // OffLowMedHigh
|
|
721
|
+
// Percent setting (0-100)
|
|
722
|
+
percentSetting: 0,
|
|
723
|
+
percentCurrent: 0,
|
|
724
|
+
// Speed setting (0-100, some fans use this instead of percent)
|
|
725
|
+
speedSetting: 0,
|
|
726
|
+
speedCurrent: 0,
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
handlers: {
|
|
730
|
+
fanControl: {
|
|
731
|
+
// Called when user changes fan speed via percent slider
|
|
732
|
+
setPercentSetting: async (request) => {
|
|
733
|
+
this.log.info(`[Fan] ✓ Handler \`setPercentSetting\` called: ${request.percentSetting}%`);
|
|
734
|
+
},
|
|
735
|
+
// Called when user changes fan mode
|
|
736
|
+
setFanMode: async (request) => {
|
|
737
|
+
const modes = ['Off', 'Low', 'Medium', 'High', 'On', 'Auto', 'Smart'];
|
|
738
|
+
const modeName = modes[request.fanMode] || `Unknown (${request.fanMode})`;
|
|
739
|
+
this.log.info(`[Fan] ✓ Handler \`setFanMode\` called: ${request.fanMode} (${modeName})`);
|
|
740
|
+
},
|
|
741
|
+
// Called when user presses up/down buttons to adjust speed
|
|
742
|
+
step: async (request) => {
|
|
743
|
+
const dir = request.direction === 0 ? 'Up' : 'Down';
|
|
744
|
+
this.log.info(`[Fan] ✓ Handler \`step\` called: direction=${dir}, wrap=${request.wrap}, lowestOff=${request.lowestOff}`);
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (accessories.length > 0) {
|
|
751
|
+
this.log.info(`✓ Registered ${accessories.length} HVAC accessories`);
|
|
752
|
+
for (const acc of accessories) {
|
|
753
|
+
this.log.info(` - ${acc.displayName}`);
|
|
754
|
+
}
|
|
755
|
+
// Register all HVAC accessories
|
|
756
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Register Matter Security & Access accessories (Door Locks, Garage Doors)
|
|
761
|
+
*/
|
|
762
|
+
registerSecurity() {
|
|
763
|
+
this.log.info('═'.repeat(80));
|
|
764
|
+
this.log.info('Registering Matter Security & Access Devices');
|
|
765
|
+
this.log.info('═'.repeat(80));
|
|
766
|
+
const accessories = [];
|
|
767
|
+
// 1. Door Lock
|
|
768
|
+
if (this.config.enableDoorLock) {
|
|
769
|
+
accessories.push({
|
|
770
|
+
uuid: this.api.matter.uuid.generate('matter-door-lock'),
|
|
771
|
+
displayName: 'Door Lock',
|
|
772
|
+
deviceType: this.api.matter.deviceTypes.DoorLock,
|
|
773
|
+
serialNumber: 'LOCK-001',
|
|
774
|
+
manufacturer: 'Matter Examples',
|
|
775
|
+
model: 'DoorLock v1',
|
|
776
|
+
clusters: {
|
|
777
|
+
doorLock: {
|
|
778
|
+
// Lock state: 0=NotFullyLocked, 1=Locked, 2=Unlocked
|
|
779
|
+
lockState: 2, // Unlocked
|
|
780
|
+
// Lock type: 0=Deadbolt, 1=Magnetic, 2=Other, etc.
|
|
781
|
+
lockType: 0, // Deadbolt
|
|
782
|
+
// Actuator enabled (can be locked/unlocked)
|
|
783
|
+
actuatorEnabled: true,
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
handlers: {
|
|
787
|
+
doorLock: {
|
|
788
|
+
// Called when user locks the door
|
|
789
|
+
lockDoor: async () => {
|
|
790
|
+
this.log.info('[Door Lock] ✓ Handler `lockDoor` called - Locking door');
|
|
791
|
+
// Update the lock state to "Locked" (1)
|
|
792
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-door-lock'), 'doorLock', { lockState: 1 });
|
|
793
|
+
},
|
|
794
|
+
// Called when user unlocks the door
|
|
795
|
+
unlockDoor: async () => {
|
|
796
|
+
this.log.info('[Door Lock] ✓ Handler `unlockDoor` called - Unlocking door');
|
|
797
|
+
// Update the lock state to "Unlocked" (2)
|
|
798
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-door-lock'), 'doorLock', { lockState: 2 });
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
// 2. Garage Door Opener
|
|
805
|
+
if (this.config.enableGarageDoor) {
|
|
806
|
+
// Note: Matter uses WindowCovering device type for garage doors
|
|
807
|
+
accessories.push({
|
|
808
|
+
uuid: this.api.matter.uuid.generate('matter-garage-door'),
|
|
809
|
+
displayName: 'Garage Door',
|
|
810
|
+
deviceType: this.api.matter.deviceTypes.WindowCovering,
|
|
811
|
+
serialNumber: 'GARAGE-001',
|
|
812
|
+
manufacturer: 'Matter Examples',
|
|
813
|
+
model: 'GarageDoor v1',
|
|
814
|
+
clusters: {
|
|
815
|
+
windowCovering: {
|
|
816
|
+
// Target position (0 = fully closed, 10000 = fully open)
|
|
817
|
+
targetPositionLiftPercent100ths: 0, // Closed
|
|
818
|
+
// Current position
|
|
819
|
+
currentPositionLiftPercent100ths: 0, // Closed
|
|
820
|
+
// Operational status
|
|
821
|
+
operationalStatus: {
|
|
822
|
+
global: 0, // Not moving
|
|
823
|
+
lift: 0,
|
|
824
|
+
tilt: 0,
|
|
825
|
+
},
|
|
826
|
+
// End product type
|
|
827
|
+
endProductType: 7, // Garage door
|
|
828
|
+
// Configuration: supports lift positioning
|
|
829
|
+
configStatus: {
|
|
830
|
+
operational: true,
|
|
831
|
+
onlineReserved: true,
|
|
832
|
+
liftMovementReversed: false,
|
|
833
|
+
liftPositionAware: true,
|
|
834
|
+
tiltPositionAware: false,
|
|
835
|
+
liftEncoderControlled: true,
|
|
836
|
+
tiltEncoderControlled: false,
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
handlers: {
|
|
841
|
+
windowCovering: {
|
|
842
|
+
// Called when user opens/closes garage door
|
|
843
|
+
goToLiftPercentage: async (request) => {
|
|
844
|
+
const percent = (request.targetPercent / 100).toFixed(0);
|
|
845
|
+
this.log.info(`[Garage Door] ✓ Handler \`goToLiftPercentage\` called: ${request.targetPercent} (${percent}% open)`);
|
|
846
|
+
// Update position
|
|
847
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-garage-door'), 'windowCovering', {
|
|
848
|
+
currentPositionLiftPercent100ths: request.targetPercent,
|
|
849
|
+
targetPositionLiftPercent100ths: request.targetPercent,
|
|
850
|
+
});
|
|
851
|
+
},
|
|
852
|
+
// Called when user presses "up" (open)
|
|
853
|
+
upOrOpen: async () => {
|
|
854
|
+
this.log.info('[Garage Door] ✓ Handler `upOrOpen` called - Opening garage door');
|
|
855
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-garage-door'), 'windowCovering', {
|
|
856
|
+
currentPositionLiftPercent100ths: 10000, // Fully open
|
|
857
|
+
targetPositionLiftPercent100ths: 10000,
|
|
858
|
+
});
|
|
859
|
+
},
|
|
860
|
+
// Called when user presses "down" (close)
|
|
861
|
+
downOrClose: async () => {
|
|
862
|
+
this.log.info('[Garage Door] ✓ Handler `downOrClose` called - Closing garage door');
|
|
863
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-garage-door'), 'windowCovering', {
|
|
864
|
+
currentPositionLiftPercent100ths: 0, // Fully closed
|
|
865
|
+
targetPositionLiftPercent100ths: 0,
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
// Called when user presses "stop"
|
|
869
|
+
stopMotion: async () => {
|
|
870
|
+
this.log.info('[Garage Door] ✓ Handler `stopMotion` called - Stopping garage door');
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (accessories.length > 0) {
|
|
877
|
+
this.log.info(`✓ Registered ${accessories.length} security & access accessories`);
|
|
878
|
+
for (const acc of accessories) {
|
|
879
|
+
this.log.info(` - ${acc.displayName}`);
|
|
880
|
+
}
|
|
881
|
+
// Register all security accessories
|
|
882
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Register Matter Window Covering accessories (Blinds, Shades)
|
|
887
|
+
*/
|
|
888
|
+
registerWindowCoverings() {
|
|
889
|
+
this.log.info('═'.repeat(80));
|
|
890
|
+
this.log.info('Registering Matter Window Covering Devices');
|
|
891
|
+
this.log.info('═'.repeat(80));
|
|
892
|
+
const accessories = [];
|
|
893
|
+
// 1. Window Covering (Blind/Shade with position control)
|
|
894
|
+
if (this.config.enableWindowBlind) {
|
|
895
|
+
accessories.push({
|
|
896
|
+
uuid: this.api.matter.uuid.generate('matter-window-blind'),
|
|
897
|
+
displayName: 'Window Blind',
|
|
898
|
+
deviceType: this.api.matter.deviceTypes.WindowCovering,
|
|
899
|
+
serialNumber: 'BLIND-001',
|
|
900
|
+
manufacturer: 'Matter Examples',
|
|
901
|
+
model: 'WindowBlind v1',
|
|
902
|
+
clusters: {
|
|
903
|
+
windowCovering: {
|
|
904
|
+
// Target position (0 = fully closed, 10000 = fully open, in hundredths of percent)
|
|
905
|
+
targetPositionLiftPercent100ths: 5000, // 50% open
|
|
906
|
+
// Current position
|
|
907
|
+
currentPositionLiftPercent100ths: 5000, // 50% open
|
|
908
|
+
// Operational status
|
|
909
|
+
operationalStatus: {
|
|
910
|
+
global: 0, // Not moving
|
|
911
|
+
lift: 0,
|
|
912
|
+
tilt: 0,
|
|
913
|
+
},
|
|
914
|
+
// End product type
|
|
915
|
+
endProductType: 0, // Rollershade
|
|
916
|
+
// Configuration
|
|
917
|
+
configStatus: {
|
|
918
|
+
operational: true,
|
|
919
|
+
onlineReserved: true,
|
|
920
|
+
liftMovementReversed: false,
|
|
921
|
+
liftPositionAware: true,
|
|
922
|
+
tiltPositionAware: false,
|
|
923
|
+
liftEncoderControlled: true,
|
|
924
|
+
tiltEncoderControlled: false,
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
handlers: {
|
|
929
|
+
windowCovering: {
|
|
930
|
+
// Called when user sets position via slider
|
|
931
|
+
goToLiftPercentage: async (request) => {
|
|
932
|
+
const percent = (request.targetPercent / 100).toFixed(0);
|
|
933
|
+
this.log.info(`[Window Blind] ✓ Handler \`goToLiftPercentage\` called: ${request.targetPercent} (${percent}% open)`);
|
|
934
|
+
// Update position
|
|
935
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-window-blind'), 'windowCovering', {
|
|
936
|
+
currentPositionLiftPercent100ths: request.targetPercent,
|
|
937
|
+
targetPositionLiftPercent100ths: request.targetPercent,
|
|
938
|
+
});
|
|
939
|
+
},
|
|
940
|
+
// Called when user presses "up" (open)
|
|
941
|
+
upOrOpen: async () => {
|
|
942
|
+
this.log.info('[Window Blind] ✓ Handler `upOrOpen` called - Opening blind');
|
|
943
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-window-blind'), 'windowCovering', {
|
|
944
|
+
currentPositionLiftPercent100ths: 10000, // Fully open
|
|
945
|
+
targetPositionLiftPercent100ths: 10000,
|
|
946
|
+
});
|
|
947
|
+
},
|
|
948
|
+
// Called when user presses "down" (close)
|
|
949
|
+
downOrClose: async () => {
|
|
950
|
+
this.log.info('[Window Blind] ✓ Handler `downOrClose` called - Closing blind');
|
|
951
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-window-blind'), 'windowCovering', {
|
|
952
|
+
currentPositionLiftPercent100ths: 0, // Fully closed
|
|
953
|
+
targetPositionLiftPercent100ths: 0,
|
|
954
|
+
});
|
|
955
|
+
},
|
|
956
|
+
// Called when user presses "stop"
|
|
957
|
+
stopMotion: async () => {
|
|
958
|
+
this.log.info('[Window Blind] ✓ Handler `stopMotion` called - Stopping blind');
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
// 2. Window Covering with Tilt (Venetian Blind)
|
|
965
|
+
if (this.config.enableVenetianBlind) {
|
|
966
|
+
accessories.push({
|
|
967
|
+
uuid: this.api.matter.uuid.generate('matter-venetian-blind'),
|
|
968
|
+
displayName: 'Venetian Blind (Tilt)',
|
|
969
|
+
deviceType: this.api.matter.deviceTypes.WindowCovering,
|
|
970
|
+
serialNumber: 'BLIND-002',
|
|
971
|
+
manufacturer: 'Matter Examples',
|
|
972
|
+
model: 'VenetianBlind v1',
|
|
973
|
+
clusters: {
|
|
974
|
+
windowCovering: {
|
|
975
|
+
// Lift position (vertical position)
|
|
976
|
+
targetPositionLiftPercent100ths: 5000, // 50% open
|
|
977
|
+
currentPositionLiftPercent100ths: 5000,
|
|
978
|
+
// Tilt position (slat angle: 0 = closed, 10000 = fully open)
|
|
979
|
+
targetPositionTiltPercent100ths: 5000, // 50% tilted
|
|
980
|
+
currentPositionTiltPercent100ths: 5000,
|
|
981
|
+
// Operational status
|
|
982
|
+
operationalStatus: {
|
|
983
|
+
global: 0,
|
|
984
|
+
lift: 0,
|
|
985
|
+
tilt: 0,
|
|
986
|
+
},
|
|
987
|
+
// End product type
|
|
988
|
+
endProductType: 8, // Venetian blind
|
|
989
|
+
// Configuration: supports both lift and tilt
|
|
990
|
+
configStatus: {
|
|
991
|
+
operational: true,
|
|
992
|
+
onlineReserved: true,
|
|
993
|
+
liftMovementReversed: false,
|
|
994
|
+
liftPositionAware: true,
|
|
995
|
+
tiltPositionAware: true,
|
|
996
|
+
liftEncoderControlled: true,
|
|
997
|
+
tiltEncoderControlled: true,
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
},
|
|
1001
|
+
handlers: {
|
|
1002
|
+
windowCovering: {
|
|
1003
|
+
// Called when user sets lift position
|
|
1004
|
+
goToLiftPercentage: async (request) => {
|
|
1005
|
+
const percent = (request.targetPercent / 100).toFixed(0);
|
|
1006
|
+
this.log.info(`[Venetian Blind] ✓ Handler \`goToLiftPercentage\` called: ${request.targetPercent} (${percent}% open)`);
|
|
1007
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-venetian-blind'), 'windowCovering', {
|
|
1008
|
+
currentPositionLiftPercent100ths: request.targetPercent,
|
|
1009
|
+
targetPositionLiftPercent100ths: request.targetPercent,
|
|
1010
|
+
});
|
|
1011
|
+
},
|
|
1012
|
+
// Called when user sets tilt angle
|
|
1013
|
+
goToTiltPercentage: async (request) => {
|
|
1014
|
+
const percent = (request.targetPercent / 100).toFixed(0);
|
|
1015
|
+
this.log.info(`[Venetian Blind] ✓ Handler \`goToTiltPercentage\` called: ${request.targetPercent} (${percent}% tilted)`);
|
|
1016
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-venetian-blind'), 'windowCovering', {
|
|
1017
|
+
currentPositionTiltPercent100ths: request.targetPercent,
|
|
1018
|
+
targetPositionTiltPercent100ths: request.targetPercent,
|
|
1019
|
+
});
|
|
1020
|
+
},
|
|
1021
|
+
upOrOpen: async () => {
|
|
1022
|
+
this.log.info('[Venetian Blind] ✓ Handler `upOrOpen` called');
|
|
1023
|
+
},
|
|
1024
|
+
downOrClose: async () => {
|
|
1025
|
+
this.log.info('[Venetian Blind] ✓ Handler `downOrClose` called');
|
|
1026
|
+
},
|
|
1027
|
+
stopMotion: async () => {
|
|
1028
|
+
this.log.info('[Venetian Blind] ✓ Handler `stopMotion` called');
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (accessories.length > 0) {
|
|
1035
|
+
this.log.info(`✓ Registered ${accessories.length} window covering accessories`);
|
|
1036
|
+
for (const acc of accessories) {
|
|
1037
|
+
this.log.info(` - ${acc.displayName}`);
|
|
1038
|
+
}
|
|
1039
|
+
// Register all window covering accessories
|
|
1040
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Register Matter Appliance accessories (Robotic Vacuum Cleaners, etc.)
|
|
1045
|
+
*/
|
|
1046
|
+
registerAppliances() {
|
|
1047
|
+
this.log.info('═'.repeat(80));
|
|
1048
|
+
this.log.info('Registering Matter Appliance Devices');
|
|
1049
|
+
this.log.info('═'.repeat(80));
|
|
1050
|
+
const accessories = [];
|
|
1051
|
+
// 1. Robotic Vacuum Cleaner
|
|
1052
|
+
if (this.config.enableRobotVacuum) {
|
|
1053
|
+
accessories.push({
|
|
1054
|
+
uuid: this.api.matter.uuid.generate('matter-robot-vacuum'),
|
|
1055
|
+
displayName: 'Robot Vacuum',
|
|
1056
|
+
deviceType: this.api.matter.deviceTypes.RoboticVacuumCleaner,
|
|
1057
|
+
serialNumber: 'VACUUM-001',
|
|
1058
|
+
manufacturer: 'Matter Examples',
|
|
1059
|
+
model: 'RobotVacuum v1',
|
|
1060
|
+
clusters: {
|
|
1061
|
+
rvcRunMode: {
|
|
1062
|
+
// Supported run modes (0=Idle, 1=Cleaning, 2=Mapping)
|
|
1063
|
+
supportedModes: [
|
|
1064
|
+
{ label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] }, // 16384 = Idle tag
|
|
1065
|
+
{ label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] }, // 16385 = Cleaning tag
|
|
1066
|
+
{ label: 'Mapping', mode: 2, modeTags: [{ value: 16386 }] }, // 16386 = Mapping tag
|
|
1067
|
+
],
|
|
1068
|
+
// Current mode
|
|
1069
|
+
currentMode: 0, // Idle
|
|
1070
|
+
},
|
|
1071
|
+
rvcOperationalState: {
|
|
1072
|
+
// Operational state list (must include at least an error state)
|
|
1073
|
+
operationalStateList: [
|
|
1074
|
+
{ operationalStateId: 0 }, // Stopped
|
|
1075
|
+
{ operationalStateId: 1 }, // Running
|
|
1076
|
+
{ operationalStateId: 2 }, // Paused
|
|
1077
|
+
{ operationalStateId: 3 }, // Error (required)
|
|
1078
|
+
{ operationalStateId: 64 }, // SeekingCharger
|
|
1079
|
+
{ operationalStateId: 65 }, // Charging
|
|
1080
|
+
{ operationalStateId: 66 }, // Docked
|
|
1081
|
+
],
|
|
1082
|
+
// Current operational state (just the ID, not an object)
|
|
1083
|
+
operationalState: 66, // Docked
|
|
1084
|
+
// Error state
|
|
1085
|
+
operationalError: {
|
|
1086
|
+
errorStateId: 0, // No error
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
rvcCleanMode: {
|
|
1090
|
+
// Supported clean modes (0=Vacuum, 1=Mop, 2=Vacuum+Mop)
|
|
1091
|
+
supportedModes: [
|
|
1092
|
+
{ label: 'Vacuum', mode: 0, modeTags: [] },
|
|
1093
|
+
{ label: 'Mop', mode: 1, modeTags: [] },
|
|
1094
|
+
{ label: 'Vacuum & Mop', mode: 2, modeTags: [] },
|
|
1095
|
+
],
|
|
1096
|
+
// Current clean mode
|
|
1097
|
+
currentMode: 0, // Vacuum
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
handlers: {
|
|
1101
|
+
rvcOperationalState: {
|
|
1102
|
+
// Called when user presses "pause" in Home app
|
|
1103
|
+
pause: async () => {
|
|
1104
|
+
this.log.info('[Robot Vacuum] ✓ Handler `pause` called - Pausing cleaning');
|
|
1105
|
+
// Update state to Paused (2)
|
|
1106
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcOperationalState', { operationalState: 2 });
|
|
1107
|
+
},
|
|
1108
|
+
// Called when user presses "resume" or "start" in Home app
|
|
1109
|
+
resume: async () => {
|
|
1110
|
+
this.log.info('[Robot Vacuum] ✓ Handler `resume` called - Resuming cleaning');
|
|
1111
|
+
// Update state to Running (1)
|
|
1112
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcOperationalState', { operationalState: 1 });
|
|
1113
|
+
},
|
|
1114
|
+
// Called when user sends robot to charging dock
|
|
1115
|
+
goHome: async () => {
|
|
1116
|
+
this.log.info('[Robot Vacuum] ✓ Handler `goHome` called - Returning to dock');
|
|
1117
|
+
// Update state to SeekingCharger (64)
|
|
1118
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcOperationalState', { operationalState: 64 });
|
|
1119
|
+
// Simulate arriving at dock after 3 seconds
|
|
1120
|
+
setTimeout(async () => {
|
|
1121
|
+
this.log.info('[Robot Vacuum] → Arrived at dock, now docked');
|
|
1122
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcOperationalState', { operationalState: 66 });
|
|
1123
|
+
}, 3000);
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
rvcRunMode: {
|
|
1127
|
+
// Called when user changes run mode (Idle, Cleaning, Mapping)
|
|
1128
|
+
changeToMode: async (request) => {
|
|
1129
|
+
const modes = ['Idle', 'Cleaning', 'Mapping'];
|
|
1130
|
+
const modeName = modes[request.newMode] || `Unknown (${request.newMode})`;
|
|
1131
|
+
this.log.info(`[Robot Vacuum] ✓ Handler \`changeToMode\` called: ${request.newMode} (${modeName})`);
|
|
1132
|
+
// Update the current mode
|
|
1133
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcRunMode', { currentMode: request.newMode });
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
rvcCleanMode: {
|
|
1137
|
+
// Called when user changes clean mode (Vacuum, Mop, Vacuum+Mop)
|
|
1138
|
+
changeToMode: async (request) => {
|
|
1139
|
+
const modes = ['Vacuum', 'Mop', 'Vacuum & Mop'];
|
|
1140
|
+
const modeName = modes[request.newMode] || `Unknown (${request.newMode})`;
|
|
1141
|
+
this.log.info(`[Robot Vacuum] ✓ Handler \`changeToMode\` called: ${request.newMode} (${modeName})`);
|
|
1142
|
+
// Update the current clean mode
|
|
1143
|
+
await this.api.matter.updateAccessoryState(this.api.matter.uuid.generate('matter-robot-vacuum'), 'rvcCleanMode', { currentMode: request.newMode });
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
if (accessories.length > 0) {
|
|
1150
|
+
this.log.info(`✓ Registered ${accessories.length} appliance accessories`);
|
|
1151
|
+
for (const acc of accessories) {
|
|
1152
|
+
this.log.info(` - ${acc.displayName}`);
|
|
1153
|
+
}
|
|
1154
|
+
// Register all appliance accessories
|
|
1155
|
+
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
559
1156
|
}
|
|
560
|
-
// Register all sensor accessories
|
|
561
|
-
this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
|
|
562
1157
|
}
|
|
563
1158
|
}
|
|
564
1159
|
//# sourceMappingURL=platform.js.map
|