@homebridge-plugins/homebridge-matter 0.0.6 → 0.1.0

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.
Files changed (95) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +67 -33
  3. package/config.schema.json +73 -77
  4. package/dist/devices/index.d.ts +13 -0
  5. package/dist/devices/index.js +22 -0
  6. package/dist/devices/index.js.map +1 -0
  7. package/dist/devices/section-12-robotic/index.d.ts +6 -0
  8. package/dist/devices/section-12-robotic/index.js +7 -0
  9. package/dist/devices/section-12-robotic/index.js.map +1 -0
  10. package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.d.ts +63 -0
  11. package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.js +318 -0
  12. package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.js.map +1 -0
  13. package/dist/devices/section-4-lighting/color-temperature-light.d.ts +7 -0
  14. package/dist/devices/section-4-lighting/color-temperature-light.js +62 -0
  15. package/dist/devices/section-4-lighting/color-temperature-light.js.map +1 -0
  16. package/dist/devices/section-4-lighting/dimmable-light.d.ts +7 -0
  17. package/dist/devices/section-4-lighting/dimmable-light.js +48 -0
  18. package/dist/devices/section-4-lighting/dimmable-light.js.map +1 -0
  19. package/dist/devices/section-4-lighting/extended-color-light.d.ts +12 -0
  20. package/dist/devices/section-4-lighting/extended-color-light.js +142 -0
  21. package/dist/devices/section-4-lighting/extended-color-light.js.map +1 -0
  22. package/dist/devices/section-4-lighting/index.d.ts +9 -0
  23. package/dist/devices/section-4-lighting/index.js +10 -0
  24. package/dist/devices/section-4-lighting/index.js.map +1 -0
  25. package/dist/devices/section-4-lighting/on-off-light.d.ts +7 -0
  26. package/dist/devices/section-4-lighting/on-off-light.js +37 -0
  27. package/dist/devices/section-4-lighting/on-off-light.js.map +1 -0
  28. package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.d.ts +7 -0
  29. package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.js +48 -0
  30. package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.js.map +1 -0
  31. package/dist/devices/section-5-smart-plugs/index.d.ts +7 -0
  32. package/dist/devices/section-5-smart-plugs/index.js +8 -0
  33. package/dist/devices/section-5-smart-plugs/index.js.map +1 -0
  34. package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.d.ts +7 -0
  35. package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.js +37 -0
  36. package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.js.map +1 -0
  37. package/dist/devices/section-6-switches/index.d.ts +6 -0
  38. package/dist/devices/section-6-switches/index.js +7 -0
  39. package/dist/devices/section-6-switches/index.js.map +1 -0
  40. package/dist/devices/section-6-switches/on-off-light-switch.d.ts +7 -0
  41. package/dist/devices/section-6-switches/on-off-light-switch.js +30 -0
  42. package/dist/devices/section-6-switches/on-off-light-switch.js.map +1 -0
  43. package/dist/devices/section-7-sensors/contact-sensor.d.ts +7 -0
  44. package/dist/devices/section-7-sensors/contact-sensor.js +27 -0
  45. package/dist/devices/section-7-sensors/contact-sensor.js.map +1 -0
  46. package/dist/devices/section-7-sensors/humidity-sensor.d.ts +7 -0
  47. package/dist/devices/section-7-sensors/humidity-sensor.js +29 -0
  48. package/dist/devices/section-7-sensors/humidity-sensor.js.map +1 -0
  49. package/dist/devices/section-7-sensors/index.d.ts +12 -0
  50. package/dist/devices/section-7-sensors/index.js +13 -0
  51. package/dist/devices/section-7-sensors/index.js.map +1 -0
  52. package/dist/devices/section-7-sensors/light-sensor.d.ts +7 -0
  53. package/dist/devices/section-7-sensors/light-sensor.js +29 -0
  54. package/dist/devices/section-7-sensors/light-sensor.js.map +1 -0
  55. package/dist/devices/section-7-sensors/occupancy-sensor.d.ts +8 -0
  56. package/dist/devices/section-7-sensors/occupancy-sensor.js +33 -0
  57. package/dist/devices/section-7-sensors/occupancy-sensor.js.map +1 -0
  58. package/dist/devices/section-7-sensors/smoke-co-alarm.d.ts +7 -0
  59. package/dist/devices/section-7-sensors/smoke-co-alarm.js +37 -0
  60. package/dist/devices/section-7-sensors/smoke-co-alarm.js.map +1 -0
  61. package/dist/devices/section-7-sensors/temperature-sensor.d.ts +7 -0
  62. package/dist/devices/section-7-sensors/temperature-sensor.js +29 -0
  63. package/dist/devices/section-7-sensors/temperature-sensor.js.map +1 -0
  64. package/dist/devices/section-7-sensors/water-leak-detector.d.ts +7 -0
  65. package/dist/devices/section-7-sensors/water-leak-detector.js +27 -0
  66. package/dist/devices/section-7-sensors/water-leak-detector.js.map +1 -0
  67. package/dist/devices/section-8-closure/door-lock.d.ts +7 -0
  68. package/dist/devices/section-8-closure/door-lock.js +48 -0
  69. package/dist/devices/section-8-closure/door-lock.js.map +1 -0
  70. package/dist/devices/section-8-closure/index.d.ts +7 -0
  71. package/dist/devices/section-8-closure/index.js +8 -0
  72. package/dist/devices/section-8-closure/index.js.map +1 -0
  73. package/dist/devices/section-8-closure/window-covering.d.ts +9 -0
  74. package/dist/devices/section-8-closure/window-covering.js +154 -0
  75. package/dist/devices/section-8-closure/window-covering.js.map +1 -0
  76. package/dist/devices/section-9-hvac/fan.d.ts +7 -0
  77. package/dist/devices/section-9-hvac/fan.js +56 -0
  78. package/dist/devices/section-9-hvac/fan.js.map +1 -0
  79. package/dist/devices/section-9-hvac/index.d.ts +7 -0
  80. package/dist/devices/section-9-hvac/index.js +8 -0
  81. package/dist/devices/section-9-hvac/index.js.map +1 -0
  82. package/dist/devices/section-9-hvac/thermostat.d.ts +7 -0
  83. package/dist/devices/section-9-hvac/thermostat.js +61 -0
  84. package/dist/devices/section-9-hvac/thermostat.js.map +1 -0
  85. package/dist/devices/types.d.ts +16 -0
  86. package/dist/devices/types.js +5 -0
  87. package/dist/devices/types.js.map +1 -0
  88. package/dist/homebridge-ui/public/index.html +269 -0
  89. package/dist/homebridge-ui/server.js +47 -0
  90. package/dist/platform.d.ts +25 -21
  91. package/dist/platform.js +133 -1023
  92. package/dist/platform.js.map +1 -1
  93. package/package.json +9 -9
  94. package/plugin-header.png +0 -0
  95. package/.claude/settings.local.json +0 -28
package/dist/platform.js CHANGED
@@ -1,14 +1,18 @@
1
+ import { registerColorTemperatureLight, registerContactSensor, registerDimmableLight, registerDimmablePlugInUnit, registerDoorLock, registerExtendedColorLight, registerFan, registerHumiditySensor, registerLightSensor, registerOccupancySensor, registerOnOffLight, registerOnOffLightSwitch, registerOnOffPlugInUnit, registerRoboticVacuumCleaner, registerSmokeCoAlarm, registerTemperatureSensor, registerThermostat, registerWaterLeakDetector, registerWindowCovering, } from './devices/index.js';
1
2
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
2
3
  /**
3
4
  * MatterPlatform
4
5
  * Demonstrates all available Matter device types in Homebridge
6
+ *
7
+ * Organized by official Matter Specification v1.4.1 categories
5
8
  */
6
9
  export class ExampleHomebridgePlatform {
7
10
  log;
8
11
  config;
9
12
  api;
10
13
  // Track restored HAP cached accessories (required for DynamicPlatformPlugin)
11
- accessories = new Map();
14
+ // This is commented out here as this plugin does not have any HAP accessories
15
+ // public readonly accessories: Map<string, PlatformAccessory> = new Map()
12
16
  // Track restored Matter cached accessories
13
17
  matterAccessories = new Map();
14
18
  constructor(log, config, api) {
@@ -16,7 +20,15 @@ export class ExampleHomebridgePlatform {
16
20
  this.config = config;
17
21
  this.api = api;
18
22
  this.log.debug('Finished initializing platform:', this.config.name);
19
- // Check if the user has matter enabled
23
+ // Does the user have a version of Homebridge that is compatible with matter?
24
+ if (!this.api.isMatterAvailable?.()) {
25
+ this.log.warn('Matter is not available in this version of Homebridge. Please update Homebridge to use this plugin.');
26
+ }
27
+ // Check if the user has matter enabled, this means:
28
+ // - If the plugin is running on the main bridge, then the user must have enabled matter in the Homebridge settings page in the UI
29
+ // - If the plugin is running on a child bridge, then the user must have enabled matter on the plugin bridge settings section in the UI
30
+ // In reality, only the below check is needed, but they are both included here for completeness
31
+ // Remember to use a '?.' optional chaining operator in case the user is running an older version of Homebridge that does not have these APIs
20
32
  if (!this.api.isMatterEnabled?.()) {
21
33
  this.log.warn('Matter is not enabled in Homebridge. Please enable Matter in the Homebridge settings to use this plugin.');
22
34
  return;
@@ -31,75 +43,52 @@ export class ExampleHomebridgePlatform {
31
43
  * Required for DynamicPlatformPlugin
32
44
  * Called when homebridge restores cached accessories from disk at startup
33
45
  */
34
- configureAccessory(accessory) {
46
+ configureAccessory( /* accessory: PlatformAccessory */) {
35
47
  // Note this is not used for Matter accessories - use configureMatterAccessory instead
36
- this.accessories.set(accessory.UUID, accessory);
48
+ // This plugin does not have any hap accessories, so here we can comment this out
49
+ // this.accessories.set(accessory.UUID, accessory)
37
50
  }
38
51
  /**
39
- * Called for each cached Matter accessory restored from disk
40
- * Track these so we can determine which accessories to remove in didFinishLaunching
52
+ * Called when homebridge restores cached Matter accessories from disk at startup
41
53
  */
42
54
  configureMatterAccessory(accessory) {
43
- this.log.info(`✓ Restored Matter accessory from cache: ${accessory.displayName}`);
44
- // Track cached Matter accessories (in real plugin, compare with cloud devices and remove orphans)
55
+ this.log.debug('Loading cached Matter accessory:', accessory.displayName);
45
56
  this.matterAccessories.set(accessory.uuid, accessory);
46
57
  }
47
58
  /**
48
- * Register all Matter accessory examples
59
+ * Register all Matter accessories
49
60
  */
50
61
  registerMatterAccessories() {
51
- this.log.info('='.repeat(80));
52
- this.log.info('HAP cached accessories:', this.accessories.size);
53
- this.log.info('Matter cached accessories:', this.matterAccessories.size);
54
- this.log.info('='.repeat(80));
55
- this.log.info('Registering Matter accessories...');
56
- // Remove disabled accessories that are cached
62
+ this.log.info(''.repeat(80));
63
+ this.log.info('Homebridge Matter Plugin');
64
+ this.log.info(''.repeat(80));
65
+ // Remove accessories that are disabled in config
57
66
  this.removeDisabledAccessories();
58
- // Register each device type
59
- this.registerLightingDevices();
60
- this.registerSwitchesAndOutlets();
61
- this.registerSensors();
62
- this.registerHVAC();
63
- this.registerSecurity();
64
- this.registerWindowCoverings();
65
- this.registerAppliances();
67
+ // Register devices by Matter specification sections
68
+ this.registerSection4Lighting();
69
+ this.registerSection5SmartPlugs();
70
+ this.registerSection6Switches();
71
+ this.registerSection7Sensors();
72
+ this.registerSection8Closure();
73
+ this.registerSection9HVAC();
74
+ this.registerSection12Robotic();
75
+ this.log.info('═'.repeat(80));
66
76
  this.log.info('Finished registering Matter accessories');
67
- // You can read current state using the API
68
- const onOffState = this.api.matter.getAccessoryState('matter-dimmable-light', this.api.matter.clusterNames.OnOff);
69
- if (onOffState) {
70
- this.log.info(`[On/Off Light] 📖 Reading Dimmable Light on/off via API: ${onOffState.onOff ? 'ON' : 'OFF'}`);
71
- }
72
- const levelState = this.api.matter.getAccessoryState('matter-dimmable-light', this.api.matter.clusterNames.LevelControl);
73
- if (levelState) {
74
- this.log.info(`[On/Off Light] 📖 Reading Dimmable Light brightness via API: ${levelState.currentLevel} (${Math.round((levelState?.currentLevel || 0) / 254 * 100)}%)`);
75
- }
76
- // // ═══════════════════════════════════════════════════════════════
77
- // // PATTERN 2 DEMONSTRATION: Update Dimmable Light WITHOUT handler
78
- // // ═══════════════════════════════════════════════════════════════
79
- // // Simulating: Dimmable light state changed externally (like via native app)
80
- // // This will update the Home app WITHOUT triggering the dimmable light's handler
81
- // // Note: Homebridge automatically defers the update to avoid transaction conflicts
82
- // this.log.info('[On/Off Light] → Updating Dimmable Light state using updateMatterAccessoryState (no handler!)')
83
- //
84
- // this.api.matter.updateAccessoryState(uuidLightDimmable, this.api.matter.clusterNames.OnOff, {
85
- // onOff: true,
86
- // })
77
+ this.log.info('═'.repeat(80));
87
78
  }
88
79
  /**
89
- * Remove disabled accessories from cache
80
+ * Remove accessories that are disabled in config
90
81
  */
91
82
  removeDisabledAccessories() {
92
- const accessoriesToRemove = [];
93
- // Define mapping of config flags to UUIDs
94
83
  const configMap = [
95
84
  { enabled: this.config.enableOnOffLight, uuid: this.api.matter.uuid.generate('matter-onoff-light'), name: 'On/Off Light' },
96
85
  { enabled: this.config.enableDimmableLight, uuid: this.api.matter.uuid.generate('matter-dimmable-light'), name: 'Dimmable Light' },
97
86
  { 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' },
87
+ { enabled: this.config.enableColourLight, uuid: this.api.matter.uuid.generate('matter-colour-light'), name: 'Colour Light (HS)' },
99
88
  { 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
89
  { enabled: this.config.enableOnOffOutlet, uuid: this.api.matter.uuid.generate('matter-onoff-outlet'), name: 'On/Off Outlet' },
102
90
  { enabled: this.config.enableDimmableOutlet, uuid: this.api.matter.uuid.generate('matter-dimmable-outlet'), name: 'Dimmable Outlet' },
91
+ { enabled: this.config.enableOnOffSwitch, uuid: this.api.matter.uuid.generate('matter-onoff-switch'), name: 'On/Off Switch' },
103
92
  { enabled: this.config.enableTemperatureSensor, uuid: this.api.hap.uuid.generate('matter-temperature-sensor'), name: 'Temperature Sensor' },
104
93
  { enabled: this.config.enableHumiditySensor, uuid: this.api.hap.uuid.generate('matter-humidity-sensor'), name: 'Humidity Sensor' },
105
94
  { enabled: this.config.enableLightSensor, uuid: this.api.hap.uuid.generate('matter-light-sensor'), name: 'Light Sensor' },
@@ -107,1052 +96,173 @@ export class ExampleHomebridgePlatform {
107
96
  { enabled: this.config.enableContactSensor, uuid: this.api.hap.uuid.generate('matter-contact-sensor'), name: 'Contact Sensor' },
108
97
  { enabled: this.config.enableLeakSensor, uuid: this.api.hap.uuid.generate('matter-leak-sensor'), name: 'Leak Sensor' },
109
98
  { 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
99
  { 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
100
  { enabled: this.config.enableWindowBlind, uuid: this.api.matter.uuid.generate('matter-window-blind'), name: 'Window Blind' },
115
101
  { enabled: this.config.enableVenetianBlind, uuid: this.api.matter.uuid.generate('matter-venetian-blind'), name: 'Venetian Blind' },
102
+ { enabled: this.config.enableThermostat, uuid: this.api.matter.uuid.generate('matter-thermostat'), name: 'Thermostat' },
103
+ { enabled: this.config.enableFan, uuid: this.api.matter.uuid.generate('matter-fan'), name: 'Fan' },
116
104
  { enabled: this.config.enableRobotVacuum, uuid: this.api.matter.uuid.generate('matter-robot-vacuum'), name: 'Robot Vacuum' },
117
105
  ];
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);
106
+ for (const { enabled, uuid, name } of configMap) {
107
+ if (enabled === false) {
108
+ const existingAccessory = this.matterAccessories.get(uuid);
109
+ if (existingAccessory) {
110
+ this.log.info(`Removing accessory '${name}' (disabled in config)`);
111
+ this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]);
112
+ this.matterAccessories.delete(uuid);
113
+ }
132
114
  }
133
115
  }
134
116
  }
135
117
  /**
136
- * Lighting Devices
118
+ * Section 4: Lighting Devices (Matter Spec § 4)
137
119
  */
138
- registerLightingDevices() {
139
- const uuidLightOnOff = this.api.matter.uuid.generate('matter-onoff-light');
140
- const uuidLightDimmable = this.api.matter.uuid.generate('matter-dimmable-light');
141
- const uuidLightColourTemp = this.api.matter.uuid.generate('matter-colour-temp-light');
142
- const uuidLightColour = this.api.matter.uuid.generate('matter-colour-light');
143
- const uuidLightExtendedColour = this.api.matter.uuid.generate('matter-extended-colour-light');
144
- // 1. On/Off Light
145
- const accessories = [];
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
- }
172
- // 2. Dimmable Light
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
- }
210
- // 3. Colour Temperature Light
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
- }
262
- // 4. Colour Light (Hue/Saturation ONLY - no CCT)
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
- }
322
- // 5. Extended Colour Light (Hue/Saturation + CCT)
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
- }
390
- // Register all lighting accessories
120
+ registerSection4Lighting() {
121
+ this.log.info(''.repeat(80));
122
+ this.log.info('Section 4: Lighting Devices (Matter Spec § 4)');
123
+ this.log.info(''.repeat(80));
124
+ const context = { api: this.api, log: this.log, config: this.config };
125
+ const accessories = [
126
+ ...registerOnOffLight(context),
127
+ ...registerDimmableLight(context),
128
+ ...registerColorTemperatureLight(context),
129
+ ...registerExtendedColorLight(context),
130
+ ];
391
131
  if (accessories.length > 0) {
132
+ this.log.info(`✓ Registered ${accessories.length} lighting device(s)`);
133
+ for (const acc of accessories) {
134
+ this.log.info(` - ${acc.displayName}`);
135
+ }
392
136
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
393
137
  }
394
138
  }
395
139
  /**
396
- * Switches and Outlets
140
+ * Section 5: Smart Plugs/Actuators (Matter Spec § 5)
397
141
  */
398
- registerSwitchesAndOutlets() {
399
- const uuidSwitch = this.api.matter.uuid.generate('matter-onoff-switch');
400
- const uuidOutlet = this.api.matter.uuid.generate('matter-onoff-outlet');
401
- const uuidDimmableOutlet = this.api.matter.uuid.generate('matter-dimmable-outlet');
402
- const accessories = [];
403
- // 1. On/Off Switch
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
- }
429
- // 2. On/Off Outlet (Smart Plug)
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
- }
455
- // 3. Dimmable Outlet
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
- }
492
- // Register all switch/outlet accessories
142
+ registerSection5SmartPlugs() {
143
+ this.log.info(''.repeat(80));
144
+ this.log.info('Section 5: Smart Plugs/Actuators (Matter Spec § 5)');
145
+ this.log.info(''.repeat(80));
146
+ const context = { api: this.api, log: this.log, config: this.config };
147
+ const accessories = [
148
+ ...registerOnOffPlugInUnit(context),
149
+ ...registerDimmablePlugInUnit(context),
150
+ ];
493
151
  if (accessories.length > 0) {
152
+ this.log.info(`✓ Registered ${accessories.length} smart plug/actuator device(s)`);
153
+ for (const acc of accessories) {
154
+ this.log.info(` - ${acc.displayName}`);
155
+ }
494
156
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
495
157
  }
496
158
  }
497
159
  /**
498
- * Register Matter sensor accessories
160
+ * Section 6: Switches & Controllers (Matter Spec § 6)
499
161
  */
500
- registerSensors() {
162
+ registerSection6Switches() {
501
163
  this.log.info('═'.repeat(80));
502
- this.log.info('Registering Matter Sensor Devices');
164
+ this.log.info('Section 6: Switches & Controllers (Matter Spec § 6)');
503
165
  this.log.info('═'.repeat(80));
504
- const accessories = [];
505
- // 1. Temperature Sensor
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
- }
523
- // 2. Humidity Sensor
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
- }
541
- // 3. Light Sensor
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
- }
559
- // 4. Motion Sensor (Occupancy)
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
- }
580
- // 5. Contact Sensor
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
- }
596
- // 6. Leak Sensor
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
- }
612
- // 7. Smoke Sensor
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
- }
166
+ const context = { api: this.api, log: this.log, config: this.config };
167
+ const accessories = [
168
+ ...registerOnOffLightSwitch(context),
169
+ ];
638
170
  if (accessories.length > 0) {
639
- this.log.info(`✓ Registered ${accessories.length} sensor accessories`);
171
+ this.log.info(`✓ Registered ${accessories.length} switch/controller device(s)`);
640
172
  for (const acc of accessories) {
641
173
  this.log.info(` - ${acc.displayName}`);
642
174
  }
643
- // Register all sensor accessories
644
175
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
645
176
  }
646
177
  }
647
178
  /**
648
- * Register Matter HVAC accessories (Thermostats, Fans)
179
+ * Section 7: Sensors (Matter Spec § 7)
649
180
  */
650
- registerHVAC() {
181
+ registerSection7Sensors() {
651
182
  this.log.info('═'.repeat(80));
652
- this.log.info('Registering Matter HVAC Devices');
183
+ this.log.info('Section 7: Sensors (Matter Spec § 7)');
653
184
  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
- }
185
+ const context = { api: this.api, log: this.log, config: this.config };
186
+ const accessories = [
187
+ ...registerContactSensor(context),
188
+ ...registerLightSensor(context),
189
+ ...registerOccupancySensor(context),
190
+ ...registerTemperatureSensor(context),
191
+ ...registerHumiditySensor(context),
192
+ ...registerSmokeCoAlarm(context),
193
+ ...registerWaterLeakDetector(context),
194
+ ];
750
195
  if (accessories.length > 0) {
751
- this.log.info(`✓ Registered ${accessories.length} HVAC accessories`);
196
+ this.log.info(`✓ Registered ${accessories.length} sensor device(s)`);
752
197
  for (const acc of accessories) {
753
198
  this.log.info(` - ${acc.displayName}`);
754
199
  }
755
- // Register all HVAC accessories
756
200
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
757
201
  }
758
202
  }
759
203
  /**
760
- * Register Matter Security & Access accessories (Door Locks, Garage Doors)
204
+ * Section 8: Closure Devices (Matter Spec § 8)
761
205
  */
762
- registerSecurity() {
206
+ registerSection8Closure() {
763
207
  this.log.info('═'.repeat(80));
764
- this.log.info('Registering Matter Security & Access Devices');
208
+ this.log.info('Section 8: Closure Devices (Matter Spec § 8)');
765
209
  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
- }
210
+ const context = { api: this.api, log: this.log, config: this.config };
211
+ const accessories = [
212
+ ...registerDoorLock(context),
213
+ ...registerWindowCovering(context),
214
+ ];
876
215
  if (accessories.length > 0) {
877
- this.log.info(`✓ Registered ${accessories.length} security & access accessories`);
216
+ this.log.info(`✓ Registered ${accessories.length} closure device(s)`);
878
217
  for (const acc of accessories) {
879
218
  this.log.info(` - ${acc.displayName}`);
880
219
  }
881
- // Register all security accessories
882
220
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
883
221
  }
884
222
  }
885
223
  /**
886
- * Register Matter Window Covering accessories (Blinds, Shades)
224
+ * Section 9: HVAC (Matter Spec § 9)
887
225
  */
888
- registerWindowCoverings() {
226
+ registerSection9HVAC() {
889
227
  this.log.info('═'.repeat(80));
890
- this.log.info('Registering Matter Window Covering Devices');
228
+ this.log.info('Section 9: HVAC (Matter Spec § 9)');
891
229
  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
- }
230
+ const context = { api: this.api, log: this.log, config: this.config };
231
+ const accessories = [
232
+ ...registerThermostat(context),
233
+ ...registerFan(context),
234
+ ];
1034
235
  if (accessories.length > 0) {
1035
- this.log.info(`✓ Registered ${accessories.length} window covering accessories`);
236
+ this.log.info(`✓ Registered ${accessories.length} HVAC device(s)`);
1036
237
  for (const acc of accessories) {
1037
238
  this.log.info(` - ${acc.displayName}`);
1038
239
  }
1039
- // Register all window covering accessories
1040
240
  this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
1041
241
  }
1042
242
  }
1043
243
  /**
1044
- * Register Matter Appliance accessories (Robotic Vacuum Cleaners, etc.)
244
+ * Section 12: Robotic Devices (Matter Spec § 12)
245
+ *
246
+ * ⚠️ IMPORTANT: RVC devices are published as external accessories
247
+ * Apple Home requires RVC devices to be on their own dedicated Matter bridge.
248
+ * Using publishExternalAccessories ensures each RVC device gets its own bridge.
1045
249
  */
1046
- registerAppliances() {
250
+ registerSection12Robotic() {
1047
251
  this.log.info('═'.repeat(80));
1048
- this.log.info('Registering Matter Appliance Devices');
252
+ this.log.info('Section 12: Robotic Devices (Matter Spec § 12)');
1049
253
  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
- }
254
+ const context = { api: this.api, log: this.log, config: this.config };
255
+ const accessories = [
256
+ ...registerRoboticVacuumCleaner(context),
257
+ ];
1149
258
  if (accessories.length > 0) {
1150
- this.log.info(`✓ Registered ${accessories.length} appliance accessories`);
259
+ this.log.info(`✓ Publishing ${accessories.length} robotic device(s) as external accessories`);
1151
260
  for (const acc of accessories) {
1152
- this.log.info(` - ${acc.displayName}`);
261
+ this.log.info(` - ${acc.displayName} (dedicated bridge for Apple Home compatibility)`);
1153
262
  }
1154
- // Register all appliance accessories
1155
- this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
263
+ // Use publishExternalAccessories to give each RVC device its own bridge
264
+ // This is required for Apple Home compatibility
265
+ this.api.matter.publishExternalAccessories(PLUGIN_NAME, accessories);
1156
266
  }
1157
267
  }
1158
268
  }