@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.
- package/CHANGELOG.md +11 -0
- package/README.md +67 -33
- package/config.schema.json +73 -77
- package/dist/devices/index.d.ts +13 -0
- package/dist/devices/index.js +22 -0
- package/dist/devices/index.js.map +1 -0
- package/dist/devices/section-12-robotic/index.d.ts +6 -0
- package/dist/devices/section-12-robotic/index.js +7 -0
- package/dist/devices/section-12-robotic/index.js.map +1 -0
- package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.d.ts +63 -0
- package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.js +318 -0
- package/dist/devices/section-12-robotic/robotic-vacuum-cleaner.js.map +1 -0
- package/dist/devices/section-4-lighting/color-temperature-light.d.ts +7 -0
- package/dist/devices/section-4-lighting/color-temperature-light.js +62 -0
- package/dist/devices/section-4-lighting/color-temperature-light.js.map +1 -0
- package/dist/devices/section-4-lighting/dimmable-light.d.ts +7 -0
- package/dist/devices/section-4-lighting/dimmable-light.js +48 -0
- package/dist/devices/section-4-lighting/dimmable-light.js.map +1 -0
- package/dist/devices/section-4-lighting/extended-color-light.d.ts +12 -0
- package/dist/devices/section-4-lighting/extended-color-light.js +142 -0
- package/dist/devices/section-4-lighting/extended-color-light.js.map +1 -0
- package/dist/devices/section-4-lighting/index.d.ts +9 -0
- package/dist/devices/section-4-lighting/index.js +10 -0
- package/dist/devices/section-4-lighting/index.js.map +1 -0
- package/dist/devices/section-4-lighting/on-off-light.d.ts +7 -0
- package/dist/devices/section-4-lighting/on-off-light.js +37 -0
- package/dist/devices/section-4-lighting/on-off-light.js.map +1 -0
- package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.d.ts +7 -0
- package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.js +48 -0
- package/dist/devices/section-5-smart-plugs/dimmable-plug-in-unit.js.map +1 -0
- package/dist/devices/section-5-smart-plugs/index.d.ts +7 -0
- package/dist/devices/section-5-smart-plugs/index.js +8 -0
- package/dist/devices/section-5-smart-plugs/index.js.map +1 -0
- package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.d.ts +7 -0
- package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.js +37 -0
- package/dist/devices/section-5-smart-plugs/on-off-plug-in-unit.js.map +1 -0
- package/dist/devices/section-6-switches/index.d.ts +6 -0
- package/dist/devices/section-6-switches/index.js +7 -0
- package/dist/devices/section-6-switches/index.js.map +1 -0
- package/dist/devices/section-6-switches/on-off-light-switch.d.ts +7 -0
- package/dist/devices/section-6-switches/on-off-light-switch.js +30 -0
- package/dist/devices/section-6-switches/on-off-light-switch.js.map +1 -0
- package/dist/devices/section-7-sensors/contact-sensor.d.ts +7 -0
- package/dist/devices/section-7-sensors/contact-sensor.js +27 -0
- package/dist/devices/section-7-sensors/contact-sensor.js.map +1 -0
- package/dist/devices/section-7-sensors/humidity-sensor.d.ts +7 -0
- package/dist/devices/section-7-sensors/humidity-sensor.js +29 -0
- package/dist/devices/section-7-sensors/humidity-sensor.js.map +1 -0
- package/dist/devices/section-7-sensors/index.d.ts +12 -0
- package/dist/devices/section-7-sensors/index.js +13 -0
- package/dist/devices/section-7-sensors/index.js.map +1 -0
- package/dist/devices/section-7-sensors/light-sensor.d.ts +7 -0
- package/dist/devices/section-7-sensors/light-sensor.js +29 -0
- package/dist/devices/section-7-sensors/light-sensor.js.map +1 -0
- package/dist/devices/section-7-sensors/occupancy-sensor.d.ts +8 -0
- package/dist/devices/section-7-sensors/occupancy-sensor.js +33 -0
- package/dist/devices/section-7-sensors/occupancy-sensor.js.map +1 -0
- package/dist/devices/section-7-sensors/smoke-co-alarm.d.ts +7 -0
- package/dist/devices/section-7-sensors/smoke-co-alarm.js +37 -0
- package/dist/devices/section-7-sensors/smoke-co-alarm.js.map +1 -0
- package/dist/devices/section-7-sensors/temperature-sensor.d.ts +7 -0
- package/dist/devices/section-7-sensors/temperature-sensor.js +29 -0
- package/dist/devices/section-7-sensors/temperature-sensor.js.map +1 -0
- package/dist/devices/section-7-sensors/water-leak-detector.d.ts +7 -0
- package/dist/devices/section-7-sensors/water-leak-detector.js +27 -0
- package/dist/devices/section-7-sensors/water-leak-detector.js.map +1 -0
- package/dist/devices/section-8-closure/door-lock.d.ts +7 -0
- package/dist/devices/section-8-closure/door-lock.js +48 -0
- package/dist/devices/section-8-closure/door-lock.js.map +1 -0
- package/dist/devices/section-8-closure/index.d.ts +7 -0
- package/dist/devices/section-8-closure/index.js +8 -0
- package/dist/devices/section-8-closure/index.js.map +1 -0
- package/dist/devices/section-8-closure/window-covering.d.ts +9 -0
- package/dist/devices/section-8-closure/window-covering.js +154 -0
- package/dist/devices/section-8-closure/window-covering.js.map +1 -0
- package/dist/devices/section-9-hvac/fan.d.ts +7 -0
- package/dist/devices/section-9-hvac/fan.js +56 -0
- package/dist/devices/section-9-hvac/fan.js.map +1 -0
- package/dist/devices/section-9-hvac/index.d.ts +7 -0
- package/dist/devices/section-9-hvac/index.js +8 -0
- package/dist/devices/section-9-hvac/index.js.map +1 -0
- package/dist/devices/section-9-hvac/thermostat.d.ts +7 -0
- package/dist/devices/section-9-hvac/thermostat.js +61 -0
- package/dist/devices/section-9-hvac/thermostat.js.map +1 -0
- package/dist/devices/types.d.ts +16 -0
- package/dist/devices/types.js +5 -0
- package/dist/devices/types.js.map +1 -0
- package/dist/homebridge-ui/public/index.html +269 -0
- package/dist/homebridge-ui/server.js +47 -0
- package/dist/platform.d.ts +25 -21
- package/dist/platform.js +133 -1023
- package/dist/platform.js.map +1 -1
- package/package.json +9 -9
- package/plugin-header.png +0 -0
- 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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
59
|
+
* Register all Matter accessories
|
|
49
60
|
*/
|
|
50
61
|
registerMatterAccessories() {
|
|
51
|
-
this.log.info('
|
|
52
|
-
this.log.info('
|
|
53
|
-
this.log.info('
|
|
54
|
-
|
|
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
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
*
|
|
140
|
+
* Section 5: Smart Plugs/Actuators (Matter Spec § 5)
|
|
397
141
|
*/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
*
|
|
160
|
+
* Section 6: Switches & Controllers (Matter Spec § 6)
|
|
499
161
|
*/
|
|
500
|
-
|
|
162
|
+
registerSection6Switches() {
|
|
501
163
|
this.log.info('═'.repeat(80));
|
|
502
|
-
this.log.info('
|
|
164
|
+
this.log.info('Section 6: Switches & Controllers (Matter Spec § 6)');
|
|
503
165
|
this.log.info('═'.repeat(80));
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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}
|
|
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
|
-
*
|
|
179
|
+
* Section 7: Sensors (Matter Spec § 7)
|
|
649
180
|
*/
|
|
650
|
-
|
|
181
|
+
registerSection7Sensors() {
|
|
651
182
|
this.log.info('═'.repeat(80));
|
|
652
|
-
this.log.info('
|
|
183
|
+
this.log.info('Section 7: Sensors (Matter Spec § 7)');
|
|
653
184
|
this.log.info('═'.repeat(80));
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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}
|
|
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
|
-
*
|
|
204
|
+
* Section 8: Closure Devices (Matter Spec § 8)
|
|
761
205
|
*/
|
|
762
|
-
|
|
206
|
+
registerSection8Closure() {
|
|
763
207
|
this.log.info('═'.repeat(80));
|
|
764
|
-
this.log.info('
|
|
208
|
+
this.log.info('Section 8: Closure Devices (Matter Spec § 8)');
|
|
765
209
|
this.log.info('═'.repeat(80));
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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}
|
|
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
|
-
*
|
|
224
|
+
* Section 9: HVAC (Matter Spec § 9)
|
|
887
225
|
*/
|
|
888
|
-
|
|
226
|
+
registerSection9HVAC() {
|
|
889
227
|
this.log.info('═'.repeat(80));
|
|
890
|
-
this.log.info('
|
|
228
|
+
this.log.info('Section 9: HVAC (Matter Spec § 9)');
|
|
891
229
|
this.log.info('═'.repeat(80));
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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}
|
|
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
|
-
*
|
|
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
|
-
|
|
250
|
+
registerSection12Robotic() {
|
|
1047
251
|
this.log.info('═'.repeat(80));
|
|
1048
|
-
this.log.info('
|
|
252
|
+
this.log.info('Section 12: Robotic Devices (Matter Spec § 12)');
|
|
1049
253
|
this.log.info('═'.repeat(80));
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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(`✓
|
|
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
|
-
//
|
|
1155
|
-
|
|
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
|
}
|