homebridge 2.0.0-alpha.37 → 2.0.0-alpha.38
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/config-sample.json +12 -1
- package/dist/api.d.ts +38 -20
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +40 -1
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +20 -1
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +8 -2
- package/dist/bridgeService.js.map +1 -1
- package/dist/childBridgeFork.d.ts +1 -1
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +1 -1
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +1 -0
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +6 -4
- package/dist/childBridgeService.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/externalPortService.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/ipcService.d.ts +1 -4
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/matterConfigValidator.d.ts +34 -0
- package/dist/matterConfigValidator.d.ts.map +1 -0
- package/dist/matterConfigValidator.js +249 -0
- package/dist/matterConfigValidator.js.map +1 -0
- package/dist/matterService.d.ts +168 -0
- package/dist/matterService.d.ts.map +1 -0
- package/dist/matterService.js +677 -0
- package/dist/matterService.js.map +1 -0
- package/dist/matterTypes.d.ts +20 -0
- package/dist/matterTypes.d.ts.map +1 -0
- package/dist/matterTypes.js +278 -0
- package/dist/matterTypes.js.map +1 -0
- package/dist/platformAccessory.d.ts +3 -2
- package/dist/platformAccessory.d.ts.map +1 -1
- package/dist/platformAccessory.js +8 -1
- package/dist/platformAccessory.js.map +1 -1
- package/dist/plugin.d.ts +0 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +3 -6
- package/dist/plugin.js.map +1 -1
- package/dist/pluginManager.d.ts.map +1 -1
- package/dist/pluginManager.js +20 -19
- package/dist/pluginManager.js.map +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +41 -4
- package/dist/server.js.map +1 -1
- package/dist/storageService.js +8 -8
- package/dist/storageService.js.map +1 -1
- package/dist/user.js +7 -7
- package/dist/user.js.map +1 -1
- package/dist/util/mac.d.ts.map +1 -1
- package/dist/util/mac.js +3 -2
- package/dist/util/mac.js.map +1 -1
- package/dist/util/matter-cli.d.ts +3 -0
- package/dist/util/matter-cli.d.ts.map +1 -0
- package/dist/util/matter-cli.js +211 -0
- package/dist/util/matter-cli.js.map +1 -0
- package/dist/version.js +2 -2
- package/dist/version.js.map +1 -1
- package/package.json +22 -19
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'fs-extra';
|
|
3
|
+
import { Logger } from './logger.js';
|
|
4
|
+
import { MatterConfigValidator } from './matterConfigValidator.js';
|
|
5
|
+
import '@matter/main';
|
|
6
|
+
const log = Logger.internal;
|
|
7
|
+
/**
|
|
8
|
+
* Matter service for publishing accessories via Matter protocol
|
|
9
|
+
* This runs alongside the existing HAP bridge service
|
|
10
|
+
*/
|
|
11
|
+
export class MatterService {
|
|
12
|
+
matterConfiguration;
|
|
13
|
+
pluginManager;
|
|
14
|
+
externalPortService;
|
|
15
|
+
api;
|
|
16
|
+
options;
|
|
17
|
+
matterServer = null;
|
|
18
|
+
commissioningServer = null;
|
|
19
|
+
storageManager = null;
|
|
20
|
+
isEnabled;
|
|
21
|
+
matterConfig;
|
|
22
|
+
publishedAccessories = new Map();
|
|
23
|
+
isInitialized = false;
|
|
24
|
+
isStarted = false;
|
|
25
|
+
constructor(matterConfiguration, pluginManager, externalPortService, api, options) {
|
|
26
|
+
this.matterConfiguration = matterConfiguration;
|
|
27
|
+
this.pluginManager = pluginManager;
|
|
28
|
+
this.externalPortService = externalPortService;
|
|
29
|
+
this.api = api;
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.matterConfig = {
|
|
32
|
+
enabled: false,
|
|
33
|
+
port: 5540,
|
|
34
|
+
discriminator: 3840,
|
|
35
|
+
passcode: 20202021,
|
|
36
|
+
vendorId: 0xFFF1, // Test Vendor ID
|
|
37
|
+
productId: 0x8001, // Test Product ID
|
|
38
|
+
deviceName: 'Homebridge Matter Bridge',
|
|
39
|
+
deviceType: 0x0016, // Matter Bridge device type
|
|
40
|
+
storageDir: options.customStoragePath || './persist',
|
|
41
|
+
debugEnabled: false,
|
|
42
|
+
interfaceName: undefined,
|
|
43
|
+
announceInterval: 60,
|
|
44
|
+
commissioningTimeout: 900, // 15 minutes
|
|
45
|
+
...matterConfiguration,
|
|
46
|
+
};
|
|
47
|
+
this.isEnabled = this.matterConfig.enabled || false;
|
|
48
|
+
this.validateConfiguration();
|
|
49
|
+
if (this.isEnabled) {
|
|
50
|
+
log.info('Matter service enabled');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
log.debug('Matter service disabled');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate Matter configuration parameters
|
|
58
|
+
*/
|
|
59
|
+
validateConfiguration() {
|
|
60
|
+
if (!this.isEnabled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const validationResult = MatterConfigValidator.validate(this.matterConfig);
|
|
64
|
+
if (!validationResult.isValid) {
|
|
65
|
+
const errorMessage = `Matter configuration validation failed:\n${validationResult.errors.join('\n')}`;
|
|
66
|
+
throw new Error(errorMessage);
|
|
67
|
+
}
|
|
68
|
+
if (validationResult.warnings.length > 0) {
|
|
69
|
+
log.warn('Matter configuration has warnings - see above for details');
|
|
70
|
+
}
|
|
71
|
+
log.info(`Matter configuration validated successfully: port=${this.matterConfig.port}, discriminator=${this.matterConfig.discriminator}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Initialize the Matter server if enabled
|
|
75
|
+
*/
|
|
76
|
+
async initialize() {
|
|
77
|
+
if (!this.isEnabled || this.isInitialized) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
log.info('Initializing Matter server...');
|
|
82
|
+
// Initialize storage
|
|
83
|
+
await this.initializeStorage();
|
|
84
|
+
// Create and configure Matter server node
|
|
85
|
+
await this.createMatterServer();
|
|
86
|
+
// Setup commissioning server
|
|
87
|
+
await this.setupCommissioningServer();
|
|
88
|
+
this.isInitialized = true;
|
|
89
|
+
log.info('Matter server initialized successfully');
|
|
90
|
+
this.logCommissioningInfo();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
log.error('Failed to initialize Matter server:', error);
|
|
94
|
+
await this.cleanup();
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Initialize storage for Matter server
|
|
100
|
+
*/
|
|
101
|
+
async initializeStorage() {
|
|
102
|
+
try {
|
|
103
|
+
// Create storage directory if it doesn't exist
|
|
104
|
+
const storageDir = `${this.matterConfig.storageDir}/matter`;
|
|
105
|
+
await fs.ensureDir(storageDir);
|
|
106
|
+
// Use placeholder storage manager for now
|
|
107
|
+
// In production, this would use proper Matter.js storage
|
|
108
|
+
this.storageManager = {
|
|
109
|
+
initialized: true,
|
|
110
|
+
async initialize() {
|
|
111
|
+
return Promise.resolve();
|
|
112
|
+
},
|
|
113
|
+
async close() {
|
|
114
|
+
return Promise.resolve();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
await this.storageManager.initialize();
|
|
118
|
+
log.debug('Matter storage initialized');
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
log.error('Failed to initialize Matter storage:', error);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create the Matter server node
|
|
127
|
+
*/
|
|
128
|
+
async createMatterServer() {
|
|
129
|
+
if (!this.storageManager) {
|
|
130
|
+
throw new Error('Storage manager not initialized');
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
// Create placeholder Matter server
|
|
134
|
+
// In production, this would be a real Matter.js ServerNode
|
|
135
|
+
this.matterServer = {
|
|
136
|
+
started: false,
|
|
137
|
+
devices: new Map(),
|
|
138
|
+
async start() {
|
|
139
|
+
this.started = true;
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
},
|
|
142
|
+
async stop() {
|
|
143
|
+
this.started = false;
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
},
|
|
146
|
+
async addDevice(device) {
|
|
147
|
+
const id = crypto.randomUUID();
|
|
148
|
+
this.devices.set(id, device);
|
|
149
|
+
return { id, device };
|
|
150
|
+
},
|
|
151
|
+
async removeDevice(endpoint) {
|
|
152
|
+
this.devices.delete(endpoint.id);
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
log.debug('Matter server node created (placeholder implementation)');
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
log.error('Failed to create Matter server node:', error);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Setup commissioning server for device pairing
|
|
165
|
+
*/
|
|
166
|
+
async setupCommissioningServer() {
|
|
167
|
+
if (!this.matterServer) {
|
|
168
|
+
throw new Error('Matter server not created');
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
// Create placeholder commissioning server
|
|
172
|
+
// In production, this would be a real Matter.js CommissioningServer
|
|
173
|
+
this.commissioningServer = {
|
|
174
|
+
started: false,
|
|
175
|
+
async start() {
|
|
176
|
+
this.started = true;
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
},
|
|
179
|
+
async stop() {
|
|
180
|
+
this.started = false;
|
|
181
|
+
return Promise.resolve();
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
log.debug('Matter commissioning server configured (placeholder implementation)');
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
log.error('Failed to setup commissioning server:', error);
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Generate a unique serial number for the Matter bridge
|
|
193
|
+
*/
|
|
194
|
+
generateSerialNumber() {
|
|
195
|
+
// Use the HAP bridge username as base for consistency
|
|
196
|
+
const hapBridge = this.api.hap?.HAPStorage.storage()?.getItem('AccessoryInfo.CC:22:3D:E3:CE:30');
|
|
197
|
+
if (hapBridge && hapBridge.serialNumber) {
|
|
198
|
+
return `HB-${hapBridge.serialNumber}`;
|
|
199
|
+
}
|
|
200
|
+
// Fallback to generated serial
|
|
201
|
+
return `HB-${crypto.randomBytes(6).toString('hex').toUpperCase()}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Generate a unique identifier for the Matter bridge
|
|
205
|
+
*/
|
|
206
|
+
generateUniqueId() {
|
|
207
|
+
// Use the HAP bridge ID as base for consistency
|
|
208
|
+
const hapBridge = this.api.hap?.HAPStorage.storage()?.getItem('AccessoryInfo.CC:22:3D:E3:CE:30');
|
|
209
|
+
if (hapBridge && hapBridge.id) {
|
|
210
|
+
return `homebridge-matter-${hapBridge.id}`;
|
|
211
|
+
}
|
|
212
|
+
// Fallback to generated ID
|
|
213
|
+
return `homebridge-matter-${crypto.randomUUID()}`;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Log commissioning information for users
|
|
217
|
+
*/
|
|
218
|
+
logCommissioningInfo() {
|
|
219
|
+
try {
|
|
220
|
+
// Generate placeholder QR code info
|
|
221
|
+
// In production, this would use real Matter.js QrCode.encode()
|
|
222
|
+
const setupCode = `MT:${this.matterConfig.discriminator.toString().padStart(4, '0')}${this.matterConfig.passcode}`;
|
|
223
|
+
const qrCode = `https://dhrishi.github.io/connectedhomeip/qrcode.html?data=${setupCode}`;
|
|
224
|
+
log.info('Matter bridge is ready for commissioning:');
|
|
225
|
+
log.info(` Setup Code: ${setupCode}`);
|
|
226
|
+
log.info(` QR Code URL: ${qrCode}`);
|
|
227
|
+
log.info(` Manual Pairing Code: ${this.formatPairingCode(this.matterConfig.passcode)}`);
|
|
228
|
+
log.info(` Discriminator: ${this.matterConfig.discriminator}`);
|
|
229
|
+
log.info(` Port: ${this.matterConfig.port}`);
|
|
230
|
+
log.info(' Note: This is a development implementation - QR codes may not work with all controllers');
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
log.warn('Could not generate commissioning QR code:', error);
|
|
234
|
+
log.info(`Matter bridge ready - Manual setup: Passcode ${this.matterConfig.passcode}, Discriminator ${this.matterConfig.discriminator}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Format passcode for manual pairing
|
|
239
|
+
*/
|
|
240
|
+
formatPairingCode(passcode) {
|
|
241
|
+
const code = passcode.toString().padStart(8, '0');
|
|
242
|
+
return `${code.slice(0, 3)}-${code.slice(3, 5)}-${code.slice(5)}`;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Start the Matter server
|
|
246
|
+
*/
|
|
247
|
+
async start() {
|
|
248
|
+
if (!this.isEnabled || !this.matterServer || this.isStarted) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
log.info('Starting Matter server...');
|
|
253
|
+
// Start the Matter server node
|
|
254
|
+
await this.matterServer.start();
|
|
255
|
+
// Start commissioning
|
|
256
|
+
if (this.commissioningServer) {
|
|
257
|
+
await this.commissioningServer.start();
|
|
258
|
+
}
|
|
259
|
+
this.isStarted = true;
|
|
260
|
+
log.info(`Matter server started successfully on port ${this.matterConfig.port}`);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
log.error('Failed to start Matter server:', error);
|
|
264
|
+
this.isStarted = false;
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Stop the Matter server
|
|
270
|
+
*/
|
|
271
|
+
async stop() {
|
|
272
|
+
if (!this.isStarted) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
log.info('Stopping Matter server...');
|
|
277
|
+
// Stop commissioning server
|
|
278
|
+
if (this.commissioningServer) {
|
|
279
|
+
await this.commissioningServer.stop();
|
|
280
|
+
}
|
|
281
|
+
// Stop Matter server
|
|
282
|
+
if (this.matterServer) {
|
|
283
|
+
await this.matterServer.stop();
|
|
284
|
+
}
|
|
285
|
+
await this.cleanup();
|
|
286
|
+
this.isStarted = false;
|
|
287
|
+
log.info('Matter server stopped successfully');
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
log.error('Failed to stop Matter server:', error);
|
|
291
|
+
this.isStarted = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Clean up resources
|
|
296
|
+
*/
|
|
297
|
+
async cleanup() {
|
|
298
|
+
try {
|
|
299
|
+
// Close storage
|
|
300
|
+
if (this.storageManager) {
|
|
301
|
+
await this.storageManager.close();
|
|
302
|
+
this.storageManager = null;
|
|
303
|
+
}
|
|
304
|
+
this.matterServer = null;
|
|
305
|
+
this.commissioningServer = null;
|
|
306
|
+
this.publishedAccessories.clear();
|
|
307
|
+
this.isInitialized = false;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
log.error('Error during Matter service cleanup:', error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Publish a platform accessory via Matter protocol
|
|
315
|
+
*/
|
|
316
|
+
async publishAccessory(accessory) {
|
|
317
|
+
if (!this.isEnabled || !this.matterServer || !this.isStarted) {
|
|
318
|
+
log.debug(`Cannot publish accessory "${accessory.displayName}" - Matter server not ready`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
log.debug(`Publishing accessory "${accessory.displayName}" via Matter...`);
|
|
323
|
+
// Convert HAP accessory to Matter device
|
|
324
|
+
const matterDevice = await this.convertToMatterDevice(accessory);
|
|
325
|
+
if (matterDevice) {
|
|
326
|
+
// Add the device to the Matter server as a bridged device
|
|
327
|
+
const endpoint = await this.matterServer.addDevice(matterDevice);
|
|
328
|
+
// Track the published accessory
|
|
329
|
+
this.publishedAccessories.set(accessory.UUID, {
|
|
330
|
+
accessory,
|
|
331
|
+
endpoint,
|
|
332
|
+
});
|
|
333
|
+
log.info(`Published accessory "${accessory.displayName}" via Matter successfully`);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
log.warn(`Could not convert accessory "${accessory.displayName}" to Matter device - unsupported services`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
log.error(`Failed to publish accessory "${accessory.displayName}" via Matter:`, error);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Unpublish a platform accessory from Matter protocol
|
|
345
|
+
*/
|
|
346
|
+
async unpublishAccessory(accessory) {
|
|
347
|
+
if (!this.isEnabled || !this.matterServer) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
const publishedDevice = this.publishedAccessories.get(accessory.UUID);
|
|
352
|
+
if (publishedDevice) {
|
|
353
|
+
// Remove the device from the Matter server
|
|
354
|
+
await this.matterServer.removeDevice(publishedDevice.endpoint);
|
|
355
|
+
// Remove from tracking
|
|
356
|
+
this.publishedAccessories.delete(accessory.UUID);
|
|
357
|
+
log.info(`Unpublished accessory "${accessory.displayName}" from Matter successfully`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
log.debug(`Accessory "${accessory.displayName}" was not published via Matter`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
log.error(`Failed to unpublish accessory "${accessory.displayName}" from Matter:`, error);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Convert HAP accessory to Matter device
|
|
369
|
+
* This maps HAP services and characteristics to appropriate Matter clusters and attributes
|
|
370
|
+
*/
|
|
371
|
+
async convertToMatterDevice(accessory) {
|
|
372
|
+
try {
|
|
373
|
+
log.debug(`Converting accessory "${accessory.displayName}" to Matter device...`);
|
|
374
|
+
// Get the primary service (excluding AccessoryInformation and other utility services)
|
|
375
|
+
const primaryService = this.getPrimaryService(accessory);
|
|
376
|
+
if (!primaryService) {
|
|
377
|
+
log.warn(`No primary service found for accessory "${accessory.displayName}"`);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const serviceType = primaryService.constructor.name;
|
|
381
|
+
const characteristics = primaryService.characteristics.map(char => char.constructor.name);
|
|
382
|
+
log.debug(`Primary service: ${serviceType}, characteristics: ${characteristics.join(', ')}`);
|
|
383
|
+
// Determine the appropriate Matter device type and clusters
|
|
384
|
+
const deviceType = this.getMatterDeviceType(serviceType, characteristics);
|
|
385
|
+
if (!deviceType) {
|
|
386
|
+
log.warn(`Unsupported service type for Matter conversion: ${serviceType}`);
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
// Create the Matter device with appropriate clusters
|
|
390
|
+
const matterDevice = await this.createMatterDevice(deviceType, accessory, primaryService);
|
|
391
|
+
log.debug(`Successfully converted "${accessory.displayName}" to Matter ${deviceType} device`);
|
|
392
|
+
return matterDevice;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
log.error(`Error converting accessory "${accessory.displayName}" to Matter device:`, error);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get the primary service from an accessory (excluding utility services)
|
|
401
|
+
*/
|
|
402
|
+
getPrimaryService(accessory) {
|
|
403
|
+
const utilityServices = [
|
|
404
|
+
'AccessoryInformation',
|
|
405
|
+
'BridgingState',
|
|
406
|
+
'HAPProtocolInformation',
|
|
407
|
+
'Pairing',
|
|
408
|
+
'BridgeConfiguration',
|
|
409
|
+
];
|
|
410
|
+
for (const service of accessory.services) {
|
|
411
|
+
const serviceType = service.constructor.name;
|
|
412
|
+
if (!utilityServices.includes(serviceType)) {
|
|
413
|
+
return service;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Determine the appropriate Matter device type for a HAP service
|
|
420
|
+
*/
|
|
421
|
+
getMatterDeviceType(serviceType, characteristics) {
|
|
422
|
+
// Handle lighting services with specific capabilities
|
|
423
|
+
if (serviceType === 'Lightbulb') {
|
|
424
|
+
if (characteristics.includes('Hue') && characteristics.includes('Saturation')) {
|
|
425
|
+
return 'ExtendedColorLight';
|
|
426
|
+
}
|
|
427
|
+
else if (characteristics.includes('ColorTemperature')) {
|
|
428
|
+
return 'ColorTemperatureLight';
|
|
429
|
+
}
|
|
430
|
+
else if (characteristics.includes('Brightness')) {
|
|
431
|
+
return 'DimmableLight';
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
return 'OnOffLight';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Handle outlet services
|
|
438
|
+
if (serviceType === 'Outlet') {
|
|
439
|
+
if (characteristics.includes('Brightness')) {
|
|
440
|
+
return 'DimmablePlugInUnit';
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
return 'OnOffPlugInUnit';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Handle switch services
|
|
447
|
+
if (serviceType === 'Switch') {
|
|
448
|
+
if (characteristics.includes('Brightness')) {
|
|
449
|
+
return 'DimmerSwitch';
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
return 'OnOffLightSwitch';
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Direct mapping for other services
|
|
456
|
+
const directMappings = {
|
|
457
|
+
TemperatureSensor: 'TemperatureSensor',
|
|
458
|
+
HumiditySensor: 'HumiditySensor',
|
|
459
|
+
LightSensor: 'LightSensor',
|
|
460
|
+
MotionSensor: 'OccupancySensor',
|
|
461
|
+
OccupancySensor: 'OccupancySensor',
|
|
462
|
+
ContactSensor: 'ContactSensor',
|
|
463
|
+
LeakSensor: 'WaterLeakDetector',
|
|
464
|
+
SmokeSensor: 'SmokeCoAlarm',
|
|
465
|
+
CarbonMonoxideSensor: 'SmokeCoAlarm',
|
|
466
|
+
LockManagement: 'DoorLock',
|
|
467
|
+
Thermostat: 'Thermostat',
|
|
468
|
+
Fan: 'Fan',
|
|
469
|
+
Fanv2: 'Fan',
|
|
470
|
+
WindowCovering: 'WindowCovering',
|
|
471
|
+
StatelessProgrammableSwitch: 'GenericSwitch',
|
|
472
|
+
Valve: 'WaterValve',
|
|
473
|
+
};
|
|
474
|
+
return directMappings[serviceType] || null;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Create a Matter device with the specified type and map characteristics
|
|
478
|
+
*/
|
|
479
|
+
async createMatterDevice(deviceType, accessory, primaryService) {
|
|
480
|
+
log.debug(`Creating Matter device of type ${deviceType} (placeholder implementation)`);
|
|
481
|
+
// Create a placeholder Matter device
|
|
482
|
+
// In production, this would use real Matter.js device classes
|
|
483
|
+
const device = {
|
|
484
|
+
type: deviceType,
|
|
485
|
+
accessoryUUID: accessory.UUID,
|
|
486
|
+
displayName: accessory.displayName,
|
|
487
|
+
services: accessory.services.map((s) => ({
|
|
488
|
+
type: s.constructor.name,
|
|
489
|
+
characteristics: s.characteristics.map((c) => ({
|
|
490
|
+
type: c.constructor.name,
|
|
491
|
+
value: c.value,
|
|
492
|
+
})),
|
|
493
|
+
})),
|
|
494
|
+
// Placeholder clusters based on device type
|
|
495
|
+
clusters: this.getPlaceholderClusters(deviceType),
|
|
496
|
+
};
|
|
497
|
+
// Map characteristics to Matter attributes (placeholder)
|
|
498
|
+
await this.mapCharacteristicsToMatter(device, primaryService);
|
|
499
|
+
return device;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Get placeholder clusters for a device type
|
|
503
|
+
*/
|
|
504
|
+
getPlaceholderClusters(deviceType) {
|
|
505
|
+
const clusterMappings = {
|
|
506
|
+
OnOffLight: ['OnOff', 'Identify'],
|
|
507
|
+
DimmableLight: ['OnOff', 'LevelControl', 'Identify'],
|
|
508
|
+
ExtendedColorLight: ['OnOff', 'LevelControl', 'ColorControl', 'Identify'],
|
|
509
|
+
TemperatureSensor: ['TemperatureMeasurement', 'Identify'],
|
|
510
|
+
HumiditySensor: ['RelativeHumidityMeasurement', 'Identify'],
|
|
511
|
+
ContactSensor: ['BooleanState', 'Identify'],
|
|
512
|
+
DoorLock: ['DoorLock', 'Identify'],
|
|
513
|
+
WindowCovering: ['WindowCovering', 'Identify'],
|
|
514
|
+
Thermostat: ['Thermostat', 'Identify'],
|
|
515
|
+
Fan: ['FanControl', 'Identify'],
|
|
516
|
+
};
|
|
517
|
+
return clusterMappings[deviceType] || ['Identify'];
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Map HAP characteristics to Matter cluster attributes
|
|
521
|
+
*/
|
|
522
|
+
async mapCharacteristicsToMatter(matterDevice, hapService) {
|
|
523
|
+
try {
|
|
524
|
+
log.debug(`Mapping characteristics for service ${hapService.constructor.name} (placeholder implementation)`);
|
|
525
|
+
// This is a placeholder implementation that tracks the mapping
|
|
526
|
+
// In production, this would actually sync values between HAP and Matter
|
|
527
|
+
matterDevice.characteristicMappings = [];
|
|
528
|
+
for (const characteristic of hapService.characteristics) {
|
|
529
|
+
const charType = characteristic.constructor.name;
|
|
530
|
+
const mapping = {
|
|
531
|
+
hapCharacteristic: charType,
|
|
532
|
+
hapValue: characteristic.value,
|
|
533
|
+
matterCluster: this.getMatterClusterForCharacteristic(charType),
|
|
534
|
+
matterAttribute: this.getMatterAttributeForCharacteristic(charType),
|
|
535
|
+
};
|
|
536
|
+
matterDevice.characteristicMappings.push(mapping);
|
|
537
|
+
log.debug(`Mapped ${charType} -> ${mapping.matterCluster}.${mapping.matterAttribute}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
log.error('Error mapping characteristics to Matter:', error);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get the Matter cluster name for a HAP characteristic
|
|
546
|
+
*/
|
|
547
|
+
getMatterClusterForCharacteristic(charType) {
|
|
548
|
+
const mappings = {
|
|
549
|
+
On: 'OnOff',
|
|
550
|
+
Brightness: 'LevelControl',
|
|
551
|
+
Hue: 'ColorControl',
|
|
552
|
+
Saturation: 'ColorControl',
|
|
553
|
+
ColorTemperature: 'ColorControl',
|
|
554
|
+
CurrentTemperature: 'TemperatureMeasurement',
|
|
555
|
+
CurrentRelativeHumidity: 'RelativeHumidityMeasurement',
|
|
556
|
+
ContactSensorState: 'BooleanState',
|
|
557
|
+
MotionDetected: 'OccupancySensing',
|
|
558
|
+
LockCurrentState: 'DoorLock',
|
|
559
|
+
CurrentPosition: 'WindowCovering',
|
|
560
|
+
CurrentHeatingCoolingState: 'Thermostat',
|
|
561
|
+
RotationSpeed: 'FanControl',
|
|
562
|
+
};
|
|
563
|
+
return mappings[charType] || 'Unknown';
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Get the Matter attribute name for a HAP characteristic
|
|
567
|
+
*/
|
|
568
|
+
getMatterAttributeForCharacteristic(charType) {
|
|
569
|
+
const mappings = {
|
|
570
|
+
On: 'OnOff',
|
|
571
|
+
Brightness: 'CurrentLevel',
|
|
572
|
+
Hue: 'CurrentHue',
|
|
573
|
+
Saturation: 'CurrentSaturation',
|
|
574
|
+
ColorTemperature: 'ColorTemperatureMireds',
|
|
575
|
+
CurrentTemperature: 'MeasuredValue',
|
|
576
|
+
CurrentRelativeHumidity: 'MeasuredValue',
|
|
577
|
+
ContactSensorState: 'StateValue',
|
|
578
|
+
MotionDetected: 'Occupancy',
|
|
579
|
+
LockCurrentState: 'LockState',
|
|
580
|
+
CurrentPosition: 'CurrentPositionLiftPercent100ths',
|
|
581
|
+
CurrentHeatingCoolingState: 'SystemMode',
|
|
582
|
+
RotationSpeed: 'PercentCurrent',
|
|
583
|
+
};
|
|
584
|
+
return mappings[charType] || 'Value';
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Get Matter server status
|
|
588
|
+
*/
|
|
589
|
+
getStatus() {
|
|
590
|
+
const status = {
|
|
591
|
+
enabled: this.isEnabled,
|
|
592
|
+
running: this.isStarted,
|
|
593
|
+
accessoryCount: this.publishedAccessories.size,
|
|
594
|
+
};
|
|
595
|
+
// Add commissioning info if server is running
|
|
596
|
+
if (this.isStarted && this.isEnabled) {
|
|
597
|
+
try {
|
|
598
|
+
const setupCode = `MT:${this.matterConfig.discriminator.toString().padStart(4, '0')}${this.matterConfig.passcode}`;
|
|
599
|
+
const qrCodeUrl = `https://dhrishi.github.io/connectedhomeip/qrcode.html?data=${setupCode}`;
|
|
600
|
+
return {
|
|
601
|
+
...status,
|
|
602
|
+
qrCode: qrCodeUrl,
|
|
603
|
+
setupCode,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
log.debug('Could not generate QR code for status:', error);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return status;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Get commissioning QR code for setup
|
|
614
|
+
*/
|
|
615
|
+
getCommissioningQRCode() {
|
|
616
|
+
if (!this.isEnabled || !this.isStarted) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
// Generate placeholder QR code
|
|
621
|
+
// In production, this would use real Matter.js QrCode.encode()
|
|
622
|
+
const setupCode = `MT:${this.matterConfig.discriminator.toString().padStart(4, '0')}${this.matterConfig.passcode}`;
|
|
623
|
+
return `https://dhrishi.github.io/connectedhomeip/qrcode.html?data=${setupCode}`;
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
log.error('Failed to generate commissioning QR code:', error);
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get manual pairing code for setup
|
|
632
|
+
*/
|
|
633
|
+
getManualPairingCode() {
|
|
634
|
+
if (!this.isEnabled || !this.isStarted) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
return this.formatPairingCode(this.matterConfig.passcode);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Get list of published accessories
|
|
641
|
+
*/
|
|
642
|
+
getPublishedAccessories() {
|
|
643
|
+
return Array.from(this.publishedAccessories.values()).map(item => item.accessory);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Check if an accessory is published via Matter
|
|
647
|
+
*/
|
|
648
|
+
isAccessoryPublished(accessory) {
|
|
649
|
+
return this.publishedAccessories.has(accessory.UUID);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get Matter configuration (read-only)
|
|
653
|
+
*/
|
|
654
|
+
getConfiguration() {
|
|
655
|
+
return { ...this.matterConfig };
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Force restart the Matter server (for configuration changes)
|
|
659
|
+
*/
|
|
660
|
+
async restart() {
|
|
661
|
+
if (!this.isEnabled) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
log.info('Restarting Matter server...');
|
|
665
|
+
try {
|
|
666
|
+
await this.stop();
|
|
667
|
+
await this.initialize();
|
|
668
|
+
await this.start();
|
|
669
|
+
log.info('Matter server restarted successfully');
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
log.error('Failed to restart Matter server:', error);
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
//# sourceMappingURL=matterService.js.map
|