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