homebridge 2.0.0-alpha.4 → 2.0.0-alpha.41
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/README.md +1 -1
- package/bin/homebridge.js +22 -0
- package/dist/api.d.ts +210 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +140 -0
- package/dist/api.js.map +1 -0
- package/dist/bridgeService.d.ts +122 -0
- package/dist/bridgeService.d.ts.map +1 -0
- package/dist/bridgeService.js +396 -0
- package/dist/bridgeService.js.map +1 -0
- package/dist/childBridgeFork.d.ts +38 -0
- package/dist/childBridgeFork.d.ts.map +1 -0
- package/dist/childBridgeFork.js +241 -2
- package/dist/childBridgeFork.js.map +1 -7
- package/dist/childBridgeService.d.ts +204 -0
- package/dist/childBridgeService.d.ts.map +1 -0
- package/dist/childBridgeService.js +452 -0
- package/dist/childBridgeService.js.map +1 -0
- package/dist/childMatterBridgeFork.d.ts +108 -0
- package/dist/childMatterBridgeFork.d.ts.map +1 -0
- package/dist/childMatterBridgeFork.js +330 -0
- package/dist/childMatterBridgeFork.js.map +1 -0
- package/dist/childMatterBridgeService.d.ts +166 -0
- package/dist/childMatterBridgeService.d.ts.map +1 -0
- package/dist/childMatterBridgeService.js +623 -0
- package/dist/childMatterBridgeService.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +90 -2
- package/dist/cli.js.map +1 -7
- package/dist/externalPortService.d.ts +33 -0
- package/dist/externalPortService.d.ts.map +1 -0
- package/dist/externalPortService.js +59 -0
- package/dist/externalPortService.js.map +1 -0
- package/dist/index.d.ts +90 -1099
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -7
- package/dist/ipcService.d.ts +46 -0
- package/dist/ipcService.d.ts.map +1 -0
- package/dist/ipcService.js +62 -0
- package/dist/ipcService.js.map +1 -0
- package/dist/logger.d.ts +78 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +138 -0
- package/dist/logger.js.map +1 -0
- package/dist/matter/index.d.ts +13 -0
- package/dist/matter/index.d.ts.map +1 -0
- package/dist/matter/index.js +12 -0
- package/dist/matter/index.js.map +1 -0
- package/dist/matter/matterBridge.d.ts +64 -0
- package/dist/matter/matterBridge.d.ts.map +1 -0
- package/dist/matter/matterBridge.js +154 -0
- package/dist/matter/matterBridge.js.map +1 -0
- package/dist/matter/matterConfigValidator.d.ts +27 -0
- package/dist/matter/matterConfigValidator.d.ts.map +1 -0
- package/dist/matter/matterConfigValidator.js +162 -0
- package/dist/matter/matterConfigValidator.js.map +1 -0
- package/dist/matter/matterDevice.d.ts +107 -0
- package/dist/matter/matterDevice.d.ts.map +1 -0
- package/dist/matter/matterDevice.js +913 -0
- package/dist/matter/matterDevice.js.map +1 -0
- package/dist/matter/matterDiagnostics.d.ts +121 -0
- package/dist/matter/matterDiagnostics.d.ts.map +1 -0
- package/dist/matter/matterDiagnostics.js +323 -0
- package/dist/matter/matterDiagnostics.js.map +1 -0
- package/dist/matter/matterErrorHandler.d.ts +113 -0
- package/dist/matter/matterErrorHandler.d.ts.map +1 -0
- package/dist/matter/matterErrorHandler.js +482 -0
- package/dist/matter/matterErrorHandler.js.map +1 -0
- package/dist/matter/matterNetworkMonitor.d.ts +65 -0
- package/dist/matter/matterNetworkMonitor.d.ts.map +1 -0
- package/dist/matter/matterNetworkMonitor.js +227 -0
- package/dist/matter/matterNetworkMonitor.js.map +1 -0
- package/dist/matter/matterServer.d.ts +110 -0
- package/dist/matter/matterServer.d.ts.map +1 -0
- package/dist/matter/matterServer.js +584 -0
- package/dist/matter/matterServer.js.map +1 -0
- package/dist/matter/matterSharedTypes.d.ts +167 -0
- package/dist/matter/matterSharedTypes.d.ts.map +1 -0
- package/dist/matter/matterSharedTypes.js +55 -0
- package/dist/matter/matterSharedTypes.js.map +1 -0
- package/dist/matter/matterTypes.d.ts +35 -0
- package/dist/matter/matterTypes.d.ts.map +1 -0
- package/dist/matter/matterTypes.js +278 -0
- package/dist/matter/matterTypes.js.map +1 -0
- package/dist/matter/portAllocator.d.ts +85 -0
- package/dist/matter/portAllocator.d.ts.map +1 -0
- package/dist/matter/portAllocator.js +296 -0
- package/dist/matter/portAllocator.js.map +1 -0
- package/dist/platformAccessory.d.ts +56 -0
- package/dist/platformAccessory.d.ts.map +1 -0
- package/dist/platformAccessory.js +105 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +182 -0
- package/dist/plugin.js.map +1 -0
- package/dist/pluginManager.d.ts +77 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +375 -0
- package/dist/pluginManager.js.map +1 -0
- package/dist/server.d.ts +67 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +799 -0
- package/dist/server.js.map +1 -0
- package/dist/storageService.d.ts +13 -0
- package/dist/storageService.d.ts.map +1 -0
- package/dist/storageService.js +41 -0
- package/dist/storageService.js.map +1 -0
- package/dist/user.d.ts +13 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +29 -0
- package/dist/user.js.map +1 -0
- package/dist/util/mac.d.ts +5 -0
- package/dist/util/mac.d.ts.map +1 -0
- package/dist/util/mac.js +14 -0
- package/dist/util/mac.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/package.json +31 -34
- package/bin/homebridge +0 -19
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matter Device Implementation
|
|
3
|
+
*
|
|
4
|
+
* Handles creation of individual Matter devices from HAP accessories
|
|
5
|
+
* with proper Matter.js integration and type safety
|
|
6
|
+
*/
|
|
7
|
+
import { Endpoint } from '@matter/main';
|
|
8
|
+
import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors';
|
|
9
|
+
import { ContactSensorDevice, DimmableLightDevice, DimmablePlugInUnitDevice, GenericSwitchDevice, HumiditySensorDevice, LightSensorDevice, OccupancySensorDevice, OnOffLightDevice, OnOffLightSwitchDevice, OnOffPlugInUnitDevice, OnOffSensorDevice, PressureSensorDevice, PumpDevice, RoomAirConditionerDevice, SmokeCoAlarmDevice, TemperatureSensorDevice, ThermostatDevice, WaterLeakDetectorDevice, WindowCoveringDevice, } from '@matter/main/devices';
|
|
10
|
+
import { Logger } from '../logger.js';
|
|
11
|
+
const log = Logger.withPrefix('Matter');
|
|
12
|
+
/**
|
|
13
|
+
* Type-safe wrapper for Matter endpoint with proper state handling
|
|
14
|
+
*/
|
|
15
|
+
class TypedMatterEndpoint {
|
|
16
|
+
endpoint;
|
|
17
|
+
stateProxy;
|
|
18
|
+
constructor(endpoint, stateProxy = {}) {
|
|
19
|
+
this.endpoint = endpoint;
|
|
20
|
+
this.stateProxy = stateProxy;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get typed state for a cluster
|
|
24
|
+
*/
|
|
25
|
+
getClusterState(cluster) {
|
|
26
|
+
return this.stateProxy[cluster];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Update cluster state with type safety
|
|
30
|
+
*/
|
|
31
|
+
async updateClusterState(cluster, updates) {
|
|
32
|
+
if (!this.stateProxy[cluster]) {
|
|
33
|
+
this.stateProxy[cluster] = {};
|
|
34
|
+
}
|
|
35
|
+
Object.assign(this.stateProxy[cluster], updates);
|
|
36
|
+
// Trigger Matter.js state update
|
|
37
|
+
// This would normally interact with the endpoint's behaviors
|
|
38
|
+
// For now, we'll log the update
|
|
39
|
+
log.debug(`Updated ${String(cluster)} state:`, updates);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the raw endpoint
|
|
43
|
+
*/
|
|
44
|
+
getRawEndpoint() {
|
|
45
|
+
return this.endpoint;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Represents a single Matter device created from a HAP accessory
|
|
50
|
+
*/
|
|
51
|
+
export class MatterDevice {
|
|
52
|
+
accessory;
|
|
53
|
+
endpoint = null;
|
|
54
|
+
typedEndpoint = null;
|
|
55
|
+
deviceType = null;
|
|
56
|
+
primaryService = null;
|
|
57
|
+
characteristicSubscriptions = new Map();
|
|
58
|
+
cleanupCallbacks = [];
|
|
59
|
+
isDestroyed = false;
|
|
60
|
+
constructor(accessory) {
|
|
61
|
+
this.accessory = accessory;
|
|
62
|
+
// Identify primary service
|
|
63
|
+
this.identifyPrimaryService();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the HAP accessory
|
|
67
|
+
*/
|
|
68
|
+
getAccessory() {
|
|
69
|
+
return this.accessory;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Identify the primary service from the accessory
|
|
73
|
+
*/
|
|
74
|
+
identifyPrimaryService() {
|
|
75
|
+
const services = this.accessory.services || [];
|
|
76
|
+
// Find the primary service (not AccessoryInformation)
|
|
77
|
+
for (const service of services) {
|
|
78
|
+
const serviceName = service.constructor.name;
|
|
79
|
+
if (serviceName !== 'AccessoryInformation' && serviceName !== 'ProtocolInformation') {
|
|
80
|
+
this.primaryService = service;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!this.primaryService && services.length > 0) {
|
|
85
|
+
// Fallback to first service if no primary found
|
|
86
|
+
this.primaryService = services[0];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Determine the Matter device type based on HAP service
|
|
91
|
+
*/
|
|
92
|
+
getMatterDeviceType() {
|
|
93
|
+
if (!this.primaryService) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const serviceName = this.primaryService.constructor.name;
|
|
97
|
+
log.debug(`Mapping HAP service '${serviceName}' for accessory '${this.accessory.displayName}'`);
|
|
98
|
+
const hasCharacteristic = (name) => this.primaryService.characteristics.some(c => c.constructor.name === name);
|
|
99
|
+
// Enhanced HAP to Matter device type mapping
|
|
100
|
+
let deviceTypeName;
|
|
101
|
+
let endpointType;
|
|
102
|
+
switch (serviceName) {
|
|
103
|
+
case 'Lightbulb':
|
|
104
|
+
// Simplify to avoid color control initialization issues
|
|
105
|
+
if (hasCharacteristic('Brightness')) {
|
|
106
|
+
deviceTypeName = 'DimmableLight';
|
|
107
|
+
endpointType = DimmableLightDevice.with(BridgedDeviceBasicInformationServer);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
deviceTypeName = 'OnOffLight';
|
|
111
|
+
endpointType = OnOffLightDevice.with(BridgedDeviceBasicInformationServer);
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case 'Outlet':
|
|
115
|
+
if (hasCharacteristic('Brightness')) {
|
|
116
|
+
deviceTypeName = 'DimmablePlugInUnit';
|
|
117
|
+
endpointType = DimmablePlugInUnitDevice.with(BridgedDeviceBasicInformationServer);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
deviceTypeName = 'OnOffPlugInUnit';
|
|
121
|
+
endpointType = OnOffPlugInUnitDevice.with(BridgedDeviceBasicInformationServer);
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
case 'Switch':
|
|
125
|
+
if (hasCharacteristic('ProgrammableSwitchEvent')) {
|
|
126
|
+
deviceTypeName = 'GenericSwitch';
|
|
127
|
+
endpointType = GenericSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
128
|
+
}
|
|
129
|
+
else if (hasCharacteristic('Brightness')) {
|
|
130
|
+
// Note: DimmerSwitch not available in Matter.js v0.15, use light switch
|
|
131
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
132
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
136
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
case 'TemperatureSensor':
|
|
140
|
+
deviceTypeName = 'TemperatureSensor';
|
|
141
|
+
endpointType = TemperatureSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
142
|
+
break;
|
|
143
|
+
case 'HumiditySensor':
|
|
144
|
+
deviceTypeName = 'HumiditySensor';
|
|
145
|
+
endpointType = HumiditySensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
146
|
+
break;
|
|
147
|
+
case 'LightSensor':
|
|
148
|
+
deviceTypeName = 'LightSensor';
|
|
149
|
+
endpointType = LightSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
150
|
+
break;
|
|
151
|
+
case 'OccupancySensor':
|
|
152
|
+
deviceTypeName = 'OccupancySensor';
|
|
153
|
+
endpointType = OccupancySensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
154
|
+
break;
|
|
155
|
+
case 'ContactSensor':
|
|
156
|
+
deviceTypeName = 'ContactSensor';
|
|
157
|
+
endpointType = ContactSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
158
|
+
break;
|
|
159
|
+
case 'LockManagement':
|
|
160
|
+
case 'Lock':
|
|
161
|
+
case 'LockMgmt':
|
|
162
|
+
case 'LockMechanism':
|
|
163
|
+
// DoorLock has issues with actuatorEnabled, use simpler switch
|
|
164
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
165
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
166
|
+
break;
|
|
167
|
+
case 'WindowCovering':
|
|
168
|
+
case 'Window':
|
|
169
|
+
deviceTypeName = 'WindowCovering';
|
|
170
|
+
endpointType = WindowCoveringDevice.with(BridgedDeviceBasicInformationServer);
|
|
171
|
+
break;
|
|
172
|
+
case 'Thermostat':
|
|
173
|
+
deviceTypeName = 'Thermostat';
|
|
174
|
+
endpointType = ThermostatDevice.with(BridgedDeviceBasicInformationServer);
|
|
175
|
+
break;
|
|
176
|
+
case 'Fan':
|
|
177
|
+
case 'Fanv2':
|
|
178
|
+
// Fan has issues with fanModeSequence and percentCurrent, use switch
|
|
179
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
180
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
181
|
+
break;
|
|
182
|
+
case 'GarageDoorOpener':
|
|
183
|
+
case 'Door':
|
|
184
|
+
// DoorLock has issues with actuatorEnabled, use simpler switch
|
|
185
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
186
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
187
|
+
break;
|
|
188
|
+
case 'HeaterCooler':
|
|
189
|
+
deviceTypeName = 'RoomAirConditioner';
|
|
190
|
+
endpointType = RoomAirConditionerDevice.with(BridgedDeviceBasicInformationServer);
|
|
191
|
+
break;
|
|
192
|
+
case 'HumidifierDehumidifier':
|
|
193
|
+
// Fan has issues with fanModeSequence and percentCurrent, use switch
|
|
194
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
195
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
196
|
+
break;
|
|
197
|
+
case 'Valve':
|
|
198
|
+
case 'Faucet':
|
|
199
|
+
// WaterValve has issues with open/close commands, use switch
|
|
200
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
201
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
202
|
+
break;
|
|
203
|
+
case 'IrrigationSystem':
|
|
204
|
+
deviceTypeName = 'Pump';
|
|
205
|
+
endpointType = PumpDevice.with(BridgedDeviceBasicInformationServer);
|
|
206
|
+
break;
|
|
207
|
+
case 'LeakSensor':
|
|
208
|
+
deviceTypeName = 'WaterLeakDetector';
|
|
209
|
+
endpointType = WaterLeakDetectorDevice.with(BridgedDeviceBasicInformationServer);
|
|
210
|
+
break;
|
|
211
|
+
case 'MotionSensor':
|
|
212
|
+
deviceTypeName = 'OccupancySensor';
|
|
213
|
+
endpointType = OccupancySensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
214
|
+
break;
|
|
215
|
+
case 'SmokeSensor':
|
|
216
|
+
case 'CarbonMonoxideSensor':
|
|
217
|
+
case 'CarbonDioxideSensor':
|
|
218
|
+
deviceTypeName = 'SmokeCoAlarm';
|
|
219
|
+
endpointType = SmokeCoAlarmDevice.with(BridgedDeviceBasicInformationServer);
|
|
220
|
+
break;
|
|
221
|
+
case 'PressureSensor':
|
|
222
|
+
deviceTypeName = 'PressureSensor';
|
|
223
|
+
endpointType = PressureSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
224
|
+
break;
|
|
225
|
+
case 'SecuritySystem':
|
|
226
|
+
deviceTypeName = 'OnOffSensor';
|
|
227
|
+
endpointType = OnOffSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
228
|
+
break;
|
|
229
|
+
case 'StatelessProgrammableSwitch':
|
|
230
|
+
deviceTypeName = 'GenericSwitch';
|
|
231
|
+
endpointType = GenericSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
232
|
+
break;
|
|
233
|
+
case 'Doorbell':
|
|
234
|
+
deviceTypeName = 'GenericSwitch';
|
|
235
|
+
endpointType = GenericSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
236
|
+
break;
|
|
237
|
+
case 'Battery':
|
|
238
|
+
case 'BatteryService':
|
|
239
|
+
deviceTypeName = 'OnOffSensor';
|
|
240
|
+
endpointType = OnOffSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
241
|
+
break;
|
|
242
|
+
case 'Slat':
|
|
243
|
+
deviceTypeName = 'WindowCovering';
|
|
244
|
+
endpointType = WindowCoveringDevice.with(BridgedDeviceBasicInformationServer);
|
|
245
|
+
break;
|
|
246
|
+
// Television/Media accessories
|
|
247
|
+
case 'Television':
|
|
248
|
+
case 'TelevisionSpeaker':
|
|
249
|
+
case 'InputSource':
|
|
250
|
+
// Use basic switch for TV control
|
|
251
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
252
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
253
|
+
break;
|
|
254
|
+
// Speaker accessories
|
|
255
|
+
case 'Speaker':
|
|
256
|
+
case 'AirPlaySpeaker':
|
|
257
|
+
// Use basic switch for speaker control
|
|
258
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
259
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
260
|
+
break;
|
|
261
|
+
// Camera accessories
|
|
262
|
+
case 'Camera':
|
|
263
|
+
case 'CameraRTPStreamManagement':
|
|
264
|
+
case 'CameraRecordingManagement':
|
|
265
|
+
case 'CameraOperatingMode':
|
|
266
|
+
case 'CameraEventRecordingManagement':
|
|
267
|
+
// Cameras aren't directly supported in Matter, use sensor
|
|
268
|
+
deviceTypeName = 'OccupancySensor';
|
|
269
|
+
endpointType = OccupancySensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
270
|
+
break;
|
|
271
|
+
// Audio/Video accessories
|
|
272
|
+
case 'Microphone':
|
|
273
|
+
case 'AudioStreamManagement':
|
|
274
|
+
// Use basic switch for audio control
|
|
275
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
276
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
277
|
+
break;
|
|
278
|
+
// Service/Management types
|
|
279
|
+
case 'AccessoryInformation':
|
|
280
|
+
case 'AccessoryRuntimeInformation':
|
|
281
|
+
case 'ServiceLabel':
|
|
282
|
+
case 'ThreadTransport':
|
|
283
|
+
case 'WiFiTransport':
|
|
284
|
+
case 'PowerManagement':
|
|
285
|
+
case 'TransferTransportManagement':
|
|
286
|
+
case 'TimeInformation':
|
|
287
|
+
case 'FirmwareUpdate':
|
|
288
|
+
case 'DiagnosticsSnapshot':
|
|
289
|
+
// Skip service/management types - return null
|
|
290
|
+
log.debug(`Skipping service type: ${serviceName}`);
|
|
291
|
+
return null;
|
|
292
|
+
// Siri/Assistant
|
|
293
|
+
case 'Siri':
|
|
294
|
+
case 'SiriEndpoint':
|
|
295
|
+
case 'Assistant':
|
|
296
|
+
case 'TargetControlManagement':
|
|
297
|
+
case 'TargetControl':
|
|
298
|
+
// Use basic switch for assistant control
|
|
299
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
300
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
301
|
+
break;
|
|
302
|
+
// Additional sensors
|
|
303
|
+
case 'AirQualitySensor':
|
|
304
|
+
// Use temperature sensor as closest match
|
|
305
|
+
deviceTypeName = 'TemperatureSensor';
|
|
306
|
+
endpointType = TemperatureSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
307
|
+
break;
|
|
308
|
+
case 'FilterMaintenance':
|
|
309
|
+
case 'AirPurifier':
|
|
310
|
+
// Map to switch for air purifier control
|
|
311
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
312
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
313
|
+
break;
|
|
314
|
+
// Additional common types
|
|
315
|
+
case 'WiFiRouter':
|
|
316
|
+
case 'WiFiSatellite':
|
|
317
|
+
// Network devices - use basic switch
|
|
318
|
+
deviceTypeName = 'OnOffLightSwitch';
|
|
319
|
+
endpointType = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
|
|
320
|
+
break;
|
|
321
|
+
case 'PowerStrip':
|
|
322
|
+
// Multiple outlets - use outlet
|
|
323
|
+
deviceTypeName = 'OnOffPlugInUnit';
|
|
324
|
+
endpointType = OnOffPlugInUnitDevice.with(BridgedDeviceBasicInformationServer);
|
|
325
|
+
break;
|
|
326
|
+
case 'ChargerService':
|
|
327
|
+
// EV Charger or device charger
|
|
328
|
+
deviceTypeName = 'OnOffPlugInUnit';
|
|
329
|
+
endpointType = OnOffPlugInUnitDevice.with(BridgedDeviceBasicInformationServer);
|
|
330
|
+
break;
|
|
331
|
+
default:
|
|
332
|
+
// Log all available services for debugging
|
|
333
|
+
if (this.accessory.services && this.accessory.services.length > 0) {
|
|
334
|
+
const serviceTypes = this.accessory.services.map(s => s.constructor.name).join(', ');
|
|
335
|
+
log.warn(`Unknown HAP service type: ${serviceName} for accessory: ${this.accessory.displayName}. Available services: ${serviceTypes}`);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
log.warn(`Unknown HAP service type: ${serviceName} for accessory: ${this.accessory.displayName}`);
|
|
339
|
+
}
|
|
340
|
+
// Default to basic on/off sensor
|
|
341
|
+
deviceTypeName = 'OnOffSensor';
|
|
342
|
+
endpointType = OnOffSensorDevice.with(BridgedDeviceBasicInformationServer);
|
|
343
|
+
}
|
|
344
|
+
this.deviceType = deviceTypeName;
|
|
345
|
+
return { deviceType: deviceTypeName, endpointType };
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Create Matter endpoint from HAP accessory
|
|
349
|
+
*/
|
|
350
|
+
async createEndpoint() {
|
|
351
|
+
try {
|
|
352
|
+
// Identify primary service
|
|
353
|
+
this.primaryService = this.getPrimaryService();
|
|
354
|
+
if (!this.primaryService) {
|
|
355
|
+
log.warn(`No primary service found for: ${this.accessory.displayName}`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
// Determine device type based on service
|
|
359
|
+
const deviceMapping = this.getMatterDeviceType();
|
|
360
|
+
if (!deviceMapping) {
|
|
361
|
+
// Always log the service type for unsupported devices
|
|
362
|
+
const serviceName = this.primaryService.constructor.name;
|
|
363
|
+
log.warn(`Unsupported device type for: ${this.accessory.displayName} (Service: ${serviceName})`);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const { deviceType, endpointType } = deviceMapping;
|
|
367
|
+
this.deviceType = deviceType;
|
|
368
|
+
log.info(`Creating Matter endpoint: ${deviceType} for ${this.accessory.displayName}`);
|
|
369
|
+
// Create endpoint with proper configuration
|
|
370
|
+
this.endpoint = await this.createMatterEndpoint(endpointType);
|
|
371
|
+
if (!this.endpoint) {
|
|
372
|
+
log.error(`Failed to create Matter endpoint for: ${this.accessory.displayName}`);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
// Create typed endpoint wrapper for type-safe state management
|
|
376
|
+
this.typedEndpoint = new TypedMatterEndpoint(this.endpoint);
|
|
377
|
+
// Initialize with current HAP values
|
|
378
|
+
await this.initializeFromHAP();
|
|
379
|
+
log.debug(`✅ Created Matter endpoint: ${this.deviceType} for ${this.accessory.displayName}`);
|
|
380
|
+
return this.endpoint;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
log.error(`Failed to create endpoint for ${this.accessory.displayName}:`, error);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Parse version string to number (major version only)
|
|
389
|
+
*/
|
|
390
|
+
parseVersion(versionString) {
|
|
391
|
+
try {
|
|
392
|
+
const parts = versionString.split('.');
|
|
393
|
+
const major = Number.parseInt(parts[0], 10);
|
|
394
|
+
return Number.isNaN(major) ? 1 : Math.min(major, 65535); // Limit to UINT16
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return 1;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get AccessoryInformation values from HAP accessory
|
|
402
|
+
*/
|
|
403
|
+
getAccessoryInformation() {
|
|
404
|
+
// Try to get AccessoryInformation service from the accessory
|
|
405
|
+
const infoService = this.accessory.services?.find(service => service.constructor.name === 'AccessoryInformation');
|
|
406
|
+
// Default values matching Homebridge HAP bridge defaults
|
|
407
|
+
let manufacturer = 'homebridge.io';
|
|
408
|
+
let model = 'homebridge';
|
|
409
|
+
let serialNumber = this.accessory.UUID.substring(0, 32);
|
|
410
|
+
let firmwareRevision = '2.0.0';
|
|
411
|
+
let hardwareRevision = '1.0.0';
|
|
412
|
+
if (infoService) {
|
|
413
|
+
// Get Manufacturer characteristic
|
|
414
|
+
const manufacturerChar = infoService.characteristics?.find(c => c.constructor.name === 'Manufacturer');
|
|
415
|
+
if (manufacturerChar?.value !== null && manufacturerChar?.value !== undefined) {
|
|
416
|
+
manufacturer = String(manufacturerChar.value) || manufacturer;
|
|
417
|
+
}
|
|
418
|
+
// Get Model characteristic
|
|
419
|
+
const modelChar = infoService.characteristics?.find(c => c.constructor.name === 'Model');
|
|
420
|
+
if (modelChar?.value !== null && modelChar?.value !== undefined) {
|
|
421
|
+
model = String(modelChar.value) || model;
|
|
422
|
+
}
|
|
423
|
+
// Get SerialNumber characteristic
|
|
424
|
+
const serialChar = infoService.characteristics?.find(c => c.constructor.name === 'SerialNumber');
|
|
425
|
+
if (serialChar?.value !== null && serialChar?.value !== undefined) {
|
|
426
|
+
const serialValue = String(serialChar.value);
|
|
427
|
+
serialNumber = serialValue ? serialValue.substring(0, 32) : serialNumber;
|
|
428
|
+
}
|
|
429
|
+
// Get FirmwareRevision characteristic
|
|
430
|
+
const firmwareChar = infoService.characteristics?.find(c => c.constructor.name === 'FirmwareRevision');
|
|
431
|
+
if (firmwareChar?.value !== null && firmwareChar?.value !== undefined) {
|
|
432
|
+
firmwareRevision = String(firmwareChar.value) || firmwareRevision;
|
|
433
|
+
}
|
|
434
|
+
// Get HardwareRevision characteristic
|
|
435
|
+
const hardwareChar = infoService.characteristics?.find(c => c.constructor.name === 'HardwareRevision');
|
|
436
|
+
if (hardwareChar?.value !== null && hardwareChar?.value !== undefined) {
|
|
437
|
+
hardwareRevision = String(hardwareChar.value) || hardwareRevision;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
manufacturer,
|
|
442
|
+
model,
|
|
443
|
+
serialNumber,
|
|
444
|
+
firmwareRevision,
|
|
445
|
+
hardwareRevision,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Create the specific Matter endpoint with proper type
|
|
450
|
+
*/
|
|
451
|
+
async createMatterEndpoint(endpointType) {
|
|
452
|
+
// Generate unique identifiers
|
|
453
|
+
const uuidClean = this.accessory.UUID.replace(/-/g, '');
|
|
454
|
+
const uniqueId = uuidClean.padEnd(32, '0').substring(0, 32);
|
|
455
|
+
const deviceName = this.accessory.displayName;
|
|
456
|
+
// Get AccessoryInformation from HAP accessory
|
|
457
|
+
const accessoryInfo = this.getAccessoryInformation();
|
|
458
|
+
log.debug(`AccessoryInformation for ${deviceName}:`, {
|
|
459
|
+
manufacturer: accessoryInfo.manufacturer,
|
|
460
|
+
model: accessoryInfo.model,
|
|
461
|
+
serialNumber: accessoryInfo.serialNumber,
|
|
462
|
+
firmware: accessoryInfo.firmwareRevision,
|
|
463
|
+
hardware: accessoryInfo.hardwareRevision,
|
|
464
|
+
});
|
|
465
|
+
// Create bridged device configuration using HAP AccessoryInformation values
|
|
466
|
+
// IMPORTANT: Both nodeLabel and productLabel must be set to the device name for it to appear correctly in Home app
|
|
467
|
+
const bridgedDeviceConfig = {
|
|
468
|
+
nodeLabel: deviceName.slice(0, 32), // Maximum 32 characters for nodeLabel
|
|
469
|
+
productLabel: deviceName.slice(0, 64), // Maximum 64 characters for productLabel
|
|
470
|
+
productName: accessoryInfo.model.slice(0, 32), // Model from HAP AccessoryInformation
|
|
471
|
+
serialNumber: accessoryInfo.serialNumber.slice(0, 32), // SerialNumber from HAP
|
|
472
|
+
uniqueId,
|
|
473
|
+
vendorName: accessoryInfo.manufacturer.slice(0, 32), // Manufacturer from HAP
|
|
474
|
+
vendorId: 0xFFF1, // Homebridge vendor ID
|
|
475
|
+
productId: 0x8001,
|
|
476
|
+
reachable: true,
|
|
477
|
+
softwareVersion: this.parseVersion(accessoryInfo.firmwareRevision),
|
|
478
|
+
softwareVersionString: accessoryInfo.firmwareRevision.slice(0, 64),
|
|
479
|
+
hardwareVersion: this.parseVersion(accessoryInfo.hardwareRevision),
|
|
480
|
+
hardwareVersionString: accessoryInfo.hardwareRevision.slice(0, 64),
|
|
481
|
+
};
|
|
482
|
+
try {
|
|
483
|
+
// Create endpoint configuration with initial state based on device type
|
|
484
|
+
const endpointConfig = {
|
|
485
|
+
id: `endpoint-${this.accessory.UUID}`,
|
|
486
|
+
bridgedDeviceBasicInformation: bridgedDeviceConfig,
|
|
487
|
+
};
|
|
488
|
+
// Set initial state based on device type
|
|
489
|
+
// This maps HAP characteristics to Matter cluster attributes
|
|
490
|
+
await this.setInitialEndpointState(endpointConfig);
|
|
491
|
+
// Create the endpoint with BridgedDeviceBasicInformation
|
|
492
|
+
// The endpoint needs the bridged device information for proper naming
|
|
493
|
+
const endpoint = new Endpoint(endpointType, endpointConfig);
|
|
494
|
+
return endpoint;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
log.error(`Failed to create Matter device: ${error}`);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Set initial endpoint state based on HAP values
|
|
503
|
+
*/
|
|
504
|
+
async setInitialEndpointState(config) {
|
|
505
|
+
// Map common characteristics
|
|
506
|
+
switch (this.deviceType) {
|
|
507
|
+
case 'OnOffLight':
|
|
508
|
+
case 'OnOffPlugInUnit':
|
|
509
|
+
case 'OnOffLightSwitch': {
|
|
510
|
+
config.onOff = {
|
|
511
|
+
onOff: this.getHAPValue('On') || false,
|
|
512
|
+
};
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
case 'DimmableLight':
|
|
516
|
+
case 'DimmablePlugInUnit': {
|
|
517
|
+
config.onOff = {
|
|
518
|
+
onOff: this.getHAPValue('On') || false,
|
|
519
|
+
};
|
|
520
|
+
config.levelControl = {
|
|
521
|
+
currentLevel: Math.round((this.getHAPValue('Brightness') || 0) * 2.54),
|
|
522
|
+
minLevel: 1,
|
|
523
|
+
maxLevel: 254,
|
|
524
|
+
};
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
case 'ColorTemperatureLight': {
|
|
528
|
+
config.onOff = {
|
|
529
|
+
onOff: this.getHAPValue('On') || false,
|
|
530
|
+
};
|
|
531
|
+
config.levelControl = {
|
|
532
|
+
currentLevel: Math.round((this.getHAPValue('Brightness') || 0) * 2.54),
|
|
533
|
+
minLevel: 1,
|
|
534
|
+
maxLevel: 254,
|
|
535
|
+
};
|
|
536
|
+
const colorTemp = this.getHAPValue('ColorTemperature') || 370;
|
|
537
|
+
const mireds = Math.round(1000000 / colorTemp);
|
|
538
|
+
config.colorControl = {
|
|
539
|
+
colorMode: 2,
|
|
540
|
+
colorTemperatureMireds: Math.max(153, Math.min(500, mireds)),
|
|
541
|
+
colorTempPhysicalMinMireds: 153,
|
|
542
|
+
colorTempPhysicalMaxMireds: 500,
|
|
543
|
+
};
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
case 'ExtendedColorLight': {
|
|
547
|
+
config.onOff = {
|
|
548
|
+
onOff: this.getHAPValue('On') || false,
|
|
549
|
+
};
|
|
550
|
+
config.levelControl = {
|
|
551
|
+
currentLevel: Math.round((this.getHAPValue('Brightness') || 0) * 2.54),
|
|
552
|
+
minLevel: 1,
|
|
553
|
+
maxLevel: 254,
|
|
554
|
+
};
|
|
555
|
+
config.colorControl = {
|
|
556
|
+
colorMode: 0,
|
|
557
|
+
currentHue: Math.round((this.getHAPValue('Hue') || 0) * 254 / 360),
|
|
558
|
+
currentSaturation: Math.round((this.getHAPValue('Saturation') || 0) * 2.54),
|
|
559
|
+
colorTemperatureMireds: 370,
|
|
560
|
+
colorTempPhysicalMinMireds: 153,
|
|
561
|
+
colorTempPhysicalMaxMireds: 500,
|
|
562
|
+
};
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case 'TemperatureSensor': {
|
|
566
|
+
config.temperatureMeasurement = {
|
|
567
|
+
measuredValue: Math.round((this.getHAPValue('CurrentTemperature') || 20) * 100),
|
|
568
|
+
minMeasuredValue: -27315,
|
|
569
|
+
maxMeasuredValue: 32767,
|
|
570
|
+
};
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
case 'HumiditySensor': {
|
|
574
|
+
config.relativeHumidityMeasurement = {
|
|
575
|
+
measuredValue: Math.round((this.getHAPValue('CurrentRelativeHumidity') || 50) * 100),
|
|
576
|
+
minMeasuredValue: 0,
|
|
577
|
+
maxMeasuredValue: 10000,
|
|
578
|
+
};
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case 'LightSensor': {
|
|
582
|
+
const lux = this.getHAPValue('CurrentAmbientLightLevel') || 100;
|
|
583
|
+
config.illuminanceMeasurement = {
|
|
584
|
+
measuredValue: Math.round(Math.log10(Math.max(1, lux)) * 10000),
|
|
585
|
+
minMeasuredValue: 0,
|
|
586
|
+
maxMeasuredValue: 65534,
|
|
587
|
+
};
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'OccupancySensor': {
|
|
591
|
+
config.occupancySensing = {
|
|
592
|
+
occupancy: {
|
|
593
|
+
occupied: Boolean(this.getHAPValue('OccupancyDetected') || this.getHAPValue('MotionDetected')),
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
case 'ContactSensor': {
|
|
599
|
+
config.booleanState = {
|
|
600
|
+
stateValue: (this.getHAPValue('ContactSensorState') || 0) === 0,
|
|
601
|
+
};
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case 'DoorLock': {
|
|
605
|
+
const lockState = this.getHAPValue('LockCurrentState') || 0;
|
|
606
|
+
config.doorLock = {
|
|
607
|
+
lockState: lockState === 1 ? 1 : 2,
|
|
608
|
+
lockType: 0,
|
|
609
|
+
};
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
case 'WindowCovering': {
|
|
613
|
+
const position = this.getHAPValue('CurrentPosition') || 0;
|
|
614
|
+
config.windowCovering = {
|
|
615
|
+
currentPositionLiftPercent100ths: position * 100,
|
|
616
|
+
targetPositionLiftPercent100ths: (this.getHAPValue('TargetPosition') || position) * 100,
|
|
617
|
+
};
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case 'Thermostat': {
|
|
621
|
+
config.thermostat = {
|
|
622
|
+
localTemperature: Math.round((this.getHAPValue('CurrentTemperature') || 20) * 100),
|
|
623
|
+
occupiedCoolingSetpoint: Math.round((this.getHAPValue('CoolingThresholdTemperature') || 26) * 100),
|
|
624
|
+
occupiedHeatingSetpoint: Math.round((this.getHAPValue('HeatingThresholdTemperature') || 20) * 100),
|
|
625
|
+
systemMode: this.getHAPValue('CurrentHeatingCoolingState') || 0,
|
|
626
|
+
};
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'Fan': {
|
|
630
|
+
const speed = this.getHAPValue('RotationSpeed') || 0;
|
|
631
|
+
config.fanControl = {
|
|
632
|
+
percentSetting: Math.round(speed),
|
|
633
|
+
fanMode: speed > 0 ? 3 : 0,
|
|
634
|
+
};
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case 'WaterLeakDetector': {
|
|
638
|
+
config.booleanState = {
|
|
639
|
+
stateValue: Boolean(this.getHAPValue('LeakDetected')),
|
|
640
|
+
};
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 'SmokeCoAlarm': {
|
|
644
|
+
config.smokeCoAlarm = {
|
|
645
|
+
smokeState: this.getHAPValue('SmokeDetected') ? 1 : 0,
|
|
646
|
+
coState: this.getHAPValue('CarbonMonoxideDetected') ? 1 : 0,
|
|
647
|
+
};
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'GenericSwitch': {
|
|
651
|
+
// Generic switch for programmable switches
|
|
652
|
+
config.switch = {
|
|
653
|
+
currentPosition: 0,
|
|
654
|
+
numberOfPositions: 2,
|
|
655
|
+
};
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
default: {
|
|
659
|
+
// Default to basic on/off if we have the characteristic
|
|
660
|
+
if (this.getHAPValue('On') !== undefined) {
|
|
661
|
+
config.onOff = {
|
|
662
|
+
onOff: this.getHAPValue('On') || false,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Initialize Matter endpoint from HAP values
|
|
671
|
+
*/
|
|
672
|
+
async initializeFromHAP() {
|
|
673
|
+
if (!this.endpoint || !this.primaryService || !this.typedEndpoint) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// Initial values are already set during endpoint creation
|
|
677
|
+
// This method is for future use when we need to refresh values
|
|
678
|
+
log.debug(`Initialized Matter device with HAP values for: ${this.accessory.displayName}`);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Start bidirectional sync between HAP and Matter
|
|
682
|
+
*/
|
|
683
|
+
startSync() {
|
|
684
|
+
if (!this.primaryService || !this.endpoint || this.isDestroyed) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
log.debug(`Starting sync for: ${this.accessory.displayName}`);
|
|
688
|
+
// Limit number of subscriptions to prevent memory issues
|
|
689
|
+
const MAX_SUBSCRIPTIONS = 50; // Prevent memory leaks from unbounded subscriptions
|
|
690
|
+
let subscriptionCount = 0;
|
|
691
|
+
// Subscribe to HAP characteristic changes
|
|
692
|
+
for (const characteristic of this.primaryService.characteristics) {
|
|
693
|
+
const charType = characteristic.constructor.name;
|
|
694
|
+
// Skip non-data characteristics
|
|
695
|
+
if (charType === 'Name' || charType === 'ServiceLabelIndex') {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
// Check subscription limit
|
|
699
|
+
if (subscriptionCount >= MAX_SUBSCRIPTIONS) {
|
|
700
|
+
log.warn(`Subscription limit reached (${MAX_SUBSCRIPTIONS}) for ${this.accessory.displayName}, skipping additional characteristics`);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
// Prevent duplicate subscriptions
|
|
704
|
+
if (this.characteristicSubscriptions.has(charType)) {
|
|
705
|
+
log.debug(`Already subscribed to ${charType} for ${this.accessory.displayName}`);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const handler = this.createHAPChangeHandler(charType);
|
|
709
|
+
if (handler) {
|
|
710
|
+
characteristic.on('change', handler);
|
|
711
|
+
this.characteristicSubscriptions.set(charType, handler);
|
|
712
|
+
this.cleanupCallbacks.push(() => {
|
|
713
|
+
characteristic.off('change', handler);
|
|
714
|
+
});
|
|
715
|
+
subscriptionCount++;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
log.debug(`✅ Sync started for: ${this.accessory.displayName}`);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Create HAP change handler for a characteristic
|
|
722
|
+
*/
|
|
723
|
+
createHAPChangeHandler(characteristicType) {
|
|
724
|
+
return (change) => {
|
|
725
|
+
if (this.isDestroyed || !this.typedEndpoint) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const { newValue } = change;
|
|
729
|
+
log.debug(`HAP change for ${this.accessory.displayName}: ${characteristicType} = ${newValue}`);
|
|
730
|
+
// Map HAP changes to Matter updates
|
|
731
|
+
// This would be expanded to handle all characteristic types
|
|
732
|
+
this.updateMatterFromHAP(characteristicType, newValue).catch((error) => {
|
|
733
|
+
log.error(`Failed to update Matter from HAP for ${this.accessory.displayName}:`, error);
|
|
734
|
+
});
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Update Matter state from HAP change
|
|
739
|
+
*/
|
|
740
|
+
async updateMatterFromHAP(characteristicType, value) {
|
|
741
|
+
if (!this.typedEndpoint) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// Map HAP characteristics to Matter cluster updates
|
|
745
|
+
switch (characteristicType) {
|
|
746
|
+
case 'On':
|
|
747
|
+
await this.typedEndpoint.updateClusterState('onOff', { onOff: Boolean(value) });
|
|
748
|
+
break;
|
|
749
|
+
case 'Brightness':
|
|
750
|
+
await this.typedEndpoint.updateClusterState('levelControl', {
|
|
751
|
+
currentLevel: Math.round(value * 2.54),
|
|
752
|
+
});
|
|
753
|
+
break;
|
|
754
|
+
case 'Hue':
|
|
755
|
+
await this.typedEndpoint.updateClusterState('colorControl', {
|
|
756
|
+
currentHue: Math.round(value * 254 / 360),
|
|
757
|
+
});
|
|
758
|
+
break;
|
|
759
|
+
case 'Saturation':
|
|
760
|
+
await this.typedEndpoint.updateClusterState('colorControl', {
|
|
761
|
+
currentSaturation: Math.round(value * 2.54),
|
|
762
|
+
});
|
|
763
|
+
break;
|
|
764
|
+
case 'ColorTemperature': {
|
|
765
|
+
const mireds = Math.round(1000000 / value);
|
|
766
|
+
await this.typedEndpoint.updateClusterState('colorControl', {
|
|
767
|
+
colorTemperatureMireds: Math.max(153, Math.min(500, mireds)),
|
|
768
|
+
});
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case 'CurrentTemperature':
|
|
772
|
+
await this.typedEndpoint.updateClusterState('temperatureMeasurement', {
|
|
773
|
+
measuredValue: Math.round(value * 100),
|
|
774
|
+
});
|
|
775
|
+
break;
|
|
776
|
+
case 'CurrentRelativeHumidity':
|
|
777
|
+
await this.typedEndpoint.updateClusterState('relativeHumidityMeasurement', {
|
|
778
|
+
measuredValue: Math.round(value * 100),
|
|
779
|
+
});
|
|
780
|
+
break;
|
|
781
|
+
case 'OccupancyDetected':
|
|
782
|
+
case 'MotionDetected':
|
|
783
|
+
await this.typedEndpoint.updateClusterState('occupancySensing', {
|
|
784
|
+
occupancy: { occupied: Boolean(value) },
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
case 'ContactSensorState':
|
|
788
|
+
await this.typedEndpoint.updateClusterState('booleanState', {
|
|
789
|
+
stateValue: value === 0,
|
|
790
|
+
});
|
|
791
|
+
break;
|
|
792
|
+
case 'LockCurrentState':
|
|
793
|
+
await this.typedEndpoint.updateClusterState('doorLock', {
|
|
794
|
+
lockState: value === 1 ? 1 : 2,
|
|
795
|
+
});
|
|
796
|
+
break;
|
|
797
|
+
// Add more characteristic mappings as needed
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Stop synchronization
|
|
802
|
+
*/
|
|
803
|
+
stopSync() {
|
|
804
|
+
// Remove all HAP characteristic subscriptions
|
|
805
|
+
if (this.primaryService) {
|
|
806
|
+
for (const [charType, handler] of this.characteristicSubscriptions) {
|
|
807
|
+
const characteristic = this.primaryService.characteristics.find(c => c.constructor.name === charType);
|
|
808
|
+
if (characteristic && typeof handler === 'function') {
|
|
809
|
+
characteristic.off('change', handler);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
this.characteristicSubscriptions.clear();
|
|
814
|
+
// Run all cleanup callbacks
|
|
815
|
+
for (const cleanup of this.cleanupCallbacks) {
|
|
816
|
+
try {
|
|
817
|
+
const result = cleanup();
|
|
818
|
+
if (result instanceof Promise) {
|
|
819
|
+
result.catch(error => log.debug('Cleanup error:', error));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
log.debug('Cleanup error:', error);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
this.cleanupCallbacks.length = 0; // Clear array contents
|
|
827
|
+
log.debug(`Sync stopped for: ${this.accessory.displayName}`);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Get the primary service from accessory
|
|
831
|
+
*/
|
|
832
|
+
getPrimaryService() {
|
|
833
|
+
const utilityServices = [
|
|
834
|
+
'AccessoryInformation',
|
|
835
|
+
'BridgingState',
|
|
836
|
+
'HAPProtocolInformation',
|
|
837
|
+
'Pairing',
|
|
838
|
+
'BridgeConfiguration',
|
|
839
|
+
];
|
|
840
|
+
for (const service of this.accessory.services) {
|
|
841
|
+
// Try multiple ways to get the service type
|
|
842
|
+
const serviceType = service.constructor.name
|
|
843
|
+
|| service.displayName
|
|
844
|
+
|| service.UUID;
|
|
845
|
+
// Also check the service's UUID/subtype
|
|
846
|
+
const serviceName = service.displayName || serviceType;
|
|
847
|
+
if (!utilityServices.includes(serviceType)) {
|
|
848
|
+
log.debug(`Found primary service for ${this.accessory.displayName}: ${serviceName} (type: ${serviceType})`);
|
|
849
|
+
return service;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Get HAP characteristic value
|
|
856
|
+
*/
|
|
857
|
+
getHAPValue(characteristicType) {
|
|
858
|
+
const characteristic = this.getHAPCharacteristic(characteristicType);
|
|
859
|
+
return characteristic?.value;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get HAP characteristic by type
|
|
863
|
+
*/
|
|
864
|
+
getHAPCharacteristic(characteristicType) {
|
|
865
|
+
if (!this.primaryService) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
return this.primaryService.characteristics.find(c => c.constructor.name === characteristicType) || null;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Convert Kelvin to Mireds
|
|
872
|
+
*/
|
|
873
|
+
kelvinToMireds(kelvin) {
|
|
874
|
+
return Math.round(1000000 / kelvin);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Convert Mireds to Kelvin
|
|
878
|
+
*/
|
|
879
|
+
miredsToKelvin(mireds) {
|
|
880
|
+
return Math.round(1000000 / mireds);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Get the device type
|
|
884
|
+
*/
|
|
885
|
+
getDeviceType() {
|
|
886
|
+
return this.deviceType;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Get the Matter endpoint
|
|
890
|
+
*/
|
|
891
|
+
async getEndpoint() {
|
|
892
|
+
return this.endpoint;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Clean up the device
|
|
896
|
+
*/
|
|
897
|
+
async destroy() {
|
|
898
|
+
if (this.isDestroyed) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
this.isDestroyed = true;
|
|
902
|
+
this.stopSync();
|
|
903
|
+
// Clear all references
|
|
904
|
+
this.endpoint = null;
|
|
905
|
+
this.typedEndpoint = null;
|
|
906
|
+
this.primaryService = null;
|
|
907
|
+
this.deviceType = null;
|
|
908
|
+
this.characteristicSubscriptions.clear();
|
|
909
|
+
this.cleanupCallbacks.length = 0; // Clear array contents
|
|
910
|
+
log.debug(`Destroyed Matter device for: ${this.accessory.displayName}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
//# sourceMappingURL=matterDevice.js.map
|