homebridge 2.0.0-alpha.41 → 2.0.0-alpha.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/api.d.ts +47 -20
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +43 -11
  4. package/dist/api.js.map +1 -1
  5. package/dist/bridgeService.d.ts +5 -10
  6. package/dist/bridgeService.d.ts.map +1 -1
  7. package/dist/bridgeService.js +3 -0
  8. package/dist/bridgeService.js.map +1 -1
  9. package/dist/childBridgeFork.d.ts +19 -0
  10. package/dist/childBridgeFork.d.ts.map +1 -1
  11. package/dist/childBridgeFork.js +198 -4
  12. package/dist/childBridgeFork.js.map +1 -1
  13. package/dist/childBridgeService.d.ts +27 -1
  14. package/dist/childBridgeService.d.ts.map +1 -1
  15. package/dist/childBridgeService.js +43 -1
  16. package/dist/childBridgeService.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +3 -1
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.d.ts +5 -6
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/ipcService.d.ts +3 -1
  25. package/dist/ipcService.d.ts.map +1 -1
  26. package/dist/ipcService.js +2 -0
  27. package/dist/ipcService.js.map +1 -1
  28. package/dist/matter/index.d.ts +4 -6
  29. package/dist/matter/index.d.ts.map +1 -1
  30. package/dist/matter/index.js +4 -5
  31. package/dist/matter/index.js.map +1 -1
  32. package/dist/matter/matterConfigValidator.d.ts +2 -3
  33. package/dist/matter/matterConfigValidator.d.ts.map +1 -1
  34. package/dist/matter/matterConfigValidator.js +47 -38
  35. package/dist/matter/matterConfigValidator.js.map +1 -1
  36. package/dist/matter/matterErrorHandler.d.ts +6 -25
  37. package/dist/matter/matterErrorHandler.d.ts.map +1 -1
  38. package/dist/matter/matterErrorHandler.js +89 -99
  39. package/dist/matter/matterErrorHandler.js.map +1 -1
  40. package/dist/matter/matterServer.d.ts +125 -39
  41. package/dist/matter/matterServer.d.ts.map +1 -1
  42. package/dist/matter/matterServer.js +463 -223
  43. package/dist/matter/matterServer.js.map +1 -1
  44. package/dist/matter/matterSharedTypes.d.ts +1 -21
  45. package/dist/matter/matterSharedTypes.d.ts.map +1 -1
  46. package/dist/matter/matterSharedTypes.js +0 -4
  47. package/dist/matter/matterSharedTypes.js.map +1 -1
  48. package/dist/matter/matterStorage.d.ts +112 -0
  49. package/dist/matter/matterStorage.d.ts.map +1 -0
  50. package/dist/matter/matterStorage.js +355 -0
  51. package/dist/matter/matterStorage.js.map +1 -0
  52. package/dist/matter/matterTypes.d.ts +148 -20
  53. package/dist/matter/matterTypes.d.ts.map +1 -1
  54. package/dist/matter/matterTypes.js +91 -263
  55. package/dist/matter/matterTypes.js.map +1 -1
  56. package/dist/plugin.d.ts.map +1 -1
  57. package/dist/plugin.js +4 -2
  58. package/dist/plugin.js.map +1 -1
  59. package/dist/server.d.ts +12 -4
  60. package/dist/server.d.ts.map +1 -1
  61. package/dist/server.js +310 -332
  62. package/dist/server.js.map +1 -1
  63. package/dist/user.d.ts +1 -0
  64. package/dist/user.d.ts.map +1 -1
  65. package/dist/user.js +3 -0
  66. package/dist/user.js.map +1 -1
  67. package/package.json +4 -5
  68. package/dist/childMatterBridgeFork.d.ts +0 -108
  69. package/dist/childMatterBridgeFork.d.ts.map +0 -1
  70. package/dist/childMatterBridgeFork.js +0 -330
  71. package/dist/childMatterBridgeFork.js.map +0 -1
  72. package/dist/childMatterBridgeService.d.ts +0 -166
  73. package/dist/childMatterBridgeService.d.ts.map +0 -1
  74. package/dist/childMatterBridgeService.js +0 -623
  75. package/dist/childMatterBridgeService.js.map +0 -1
  76. package/dist/matter/matterBridge.d.ts +0 -64
  77. package/dist/matter/matterBridge.d.ts.map +0 -1
  78. package/dist/matter/matterBridge.js +0 -154
  79. package/dist/matter/matterBridge.js.map +0 -1
  80. package/dist/matter/matterDevice.d.ts +0 -107
  81. package/dist/matter/matterDevice.d.ts.map +0 -1
  82. package/dist/matter/matterDevice.js +0 -913
  83. package/dist/matter/matterDevice.js.map +0 -1
  84. package/dist/matter/portAllocator.d.ts +0 -85
  85. package/dist/matter/portAllocator.d.ts.map +0 -1
  86. package/dist/matter/portAllocator.js +0 -296
  87. package/dist/matter/portAllocator.js.map +0 -1
@@ -1,9 +1,8 @@
1
- /* global NodeJS */
2
1
  /**
3
- * Real Matter.js Server Implementation
2
+ * Matter.js Server Implementation for Homebridge Plugin API
4
3
  *
5
- * Complete Matter.js integration
6
- * and official Matter.js v0.15 documentation
4
+ * This provides a Matter bridge that plugins can use to register
5
+ * Matter accessories via the Homebridge API.
7
6
  */
8
7
  import * as crypto from 'node:crypto';
9
8
  import * as fs from 'node:fs';
@@ -18,61 +17,66 @@ import * as fse from 'fs-extra';
18
17
  import QRCode from 'qrcode-terminal';
19
18
  import { Logger } from '../logger.js';
20
19
  import getVersion from '../version.js';
21
- import { MatterDevice } from './matterDevice.js';
22
20
  import { diagnostics } from './matterDiagnostics.js';
23
21
  import { errorHandler } from './matterErrorHandler.js';
24
22
  import { networkMonitor } from './matterNetworkMonitor.js';
23
+ import { MatterStorageManager } from './matterStorage.js';
24
+ import { clusters, deviceTypes, MatterDeviceError, } from './matterTypes.js';
25
25
  const log = Logger.withPrefix('Matter');
26
+ // Constants for Matter server configuration
27
+ const DEFAULT_MATTER_PORT = 5540;
28
+ const DEFAULT_VENDOR_ID = 0xFFF1; // Test vendor ID from Matter spec
29
+ const DEFAULT_PRODUCT_ID = 0x8001; // Test product ID
30
+ const MAX_DEVICES_PER_BRIDGE = 1000; // Matter spec maximum devices per aggregator
31
+ const SERVER_READY_TIMEOUT_MS = 5000;
32
+ const SERVER_READY_POLL_INTERVAL_MS = 100;
33
+ const SERVER_INIT_DELAY_MS = 200;
34
+ const MAX_PASSCODE_ATTEMPTS = 100;
26
35
  /**
27
- * Real Matter.js Server for Homebridge
28
- * Creates a Matter bridge that exposes HAP accessories to Matter controllers
36
+ * Matter Server for Homebridge Plugin API
37
+ * Allows plugins to register Matter accessories explicitly
29
38
  */
30
39
  export class MatterServer {
31
40
  config;
32
41
  serverNode = null;
33
42
  aggregator = null;
34
- devices = new Map();
35
- removedDevices = new Set(); // Track removed devices for restart
43
+ accessories = new Map();
36
44
  isRunning = false;
37
- restartTimer = null;
38
- isRestarting = false;
39
- MAX_DEVICES = 1000; // Maximum number of devices
40
- MAX_REMOVED_DEVICES = 10; // Trigger restart after this many removals
41
- RESTART_DELAY_MS = 30000; // Delay before restarting server
45
+ MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
42
46
  shutdownHandler = null;
43
47
  // Internal commissioning values (generated, not user-configurable)
44
- passcode;
45
- discriminator;
48
+ passcode = 0;
49
+ discriminator = 0;
46
50
  vendorId;
47
51
  productId;
48
52
  commissioningInfo = {};
49
53
  serialNumber;
50
54
  cleanupHandlers = [];
55
+ storageManager = null;
51
56
  constructor(config = {}) {
52
57
  this.config = config;
53
58
  // Store the user config with defaults
54
59
  this.config = {
55
- port: config.port || 5540,
60
+ port: config.port || DEFAULT_MATTER_PORT,
56
61
  name: config.name || 'Homebridge Matter Bridge',
57
62
  // Use a consistent uniqueId based on the name to ensure storage persistence
58
63
  uniqueId: config.uniqueId || `homebridge-matter-${config.name?.replace(/[^a-z0-9]/gi, '-') || 'bridge'}`,
59
64
  storagePath: config.storagePath,
60
- mdnsInterface: config.mdnsInterface,
61
- ipv4: config.ipv4 !== false, // Default to true
62
- ipv6: config.ipv6 !== false, // Default to true
63
65
  };
64
- // Generate internal commissioning values
65
- this.passcode = this.generateSecurePasscode();
66
- this.discriminator = this.generateRandomDiscriminator();
67
- this.vendorId = 0xFFF1; // Test vendor ID
68
- this.productId = 0x8001; // Test product ID
66
+ // Initialize commissioning values (will be loaded from storage in start())
67
+ this.vendorId = DEFAULT_VENDOR_ID;
68
+ this.productId = DEFAULT_PRODUCT_ID;
69
69
  }
70
70
  /**
71
71
  * Generate a secure random passcode
72
+ * According to Matter spec, passcode must be:
73
+ * - 8 digits (00000001 to 99999998)
74
+ * - Not in the invalid list
75
+ * - Not sequential or repeating patterns
72
76
  */
73
77
  generateSecurePasscode() {
74
78
  let passcode;
75
- const maxAttempts = 100;
79
+ const maxAttempts = MAX_PASSCODE_ATTEMPTS;
76
80
  let attempts = 0;
77
81
  const invalidPasscodes = [
78
82
  0,
@@ -92,22 +96,137 @@ export class MatterServer {
92
96
  // Use cryptographically secure random number generation
93
97
  const randomBytes = crypto.randomBytes(4);
94
98
  const randomValue = randomBytes.readUInt32BE(0);
99
+ // Generate a value between 1 and 99999998
95
100
  passcode = (randomValue % 99999998) + 1;
96
101
  attempts++;
97
102
  if (attempts > maxAttempts) {
98
103
  throw new Error('Failed to generate secure passcode after maximum attempts');
99
104
  }
100
105
  } while (invalidPasscodes.includes(passcode)
101
- || passcode.toString().padStart(8, '0').length !== 8);
106
+ || !this.isValidPasscode(passcode));
102
107
  return passcode;
103
108
  }
109
+ /**
110
+ * Validate a passcode according to Matter specifications
111
+ */
112
+ isValidPasscode(passcode) {
113
+ // Must be between 1 and 99999998
114
+ if (passcode < 1 || passcode > 99999998) {
115
+ return false;
116
+ }
117
+ // Convert to 8-digit string
118
+ const passcodeStr = passcode.toString().padStart(8, '0');
119
+ // Check for sequential patterns (12345678, 23456789, etc.)
120
+ let isSequential = true;
121
+ for (let i = 1; i < passcodeStr.length; i++) {
122
+ if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
123
+ isSequential = false;
124
+ break;
125
+ }
126
+ }
127
+ if (isSequential) {
128
+ return false;
129
+ }
130
+ // Check for reverse sequential (87654321, 76543210, etc.)
131
+ let isReverseSequential = true;
132
+ for (let i = 1; i < passcodeStr.length; i++) {
133
+ if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
134
+ isReverseSequential = false;
135
+ break;
136
+ }
137
+ }
138
+ if (isReverseSequential) {
139
+ return false;
140
+ }
141
+ // Check for too many repeating digits (more than 3 of same digit)
142
+ const digitCounts = new Map();
143
+ for (const digit of passcodeStr) {
144
+ digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
145
+ if (digitCounts.get(digit) > 3) {
146
+ return false;
147
+ }
148
+ }
149
+ return true;
150
+ }
104
151
  /**
105
152
  * Generate a random discriminator
153
+ * According to Matter spec, discriminator must be:
154
+ * - 12 bits (0-4095)
155
+ * - Should be random for security
106
156
  */
107
157
  generateRandomDiscriminator() {
108
158
  // Generate cryptographically secure random 12-bit discriminator (0-4095)
109
159
  const randomBytes = crypto.randomBytes(2);
110
- return randomBytes.readUInt16BE(0) & 0x0FFF; // Mask to 12 bits
160
+ const discriminator = randomBytes.readUInt16BE(0) & 0x0FFF; // Mask to 12 bits
161
+ // Validate discriminator range
162
+ if (discriminator < 0 || discriminator > 4095) {
163
+ throw new Error(`Invalid discriminator generated: ${discriminator}`);
164
+ }
165
+ return discriminator;
166
+ }
167
+ /**
168
+ * Create ServerNode with automatic recovery from corrupted storage
169
+ * If ServerNode creation fails due to corrupted fabric data, automatically
170
+ * clean up the ServerNodeStore and retry once
171
+ */
172
+ async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
173
+ try {
174
+ // First attempt to create ServerNode
175
+ return await ServerNode.create(nodeOptions);
176
+ }
177
+ catch (error) {
178
+ // Check if this is a storage corruption error
179
+ const isStorageError = error?.message?.includes('Invalid public key encoding')
180
+ || error?.message?.includes('FabricManager unavailable')
181
+ || error?.message?.includes('key-input')
182
+ || error?.cause?.message?.includes('Invalid public key encoding');
183
+ if (!isStorageError) {
184
+ // Not a storage error, rethrow
185
+ throw error;
186
+ }
187
+ // Storage is corrupted - clean up and retry
188
+ log.warn('Detected corrupted Matter storage, attempting automatic recovery...');
189
+ // The ServerNodeStore directory is inside our storage path with the same name as the bridge ID
190
+ const environment = Environment.default;
191
+ const storageService = environment.get(StorageService);
192
+ const storageLocation = storageService.location;
193
+ if (!storageLocation) {
194
+ throw new Error('Storage location not set, cannot recover from corrupted storage');
195
+ }
196
+ const serverNodeStorePath = path.join(storageLocation, sanitizedId);
197
+ const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
198
+ try {
199
+ let removedSomething = false;
200
+ // Delete the ServerNodeStore subdirectory
201
+ if (fs.existsSync(serverNodeStorePath)) {
202
+ log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
203
+ await fse.remove(serverNodeStorePath);
204
+ removedSomething = true;
205
+ }
206
+ // Delete the ServerNodeStore JSON file (contains fabric data)
207
+ if (fs.existsSync(serverNodeStoreJsonFile)) {
208
+ log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
209
+ await fse.remove(serverNodeStoreJsonFile);
210
+ removedSomething = true;
211
+ }
212
+ if (removedSomething) {
213
+ log.info('Corrupted storage removed, retrying ServerNode creation...');
214
+ }
215
+ else {
216
+ log.warn('No corrupted storage files found, corruption may be elsewhere');
217
+ }
218
+ // Retry ServerNode creation
219
+ const serverNode = await ServerNode.create(nodeOptions);
220
+ log.info('Successfully recovered from corrupted Matter storage');
221
+ return serverNode;
222
+ }
223
+ catch (retryError) {
224
+ log.error('Failed to recover from corrupted storage:', retryError);
225
+ log.error('Original error:', error);
226
+ throw new Error('Matter storage is corrupted and automatic recovery failed. '
227
+ + `Please manually delete: ${serverNodeStorePath}`);
228
+ }
229
+ }
111
230
  }
112
231
  /**
113
232
  * Start the Matter server
@@ -119,9 +238,12 @@ export class MatterServer {
119
238
  }
120
239
  try {
121
240
  log.info('Starting Matter.js server...');
122
- log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
123
- // Validate and set up storage path
241
+ // IMPORTANT: Storage must be configured BEFORE any Matter.js operations
242
+ // This ensures persistent fabric data across restarts
124
243
  await this.setupStorage();
244
+ // Load or generate commissioning credentials
245
+ await this.loadOrGenerateCredentials();
246
+ log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
125
247
  // Start network monitoring
126
248
  networkMonitor.startMonitoring();
127
249
  this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
@@ -133,43 +255,43 @@ export class MatterServer {
133
255
  passcode: this.passcode,
134
256
  discriminator: this.discriminator,
135
257
  };
258
+ log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
136
259
  // Ensure we have a name for the bridge
137
260
  const bridgeName = this.config.name || 'Homebridge Matter Bridge';
261
+ // Sanitize the uniqueId to ensure it's filesystem-safe
262
+ // Replace any characters that could cause issues (colons, slashes, etc.)
263
+ // Use only alphanumeric and hyphens, collapse multiple hyphens, trim leading/trailing hyphens
264
+ const sanitizedId = this.config.uniqueId.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
138
265
  // Create node options with proper typing
139
266
  const nodeOptions = {
140
- id: this.config.uniqueId,
267
+ id: sanitizedId,
141
268
  network: {
142
269
  port: this.config.port,
143
- ipv4: this.config.ipv4,
144
- ipv6: this.config.ipv6,
145
- mdnsInterface: this.config.mdnsInterface,
270
+ ipv4: true, // Always enable IPv4 for Matter
146
271
  },
147
272
  commissioning: commissioningOptions,
148
273
  productDescription: {
149
- name: bridgeName, // This should be the user-visible name
274
+ name: bridgeName,
150
275
  deviceType: AggregatorEndpoint.deviceType,
151
276
  },
152
277
  basicInformation: {
153
- // Try setting nodeLabel to the bridge name instead of product name
154
278
  nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
155
279
  vendorId: VendorId(this.vendorId),
156
280
  vendorName: 'Homebridge'.slice(0, 32),
157
281
  productId: this.productId,
158
282
  productName: 'Homebridge Matter Bridge'.slice(0, 32),
159
- // Set productLabel to bridge name as well
160
283
  productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
161
284
  serialNumber: this.serialNumber = this.generateSerialNumber(),
162
285
  hardwareVersion: 1,
163
- hardwareVersionString: os.release(), // Hardware version
286
+ hardwareVersionString: os.release(),
164
287
  softwareVersion: 1,
165
- softwareVersionString: getVersion(), // Shows as "Firmware" in Home app
288
+ softwareVersionString: getVersion(),
166
289
  reachable: true,
167
290
  },
168
291
  };
169
- // Create server node with proper configuration
170
- this.serverNode = await ServerNode.create(nodeOptions);
292
+ // Create server node with automatic recovery from corrupted storage
293
+ this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId);
171
294
  // Create aggregator endpoint for bridge pattern
172
- // The bridge name is set via the ServerNode's basicInformation
173
295
  this.aggregator = new Endpoint(AggregatorEndpoint, {
174
296
  id: 'homebridge-aggregator',
175
297
  });
@@ -195,8 +317,8 @@ export class MatterServer {
195
317
  // Wait for server to be ready
196
318
  await this.waitForServerReady();
197
319
  this.isRunning = true;
198
- log.info(`✅ Matter server started successfully on port ${this.config.port}`);
199
- log.info('Homebridge accessories can now be added to Matter controllers');
320
+ log.info(`Matter server started successfully on port ${this.config.port}`);
321
+ log.info('Plugins can now register Matter accessories via the API');
200
322
  }
201
323
  catch (error) {
202
324
  log.error('Failed to start Matter server:', error);
@@ -232,15 +354,69 @@ export class MatterServer {
232
354
  catch (error) {
233
355
  throw new Error(`Storage path not accessible: ${error}`);
234
356
  }
235
- // Create Matter-specific storage directory with bridge-specific subfolder
236
- const bridgeId = this.config.uniqueId?.replace(/[^a-z0-9-]/gi, '_') || 'default';
237
- const matterStoragePath = path.join(normalizedPath, '.matter', bridgeId);
357
+ // Create bridge-specific storage directory
358
+ // Use only alphanumeric characters and hyphens for maximum compatibility
359
+ const bridgeId = this.config.uniqueId?.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'default';
360
+ const matterStoragePath = path.join(normalizedPath, bridgeId);
238
361
  await fse.ensureDir(matterStoragePath);
239
- // Configure environment to use our storage
362
+ // Create storage manager
363
+ this.storageManager = new MatterStorageManager(matterStoragePath);
364
+ // Configure environment to use our custom storage
240
365
  const environment = Environment.default;
241
366
  const storageService = environment.get(StorageService);
242
367
  storageService.location = matterStoragePath;
243
- log.info(`✅ Matter storage initialized at: ${matterStoragePath}`);
368
+ // CRITICAL: Override storage factory with custom implementation
369
+ // This ensures fabric data is properly persisted
370
+ storageService.factory = (namespace) => {
371
+ if (!this.storageManager) {
372
+ throw new Error('Storage manager not initialized');
373
+ }
374
+ const storage = this.storageManager.getStorage(namespace);
375
+ // Initialize asynchronously - Matter.js handles async storage properly
376
+ storage.initialize().catch((error) => {
377
+ log.error(`Failed to initialize storage namespace ${namespace}:`, error);
378
+ });
379
+ // Note: Cast to unknown first to satisfy TypeScript - our storage implements the required interface
380
+ return storage;
381
+ };
382
+ // Add cleanup handler for storage
383
+ this.cleanupHandlers.push(async () => {
384
+ if (this.storageManager) {
385
+ await this.storageManager.closeAll();
386
+ }
387
+ });
388
+ log.info(`Matter storage initialized at: ${matterStoragePath}`);
389
+ }
390
+ /**
391
+ * Load or generate commissioning credentials (passcode and discriminator)
392
+ * These must be persistent across restarts to maintain the same QR code
393
+ */
394
+ async loadOrGenerateCredentials() {
395
+ if (!this.storageManager) {
396
+ throw new Error('Storage manager not initialized');
397
+ }
398
+ const storage = this.storageManager.getStorage('commissioning');
399
+ // CRITICAL: Initialize storage before reading to avoid race condition
400
+ await storage.initialize();
401
+ // Try to load existing credentials
402
+ const storedPasscode = storage.get([], 'passcode');
403
+ const storedDiscriminator = storage.get([], 'discriminator');
404
+ if (storedPasscode && storedDiscriminator) {
405
+ // Use stored credentials
406
+ log.info('Loading existing commissioning credentials from storage');
407
+ this.passcode = storedPasscode;
408
+ this.discriminator = storedDiscriminator;
409
+ }
410
+ else {
411
+ // Generate new credentials and store them
412
+ log.info('Generating new commissioning credentials');
413
+ this.passcode = this.generateSecurePasscode();
414
+ this.discriminator = this.generateRandomDiscriminator();
415
+ // Store for future use
416
+ storage.set([], 'passcode', this.passcode);
417
+ storage.set([], 'discriminator', this.discriminator);
418
+ log.info('Commissioning credentials saved to storage');
419
+ }
244
420
  }
245
421
  /**
246
422
  * Generate serial number for the bridge
@@ -259,14 +435,13 @@ export class MatterServer {
259
435
  const vendorId = this.vendorId;
260
436
  const productId = this.productId;
261
437
  // Use Matter.js library to generate pairing codes properly
262
- // Generate 11-digit code (without vendor/product IDs) for better compatibility
263
438
  const manualCode = ManualPairingCodeCodec.encode({
264
439
  discriminator,
265
440
  passcode: this.passcode,
266
- // Omit vendorId and productId to generate 11-digit code instead of 21-digit
267
441
  });
268
442
  // Format as XXXX-XXX-XXXX for display
269
443
  const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`;
444
+ log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`);
270
445
  const qrCodePayload = QrPairingCodeCodec.encode([{
271
446
  version: 0,
272
447
  vendorId,
@@ -276,6 +451,8 @@ export class MatterServer {
276
451
  discriminator,
277
452
  passcode: this.passcode,
278
453
  }]);
454
+ log.info(`Generated QR code: ${qrCodePayload}`);
455
+ log.info(`Generated manual code: ${manualPairingCode}`);
279
456
  // Store commissioning info
280
457
  this.commissioningInfo = {
281
458
  qrCode: qrCodePayload,
@@ -299,220 +476,216 @@ export class MatterServer {
299
476
  /**
300
477
  * Wait for the server to be ready
301
478
  */
302
- async waitForServerReady(maxWaitTime = 5000) {
479
+ async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS) {
303
480
  const startTime = Date.now();
304
481
  while (!this.serverNode || !this.aggregator) {
305
482
  if (Date.now() - startTime > maxWaitTime) {
306
483
  throw new Error('Server failed to become ready within timeout');
307
484
  }
308
- await new Promise(resolve => setTimeout(resolve, 100));
485
+ await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS));
309
486
  }
310
487
  // Additional small delay to ensure everything is initialized
311
- await new Promise(resolve => setTimeout(resolve, 200));
488
+ await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
312
489
  }
313
490
  /**
314
- * Add a Homebridge accessory as a bridged Matter device
491
+ * Register a Matter accessory (Plugin API)
315
492
  */
316
- async addAccessory(accessory) {
493
+ async registerAccessory(accessory) {
317
494
  if (!this.serverNode || !this.aggregator) {
318
- log.error('Matter server not started - cannot add accessory');
319
- return null;
495
+ throw new MatterDeviceError('Matter server not started');
320
496
  }
321
- // Check device limit
322
- if (this.devices.size >= this.MAX_DEVICES) {
323
- log.error(`Device limit reached (${this.MAX_DEVICES}), cannot add ${accessory.displayName}`);
324
- return null;
497
+ // Validate required fields
498
+ if (!accessory.deviceType) {
499
+ throw new MatterDeviceError(`Missing deviceType for accessory "${accessory.displayName}". `
500
+ + 'Make sure you are using api.matterDeviceTypes (e.g., api.matterDeviceTypes.OnOffLight)');
501
+ }
502
+ if (!accessory.uuid) {
503
+ throw new MatterDeviceError('Accessory must have a uuid');
325
504
  }
326
- if (this.devices.has(accessory.UUID)) {
327
- log.debug(`Accessory ${accessory.displayName} already exists as Matter device`);
328
- return this.devices.get(accessory.UUID) || null;
505
+ if (!accessory.displayName) {
506
+ throw new MatterDeviceError('Accessory must have a displayName');
507
+ }
508
+ if (!accessory.serialNumber) {
509
+ throw new MatterDeviceError(`Accessory "${accessory.displayName}" must have a serialNumber`);
510
+ }
511
+ if (!accessory.manufacturer) {
512
+ throw new MatterDeviceError(`Accessory "${accessory.displayName}" must have a manufacturer`);
513
+ }
514
+ if (!accessory.model) {
515
+ throw new MatterDeviceError(`Accessory "${accessory.displayName}" must have a model`);
516
+ }
517
+ if (!accessory.clusters || typeof accessory.clusters !== 'object') {
518
+ throw new MatterDeviceError(`Accessory "${accessory.displayName}" must have clusters defined`);
519
+ }
520
+ // Check if already registered
521
+ if (this.accessories.has(accessory.uuid)) {
522
+ throw new MatterDeviceError(`Accessory with UUID ${accessory.uuid} is already registered`);
523
+ }
524
+ // Check device limit
525
+ if (this.accessories.size >= this.MAX_DEVICES) {
526
+ throw new MatterDeviceError(`Maximum device limit (${this.MAX_DEVICES}) reached`);
329
527
  }
330
528
  try {
331
- log.info(`Adding Matter device: ${accessory.displayName}`);
332
- // Create Matter device from HAP accessory
333
- const matterDevice = new MatterDevice(accessory);
334
- const endpoint = await matterDevice.createEndpoint();
335
- if (!endpoint) {
336
- log.warn(`Could not create Matter endpoint for: ${accessory.displayName}`);
337
- return null;
338
- }
339
- // Add to aggregator (bridge)
340
- log.debug(`Adding endpoint to aggregator for ${accessory.displayName} (UUID: ${accessory.UUID})`);
529
+ // Create endpoint with device type
530
+ const endpoint = new Endpoint(accessory.deviceType, {
531
+ id: accessory.uuid,
532
+ });
533
+ // Add to aggregator FIRST (required before we can configure it)
341
534
  await this.aggregator.add(endpoint);
342
- // Store for management
343
- this.devices.set(accessory.UUID, matterDevice);
344
- // Set up bidirectional sync
345
- matterDevice.startSync();
346
- log.info(`Added Matter device: ${accessory.displayName} (${matterDevice.getDeviceType()})`);
347
- return matterDevice;
535
+ // NOW configure the endpoint
536
+ await this.configureEndpoint(endpoint, accessory);
537
+ // Store accessory
538
+ const internalAccessory = {
539
+ ...accessory,
540
+ endpoint,
541
+ registered: true,
542
+ };
543
+ this.accessories.set(accessory.uuid, internalAccessory);
544
+ log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
348
545
  }
349
546
  catch (error) {
350
- log.error(`Failed to add Matter device for ${accessory.displayName}:`, error);
351
- await errorHandler.handleError(error, 'add-accessory');
352
- return null;
547
+ log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
548
+ throw new MatterDeviceError(`Failed to register accessory: ${error}`);
353
549
  }
354
550
  }
355
551
  /**
356
- * Remove a Matter device
552
+ * Unregister a Matter accessory (Plugin API)
357
553
  */
358
- async removeAccessory(accessory) {
359
- const device = this.devices.get(accessory.UUID);
360
- if (!device) {
361
- log.debug(`No Matter device found for: ${accessory.displayName}`);
554
+ async unregisterAccessory(uuid) {
555
+ const accessory = this.accessories.get(uuid);
556
+ if (!accessory) {
557
+ log.debug(`Accessory ${uuid} not found, ignoring unregister request`);
362
558
  return;
363
559
  }
364
560
  try {
365
- // Stop sync
366
- device.stopSync();
367
- // Get endpoint to remove
368
- const endpoint = await device.getEndpoint();
369
- if (endpoint && this.aggregator) {
370
- // Remove from aggregator
371
- // Note: Matter.js v0.15 doesn't provide a direct API to remove endpoints
372
- // from an aggregator after they're added. This is a limitation of the current API.
373
- // The endpoint will be cleaned up when the server stops.
374
- log.debug('Endpoint marked for removal - will be cleaned up on server restart');
375
- // Track removed devices and schedule restart if threshold reached
376
- this.removedDevices.add(accessory.UUID);
377
- if (this.removedDevices.size >= this.MAX_REMOVED_DEVICES) {
378
- log.warn(`Reached ${this.MAX_REMOVED_DEVICES} removed devices - scheduling Matter server restart in 30 seconds`);
379
- this.scheduleRestart();
380
- }
561
+ if (accessory.endpoint && this.aggregator) {
562
+ await accessory.endpoint.close();
563
+ log.debug(`Removed endpoint for ${accessory.displayName}`);
381
564
  }
382
- // Clean up device
383
- await device.destroy();
384
- this.devices.delete(accessory.UUID);
385
- log.info(`Removed Matter device: ${accessory.displayName}`);
565
+ this.accessories.delete(uuid);
566
+ log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
386
567
  }
387
568
  catch (error) {
388
- log.error(`Failed to remove Matter device for ${accessory.displayName}:`, error);
389
- await errorHandler.handleError(error, 'remove-accessory');
569
+ log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
570
+ throw new MatterDeviceError(`Failed to unregister accessory: ${error}`);
390
571
  }
391
572
  }
392
573
  /**
393
- * Schedule a server restart to clean up removed endpoints
574
+ * Update a Matter accessory's state (Plugin API)
394
575
  */
395
- scheduleRestart() {
396
- // Prevent scheduling if already restarting
397
- if (this.isRestarting) {
398
- log.debug('Restart already in progress, skipping schedule');
399
- return;
400
- }
401
- // Cancel any existing restart timer
402
- if (this.restartTimer) {
403
- clearTimeout(this.restartTimer);
576
+ async updateAccessoryState(uuid, cluster, attributes) {
577
+ const accessory = this.accessories.get(uuid);
578
+ if (!accessory || !accessory.endpoint) {
579
+ throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
404
580
  }
405
- this.restartTimer = setTimeout(async () => {
406
- // Check again in case state changed
407
- if (this.isRestarting) {
408
- log.debug('Restart already in progress, skipping');
409
- return;
410
- }
411
- this.isRestarting = true;
412
- log.info('Restarting Matter server to clean up removed endpoints...');
413
- try {
414
- // Save current devices
415
- const currentDevices = Array.from(this.devices.values());
416
- // Stop server
417
- await this.stop();
418
- // Clear removed devices tracking
419
- this.removedDevices.clear();
420
- // Wait a moment
421
- await new Promise(resolve => setTimeout(resolve, 2000));
422
- // Restart server
423
- await this.start();
424
- // Re-add current devices
425
- for (const device of currentDevices) {
426
- const accessory = device.getAccessory();
427
- if (accessory) {
428
- await this.addAccessory(accessory);
429
- }
430
- }
431
- log.info('Matter server restarted successfully');
432
- }
433
- catch (error) {
434
- log.error('Failed to restart Matter server:', error);
581
+ try {
582
+ // Update the endpoint's cluster state
583
+ // Note: Endpoint types from Matter.js don't expose state properly, needs runtime check
584
+ const endpoint = accessory.endpoint;
585
+ if (endpoint.state?.[cluster]) {
586
+ Object.assign(endpoint.state[cluster], attributes);
587
+ log.debug(`Updated ${cluster} state for ${accessory.displayName}:`, attributes);
435
588
  }
436
- finally {
437
- this.isRestarting = false;
438
- this.restartTimer = null;
589
+ else {
590
+ log.warn(`Cluster ${cluster} not found on accessory ${accessory.displayName}`);
439
591
  }
440
- }, this.RESTART_DELAY_MS);
592
+ }
593
+ catch (error) {
594
+ log.error(`Failed to update state for accessory ${uuid}:`, error);
595
+ throw new MatterDeviceError(`Failed to update accessory state: ${error}`);
596
+ }
441
597
  }
442
598
  /**
443
- * Get server information
599
+ * Get all registered accessories (Plugin API)
444
600
  */
445
- getInfo() {
446
- // Record diagnostics info (async, fire and forget)
447
- diagnostics.collectDiagnostics({
448
- enabled: true,
449
- initialized: this.serverNode !== null,
450
- running: this.isRunning,
451
- port: this.config.port,
452
- deviceCount: this.devices.size,
453
- bridgeCount: 1,
454
- }).catch(error => log.debug('Failed to collect diagnostics:', error));
455
- return {
456
- running: this.isRunning,
457
- port: this.config.port,
458
- deviceCount: this.devices.size,
459
- config: this.config,
460
- serialNumber: this.serialNumber,
461
- commissioned: this.isCommissioned(),
462
- };
601
+ getAccessories() {
602
+ return Array.from(this.accessories.values()).map((acc) => {
603
+ // Return copy without internal fields
604
+ // eslint-disable-next-line unused-imports/no-unused-vars
605
+ const { endpoint, registered, ...publicAccessory } = acc;
606
+ return publicAccessory;
607
+ });
463
608
  }
464
609
  /**
465
- * Get commissioning information
610
+ * Get a specific accessory by UUID (Plugin API)
466
611
  */
467
- getCommissioningInfo() {
468
- return {
469
- ...this.commissioningInfo,
470
- commissioned: this.isCommissioned(),
471
- };
612
+ getAccessory(uuid) {
613
+ const accessory = this.accessories.get(uuid);
614
+ if (!accessory) {
615
+ return undefined;
616
+ }
617
+ // Return copy without internal fields
618
+ // eslint-disable-next-line unused-imports/no-unused-vars
619
+ const { endpoint, registered, ...publicAccessory } = accessory;
620
+ return publicAccessory;
472
621
  }
473
622
  /**
474
- * Check if the server is commissioned
623
+ * Configure a Matter endpoint after it's been added to the aggregator
475
624
  */
476
- isCommissioned() {
477
- // Type-safe check for commissioning state
478
- try {
479
- const serverState = this.serverNode;
480
- return serverState?.state?.commissioning?.commissioned === true;
625
+ async configureEndpoint(endpoint, accessory) {
626
+ // Note: bridgedDeviceBasicInformation is not available in Matter.js v0.15.4
627
+ // The BridgedDeviceBasicInformation cluster is automatically added to bridged devices
628
+ // but cannot be configured via endpoint.set() in this version
629
+ // Leaving this commented out to avoid error logs
630
+ // try {
631
+ // await endpoint.set({
632
+ // bridgedDeviceBasicInformation: {
633
+ // nodeLabel: accessory.displayName.slice(0, 32),
634
+ // vendorName: accessory.manufacturer.slice(0, 32),
635
+ // vendorId: VendorId(this.vendorId),
636
+ // productName: accessory.model.slice(0, 32),
637
+ // productLabel: accessory.displayName.slice(0, 64),
638
+ // serialNumber: accessory.serialNumber,
639
+ // reachable: true,
640
+ // ...(accessory.hardwareRevision && { hardwareVersionString: accessory.hardwareRevision }),
641
+ // ...(accessory.softwareVersion && { softwareVersionString: accessory.softwareVersion }),
642
+ // },
643
+ // } as any)
644
+ // } catch (error) {
645
+ // log.debug(`Could not set bridgedDeviceBasicInformation for ${accessory.displayName}: ${error}`)
646
+ // }
647
+ // Set up cluster states
648
+ for (const [clusterName, attributes] of Object.entries(accessory.clusters)) {
649
+ // Cast to any temporarily to work around Matter.js type limitations
650
+ await endpoint.set({ [clusterName]: attributes });
481
651
  }
482
- catch {
483
- return false;
652
+ // Set up command handlers if provided
653
+ if (accessory.handlers) {
654
+ for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
655
+ for (const [commandName, handler] of Object.entries(handlers)) {
656
+ const endpointWithEvents = endpoint;
657
+ if (endpointWithEvents.events?.[clusterName]?.[`${commandName}$Action`]) {
658
+ endpointWithEvents.events[clusterName][`${commandName}$Action`].on(handler);
659
+ }
660
+ }
661
+ }
484
662
  }
485
663
  }
486
- /**
487
- * Get all Matter devices
488
- */
489
- getDevices() {
490
- return this.devices;
491
- }
492
664
  /**
493
665
  * Stop the Matter server
494
666
  */
495
667
  async stop() {
496
668
  if (!this.isRunning) {
669
+ log.debug('Matter server is not running');
497
670
  return;
498
671
  }
499
- log.info('Stopping Matter server...');
672
+ this.isRunning = false;
500
673
  // Stop monitoring
501
674
  networkMonitor.stopMonitoring();
502
675
  diagnostics.stopDiagnostics();
503
676
  try {
504
- // Clean up all devices
505
- for (const device of this.devices.values()) {
677
+ // Clean up all accessories
678
+ for (const accessory of this.accessories.values()) {
506
679
  try {
507
- device.stopSync();
508
- await device.destroy();
680
+ if (accessory.endpoint) {
681
+ await accessory.endpoint.close();
682
+ }
509
683
  }
510
684
  catch (error) {
511
- log.error('Failed to clean up device:', error);
512
- await errorHandler.handleError(error, 'device-cleanup');
685
+ log.error('Failed to clean up accessory:', error);
513
686
  }
514
687
  }
515
- this.devices.clear();
688
+ this.accessories.clear();
516
689
  // Stop server
517
690
  if (this.serverNode) {
518
691
  await this.serverNode.close();
@@ -549,11 +722,6 @@ export class MatterServer {
549
722
  }
550
723
  }
551
724
  this.cleanupHandlers = [];
552
- // Cancel any pending restart
553
- if (this.restartTimer) {
554
- clearTimeout(this.restartTimer);
555
- this.restartTimer = null;
556
- }
557
725
  // Clear references
558
726
  this.serverNode = null;
559
727
  this.aggregator = null;
@@ -561,24 +729,96 @@ export class MatterServer {
561
729
  this.commissioningInfo = {};
562
730
  }
563
731
  /**
564
- * Clean up all devices
732
+ * Get fabric information for commissioned controllers
565
733
  */
566
- async cleanupAllDevices() {
567
- const cleanupPromises = [];
568
- for (const [uuid, device] of this.devices.entries()) {
569
- cleanupPromises.push((async () => {
570
- try {
571
- log.debug(`Cleaning up device: ${uuid}`);
572
- device.stopSync();
573
- await device.destroy();
574
- }
575
- catch (error) {
576
- log.error(`Failed to clean up device ${uuid}:`, error);
577
- }
578
- })());
734
+ getFabricInfo() {
735
+ try {
736
+ if (!this.serverNode) {
737
+ return [];
738
+ }
739
+ const serverState = this.serverNode;
740
+ const fabrics = serverState?.state?.commissioning?.fabrics;
741
+ // Ensure fabrics is an array before mapping
742
+ if (!Array.isArray(fabrics)) {
743
+ return [];
744
+ }
745
+ return fabrics.map(fabric => ({
746
+ fabricIndex: fabric.fabricIndex,
747
+ fabricId: fabric.fabricId?.toString() || '',
748
+ nodeId: fabric.nodeId?.toString() || '',
749
+ rootVendorId: fabric.rootVendorId || 0,
750
+ label: fabric.label,
751
+ }));
752
+ }
753
+ catch (error) {
754
+ log.error('Failed to get fabric info:', error);
755
+ return [];
756
+ }
757
+ }
758
+ /**
759
+ * Check if the server is commissioned
760
+ */
761
+ isCommissioned() {
762
+ const fabrics = this.getFabricInfo();
763
+ return fabrics.length > 0;
764
+ }
765
+ /**
766
+ * Get the number of commissioned fabrics
767
+ */
768
+ getCommissionedFabricCount() {
769
+ return this.getFabricInfo().length;
770
+ }
771
+ /**
772
+ * Get server status information
773
+ */
774
+ getServerInfo() {
775
+ return {
776
+ running: this.isRunning,
777
+ port: this.config.port || 5540,
778
+ deviceCount: this.accessories.size,
779
+ commissioned: this.isCommissioned(),
780
+ fabricCount: this.getCommissionedFabricCount(),
781
+ serialNumber: this.serialNumber,
782
+ };
783
+ }
784
+ /**
785
+ * Get commissioning information
786
+ */
787
+ getCommissioningInfo() {
788
+ return {
789
+ ...this.commissioningInfo,
790
+ serialNumber: this.serialNumber,
791
+ passcode: this.passcode,
792
+ discriminator: this.discriminator,
793
+ commissioned: this.isCommissioned(),
794
+ };
795
+ }
796
+ /**
797
+ * Get storage statistics
798
+ */
799
+ getStorageStats() {
800
+ if (!this.storageManager) {
801
+ return null;
579
802
  }
580
- await Promise.all(cleanupPromises);
581
- this.devices.clear();
803
+ return this.storageManager.getAllStats();
804
+ }
805
+ /**
806
+ * Check if server is running
807
+ */
808
+ isServerRunning() {
809
+ return this.isRunning;
810
+ }
811
+ /**
812
+ * Get Matter device types available for plugin use
813
+ */
814
+ getDeviceTypes() {
815
+ return deviceTypes;
816
+ }
817
+ /**
818
+ * Get Matter clusters available for plugin use
819
+ */
820
+ getClusters() {
821
+ return clusters;
582
822
  }
583
823
  }
584
824
  //# sourceMappingURL=matterServer.js.map