homebridge 2.0.0-alpha.41 → 2.0.0-alpha.43

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