homebridge 2.0.0-alpha.43 → 2.0.0-alpha.45
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/dist/api.d.ts +37 -7
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +30 -4
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +5 -16
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +8 -24
- package/dist/bridgeService.js.map +1 -1
- package/dist/childBridgeFork.d.ts +3 -15
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +46 -181
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +20 -43
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +23 -66
- package/dist/childBridgeService.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ipcService.d.ts +23 -21
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js +0 -15
- package/dist/ipcService.js.map +1 -1
- package/dist/matter/index.d.ts +1 -1
- package/dist/matter/index.d.ts.map +1 -1
- package/dist/matter/index.js.map +1 -1
- package/dist/matter/matterAccessoryCache.d.ts +73 -0
- package/dist/matter/matterAccessoryCache.d.ts.map +1 -0
- package/dist/matter/matterAccessoryCache.js +168 -0
- package/dist/matter/matterAccessoryCache.js.map +1 -0
- package/dist/matter/matterBehaviors.d.ts +123 -0
- package/dist/matter/matterBehaviors.d.ts.map +1 -0
- package/dist/matter/matterBehaviors.js +582 -0
- package/dist/matter/matterBehaviors.js.map +1 -0
- package/dist/matter/matterConfigValidator.d.ts +0 -1
- package/dist/matter/matterConfigValidator.d.ts.map +1 -1
- package/dist/matter/matterConfigValidator.js +15 -45
- package/dist/matter/matterConfigValidator.js.map +1 -1
- package/dist/matter/matterErrorHandler.d.ts +1 -1
- package/dist/matter/matterErrorHandler.d.ts.map +1 -1
- package/dist/matter/matterErrorHandler.js +35 -22
- package/dist/matter/matterErrorHandler.js.map +1 -1
- package/dist/matter/matterNetworkMonitor.d.ts +3 -0
- package/dist/matter/matterNetworkMonitor.d.ts.map +1 -1
- package/dist/matter/matterNetworkMonitor.js +49 -26
- package/dist/matter/matterNetworkMonitor.js.map +1 -1
- package/dist/matter/matterServer.d.ts +79 -9
- package/dist/matter/matterServer.d.ts.map +1 -1
- package/dist/matter/matterServer.js +491 -111
- package/dist/matter/matterServer.js.map +1 -1
- package/dist/matter/matterSharedTypes.d.ts +36 -16
- package/dist/matter/matterSharedTypes.d.ts.map +1 -1
- package/dist/matter/matterSharedTypes.js +0 -3
- package/dist/matter/matterSharedTypes.js.map +1 -1
- package/dist/matter/matterStorage.d.ts +11 -1
- package/dist/matter/matterStorage.d.ts.map +1 -1
- package/dist/matter/matterStorage.js +12 -2
- package/dist/matter/matterStorage.js.map +1 -1
- package/dist/matter/matterTypes.d.ts +69 -20
- package/dist/matter/matterTypes.d.ts.map +1 -1
- package/dist/matter/matterTypes.js.map +1 -1
- package/dist/matter/matterValidation.d.ts +57 -0
- package/dist/matter/matterValidation.d.ts.map +1 -0
- package/dist/matter/matterValidation.js +97 -0
- package/dist/matter/matterValidation.js.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -4
- package/dist/plugin.js.map +1 -1
- package/dist/server.d.ts +0 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +97 -280
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/dist/bridgeTypes.d.ts +0 -54
- package/dist/bridgeTypes.d.ts.map +0 -1
- package/dist/bridgeTypes.js +0 -8
- package/dist/bridgeTypes.js.map +0 -1
- package/dist/matter/matterDiagnostics.d.ts +0 -121
- package/dist/matter/matterDiagnostics.d.ts.map +0 -1
- package/dist/matter/matterDiagnostics.js +0 -323
- package/dist/matter/matterDiagnostics.js.map +0 -1
|
@@ -5,23 +5,26 @@
|
|
|
5
5
|
* Matter accessories via the Homebridge API.
|
|
6
6
|
*/
|
|
7
7
|
import * as crypto from 'node:crypto';
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
8
9
|
import * as fs from 'node:fs';
|
|
9
10
|
import { access, writeFile } from 'node:fs/promises';
|
|
10
11
|
import * as os from 'node:os';
|
|
11
12
|
import * as path from 'node:path';
|
|
12
13
|
import process from 'node:process';
|
|
13
|
-
import { Endpoint, Environment, ServerNode, StorageService, VendorId
|
|
14
|
+
import { Endpoint, Environment, ServerNode, StorageService, VendorId } from '@matter/main';
|
|
14
15
|
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
15
|
-
import { ManualPairingCodeCodec, QrPairingCodeCodec
|
|
16
|
+
import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/types/schema';
|
|
16
17
|
import * as fse from 'fs-extra';
|
|
17
18
|
import QRCode from 'qrcode-terminal';
|
|
18
19
|
import { Logger } from '../logger.js';
|
|
19
20
|
import getVersion from '../version.js';
|
|
20
|
-
import {
|
|
21
|
+
import { MatterAccessoryCache } from './matterAccessoryCache.js';
|
|
22
|
+
import { HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer, registerHandler, setAccessoriesMap, } from './matterBehaviors.js';
|
|
21
23
|
import { errorHandler } from './matterErrorHandler.js';
|
|
22
24
|
import { networkMonitor } from './matterNetworkMonitor.js';
|
|
23
25
|
import { MatterStorageManager } from './matterStorage.js';
|
|
24
26
|
import { clusters, deviceTypes, MatterDeviceError, } from './matterTypes.js';
|
|
27
|
+
import { sanitizeUniqueId, truncateString, validatePort } from './matterValidation.js';
|
|
25
28
|
const log = Logger.withPrefix('Matter');
|
|
26
29
|
// Constants for Matter server configuration
|
|
27
30
|
const DEFAULT_MATTER_PORT = 5540;
|
|
@@ -32,11 +35,14 @@ const SERVER_READY_TIMEOUT_MS = 5000;
|
|
|
32
35
|
const SERVER_READY_POLL_INTERVAL_MS = 100;
|
|
33
36
|
const SERVER_INIT_DELAY_MS = 200;
|
|
34
37
|
const MAX_PASSCODE_ATTEMPTS = 100;
|
|
38
|
+
const FABRIC_MONITOR_INTERVAL_MS = 2000; // Check for fabric changes every 2 seconds
|
|
35
39
|
/**
|
|
36
40
|
* Matter Server for Homebridge Plugin API
|
|
37
41
|
* Allows plugins to register Matter accessories explicitly
|
|
42
|
+
*
|
|
43
|
+
* Extends EventEmitter to provide commissioning lifecycle events
|
|
38
44
|
*/
|
|
39
|
-
export class MatterServer {
|
|
45
|
+
export class MatterServer extends EventEmitter {
|
|
40
46
|
config;
|
|
41
47
|
serverNode = null;
|
|
42
48
|
aggregator = null;
|
|
@@ -54,19 +60,87 @@ export class MatterServer {
|
|
|
54
60
|
cleanupHandlers = [];
|
|
55
61
|
storageManager = null;
|
|
56
62
|
matterStoragePath;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
accessoryCache = null;
|
|
64
|
+
// Fabric monitoring
|
|
65
|
+
fabricMonitorInterval = null;
|
|
66
|
+
previousFabrics = new Map();
|
|
67
|
+
constructor(config) {
|
|
68
|
+
super();
|
|
69
|
+
// Store the validated config
|
|
70
|
+
this.config = this.validateAndSanitizeConfig(config);
|
|
71
|
+
// Enable debug logging if requested
|
|
72
|
+
if (this.config.debugModeEnabled) {
|
|
73
|
+
log.info('Matter debug mode enabled - verbose logging active');
|
|
74
|
+
}
|
|
67
75
|
// Initialize commissioning values (will be loaded from storage in start())
|
|
68
76
|
this.vendorId = DEFAULT_VENDOR_ID;
|
|
69
77
|
this.productId = DEFAULT_PRODUCT_ID;
|
|
78
|
+
// Provide accessories map reference to behaviors for cache syncing
|
|
79
|
+
setAccessoriesMap(this.accessories);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate and sanitize Matter server configuration
|
|
83
|
+
* Throws descriptive errors if configuration is invalid
|
|
84
|
+
*/
|
|
85
|
+
validateAndSanitizeConfig(config) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
// Validate port
|
|
88
|
+
const port = config.port || DEFAULT_MATTER_PORT;
|
|
89
|
+
const portValidation = validatePort(port, false);
|
|
90
|
+
if (!portValidation.valid) {
|
|
91
|
+
errors.push(`Invalid port: ${portValidation.error}`);
|
|
92
|
+
}
|
|
93
|
+
// Validate and sanitize uniqueId (REQUIRED)
|
|
94
|
+
if (!config.uniqueId) {
|
|
95
|
+
errors.push('uniqueId is required for Matter server configuration');
|
|
96
|
+
}
|
|
97
|
+
const rawUniqueId = config.uniqueId || '';
|
|
98
|
+
const uniqueIdResult = sanitizeUniqueId(rawUniqueId);
|
|
99
|
+
const uniqueId = uniqueIdResult.value;
|
|
100
|
+
if (uniqueId.length === 0) {
|
|
101
|
+
errors.push('Invalid uniqueId: must be a non-empty string');
|
|
102
|
+
}
|
|
103
|
+
// Validate storagePath (if provided)
|
|
104
|
+
let storagePath = config.storagePath;
|
|
105
|
+
if (storagePath !== undefined) {
|
|
106
|
+
storagePath = path.resolve(storagePath); // Resolve to absolute path
|
|
107
|
+
}
|
|
108
|
+
// Validate and sanitize manufacturer
|
|
109
|
+
let manufacturer = config.manufacturer;
|
|
110
|
+
if (manufacturer !== undefined) {
|
|
111
|
+
manufacturer = truncateString(manufacturer, 32, 'Manufacturer name').value;
|
|
112
|
+
}
|
|
113
|
+
// Validate and sanitize model
|
|
114
|
+
let model = config.model;
|
|
115
|
+
if (model !== undefined) {
|
|
116
|
+
model = truncateString(model, 32, 'Model name').value;
|
|
117
|
+
}
|
|
118
|
+
// Validate firmwareRevision
|
|
119
|
+
let firmwareRevision = config.firmwareRevision;
|
|
120
|
+
if (firmwareRevision !== undefined) {
|
|
121
|
+
firmwareRevision = truncateString(firmwareRevision, 64, 'Firmware revision').value;
|
|
122
|
+
}
|
|
123
|
+
// Validate serialNumber
|
|
124
|
+
let serialNumber = config.serialNumber;
|
|
125
|
+
if (serialNumber !== undefined) {
|
|
126
|
+
serialNumber = truncateString(serialNumber, 32, 'Serial number').value;
|
|
127
|
+
}
|
|
128
|
+
// Validate debugModeEnabled
|
|
129
|
+
const debugModeEnabled = config.debugModeEnabled || false;
|
|
130
|
+
// Throw if there are validation errors
|
|
131
|
+
if (errors.length > 0) {
|
|
132
|
+
throw new MatterDeviceError(`Matter configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
port,
|
|
136
|
+
uniqueId,
|
|
137
|
+
storagePath,
|
|
138
|
+
manufacturer,
|
|
139
|
+
model,
|
|
140
|
+
firmwareRevision,
|
|
141
|
+
serialNumber,
|
|
142
|
+
debugModeEnabled,
|
|
143
|
+
};
|
|
70
144
|
}
|
|
71
145
|
/**
|
|
72
146
|
* Generate a secure random passcode
|
|
@@ -167,8 +241,21 @@ export class MatterServer {
|
|
|
167
241
|
}
|
|
168
242
|
/**
|
|
169
243
|
* Create ServerNode with automatic recovery from corrupted storage
|
|
170
|
-
*
|
|
171
|
-
*
|
|
244
|
+
*
|
|
245
|
+
* Matter.js can fail to start if fabric data is corrupted (common after
|
|
246
|
+
* hard shutdowns or disk errors). This method implements automatic recovery by:
|
|
247
|
+
*
|
|
248
|
+
* 1. Attempting normal ServerNode creation
|
|
249
|
+
* 2. If it fails with storage errors, identifying and removing corrupted files
|
|
250
|
+
* 3. Retrying ServerNode creation with fresh storage
|
|
251
|
+
*
|
|
252
|
+
* This prevents the need for manual intervention while preserving data
|
|
253
|
+
* safety by only removing storage on confirmed corruption errors.
|
|
254
|
+
*
|
|
255
|
+
* @param nodeOptions - Matter.js ServerNode configuration
|
|
256
|
+
* @param sanitizedId - Filesystem-safe bridge identifier
|
|
257
|
+
* @returns Initialized ServerNode instance
|
|
258
|
+
* @throws Error if recovery fails or error is not storage-related
|
|
172
259
|
*/
|
|
173
260
|
async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
|
|
174
261
|
try {
|
|
@@ -248,21 +335,16 @@ export class MatterServer {
|
|
|
248
335
|
// Start network monitoring
|
|
249
336
|
networkMonitor.startMonitoring();
|
|
250
337
|
this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
|
|
251
|
-
// Start diagnostics
|
|
252
|
-
diagnostics.startDiagnostics();
|
|
253
|
-
this.cleanupHandlers.push(() => diagnostics.stopDiagnostics());
|
|
254
338
|
// Create commissioning options
|
|
255
339
|
const commissioningOptions = {
|
|
256
340
|
passcode: this.passcode,
|
|
257
341
|
discriminator: this.discriminator,
|
|
258
342
|
};
|
|
259
343
|
log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
|
|
260
|
-
//
|
|
261
|
-
const bridgeName =
|
|
262
|
-
//
|
|
263
|
-
|
|
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, '');
|
|
344
|
+
// Use default name for the Matter bridge
|
|
345
|
+
const bridgeName = 'Homebridge Matter Bridge';
|
|
346
|
+
// uniqueId is already sanitized in validateAndSanitizeConfig()
|
|
347
|
+
const sanitizedId = this.config.uniqueId;
|
|
266
348
|
// Create node options with proper typing
|
|
267
349
|
const nodeOptions = {
|
|
268
350
|
id: sanitizedId,
|
|
@@ -278,15 +360,15 @@ export class MatterServer {
|
|
|
278
360
|
basicInformation: {
|
|
279
361
|
nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
|
|
280
362
|
vendorId: VendorId(this.vendorId),
|
|
281
|
-
vendorName: 'Homebridge'.slice(0, 32),
|
|
363
|
+
vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
|
|
282
364
|
productId: this.productId,
|
|
283
|
-
productName: 'Homebridge Matter Bridge'.slice(0, 32),
|
|
365
|
+
productName: (this.config.model || 'Homebridge Matter Bridge').slice(0, 32),
|
|
284
366
|
productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
|
|
285
|
-
serialNumber: this.serialNumber = this.
|
|
367
|
+
serialNumber: this.serialNumber = this.config.serialNumber || this.config.uniqueId,
|
|
286
368
|
hardwareVersion: 1,
|
|
287
369
|
hardwareVersionString: os.release(),
|
|
288
370
|
softwareVersion: 1,
|
|
289
|
-
softwareVersionString: getVersion(),
|
|
371
|
+
softwareVersionString: this.config.firmwareRevision || getVersion(),
|
|
290
372
|
reachable: true,
|
|
291
373
|
},
|
|
292
374
|
};
|
|
@@ -317,6 +399,12 @@ export class MatterServer {
|
|
|
317
399
|
});
|
|
318
400
|
// Wait for server to be ready
|
|
319
401
|
await this.waitForServerReady();
|
|
402
|
+
// Load cached accessories (don't restore them yet - wait for plugins to re-register)
|
|
403
|
+
if (this.accessoryCache) {
|
|
404
|
+
await this.accessoryCache.load();
|
|
405
|
+
}
|
|
406
|
+
// Start fabric monitoring to emit commissioning events
|
|
407
|
+
this.startFabricMonitoring();
|
|
320
408
|
this.isRunning = true;
|
|
321
409
|
log.info(`Matter server started successfully on port ${this.config.port}`);
|
|
322
410
|
log.info('Plugins can now register Matter accessories via the API');
|
|
@@ -356,12 +444,14 @@ export class MatterServer {
|
|
|
356
444
|
throw new Error(`Storage path not accessible: ${error}`);
|
|
357
445
|
}
|
|
358
446
|
// Create bridge-specific storage directory
|
|
359
|
-
//
|
|
360
|
-
const bridgeId = this.config.uniqueId
|
|
447
|
+
// uniqueId is already sanitized in validateAndSanitizeConfig()
|
|
448
|
+
const bridgeId = this.config.uniqueId || 'default';
|
|
361
449
|
this.matterStoragePath = path.join(normalizedPath, bridgeId);
|
|
362
450
|
await fse.ensureDir(this.matterStoragePath);
|
|
363
451
|
// Create storage manager
|
|
364
452
|
this.storageManager = new MatterStorageManager(this.matterStoragePath);
|
|
453
|
+
// Create accessory cache
|
|
454
|
+
this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
|
|
365
455
|
// Configure environment to use our custom storage
|
|
366
456
|
const environment = Environment.default;
|
|
367
457
|
const storageService = environment.get(StorageService);
|
|
@@ -396,7 +486,8 @@ export class MatterServer {
|
|
|
396
486
|
if (!this.storageManager) {
|
|
397
487
|
throw new Error('Storage manager not initialized');
|
|
398
488
|
}
|
|
399
|
-
|
|
489
|
+
// Use 'credentials' namespace
|
|
490
|
+
const storage = this.storageManager.getStorage('credentials');
|
|
400
491
|
// CRITICAL: Initialize storage before reading to avoid race condition
|
|
401
492
|
await storage.initialize();
|
|
402
493
|
// Try to load existing credentials
|
|
@@ -419,14 +510,6 @@ export class MatterServer {
|
|
|
419
510
|
log.info('Commissioning credentials saved to storage');
|
|
420
511
|
}
|
|
421
512
|
}
|
|
422
|
-
/**
|
|
423
|
-
* Generate serial number for the bridge
|
|
424
|
-
*/
|
|
425
|
-
generateSerialNumber() {
|
|
426
|
-
const timestamp = Date.now().toString(36).toUpperCase();
|
|
427
|
-
const random = crypto.randomBytes(2).toString('hex').toUpperCase();
|
|
428
|
-
return `HB-${timestamp}-${random}`;
|
|
429
|
-
}
|
|
430
513
|
/**
|
|
431
514
|
* Generate and display commissioning information
|
|
432
515
|
*/
|
|
@@ -508,6 +591,115 @@ export class MatterServer {
|
|
|
508
591
|
// Additional small delay to ensure everything is initialized
|
|
509
592
|
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
510
593
|
}
|
|
594
|
+
/**
|
|
595
|
+
* Start monitoring fabric changes to emit commissioning events
|
|
596
|
+
*/
|
|
597
|
+
startFabricMonitoring() {
|
|
598
|
+
// Stop any existing monitor
|
|
599
|
+
this.stopFabricMonitoring();
|
|
600
|
+
// Initialize with current fabrics
|
|
601
|
+
const initialFabrics = this.getFabricInfo();
|
|
602
|
+
for (const fabric of initialFabrics) {
|
|
603
|
+
this.previousFabrics.set(fabric.fabricIndex, fabric);
|
|
604
|
+
}
|
|
605
|
+
log.debug('Starting fabric monitoring for commissioning events');
|
|
606
|
+
// Set up periodic monitoring
|
|
607
|
+
this.fabricMonitorInterval = setInterval(() => {
|
|
608
|
+
this.checkFabricChanges();
|
|
609
|
+
}, FABRIC_MONITOR_INTERVAL_MS);
|
|
610
|
+
// Add to clean up handlers
|
|
611
|
+
this.cleanupHandlers.push(() => this.stopFabricMonitoring());
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Stop fabric monitoring
|
|
615
|
+
*/
|
|
616
|
+
stopFabricMonitoring() {
|
|
617
|
+
if (this.fabricMonitorInterval) {
|
|
618
|
+
clearInterval(this.fabricMonitorInterval);
|
|
619
|
+
this.fabricMonitorInterval = null;
|
|
620
|
+
log.debug('Stopped fabric monitoring');
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Check for fabric changes and emit appropriate events
|
|
625
|
+
*/
|
|
626
|
+
checkFabricChanges() {
|
|
627
|
+
try {
|
|
628
|
+
const currentFabrics = this.getFabricInfo();
|
|
629
|
+
const currentFabricMap = new Map();
|
|
630
|
+
// Build map of current fabrics
|
|
631
|
+
for (const fabric of currentFabrics) {
|
|
632
|
+
currentFabricMap.set(fabric.fabricIndex, fabric);
|
|
633
|
+
}
|
|
634
|
+
const previousCount = this.previousFabrics.size;
|
|
635
|
+
const currentCount = currentFabricMap.size;
|
|
636
|
+
// Check for added fabrics
|
|
637
|
+
for (const [fabricIndex, fabric] of currentFabricMap) {
|
|
638
|
+
if (!this.previousFabrics.has(fabricIndex)) {
|
|
639
|
+
log.info(`Fabric added: ${fabric.fabricId} (index: ${fabricIndex})`);
|
|
640
|
+
this.emit('fabric-added', fabric);
|
|
641
|
+
// If this is the first fabric, emit 'commissioned' event
|
|
642
|
+
if (previousCount === 0 && currentCount === 1) {
|
|
643
|
+
log.info('Bridge commissioned for the first time');
|
|
644
|
+
this.emit('commissioned', fabric);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Check for removed fabrics
|
|
649
|
+
for (const [fabricIndex, fabric] of this.previousFabrics) {
|
|
650
|
+
if (!currentFabricMap.has(fabricIndex)) {
|
|
651
|
+
log.info(`Fabric removed: ${fabric.fabricId} (index: ${fabricIndex})`);
|
|
652
|
+
this.emit('fabric-removed', fabric);
|
|
653
|
+
// If this was the last fabric, emit 'decommissioned' event
|
|
654
|
+
if (previousCount === 1 && currentCount === 0) {
|
|
655
|
+
log.info('Bridge decommissioned (last fabric removed)');
|
|
656
|
+
this.emit('decommissioned');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Emit general commissioning-changed event if count changed
|
|
661
|
+
if (previousCount !== currentCount) {
|
|
662
|
+
const commissioned = currentCount > 0;
|
|
663
|
+
log.debug(`Commissioning state changed: commissioned=${commissioned}, fabricCount=${currentCount}`);
|
|
664
|
+
this.emit('commissioning-changed', commissioned, currentCount);
|
|
665
|
+
// Update commissioning info file
|
|
666
|
+
this.updateCommissioningFile().catch((error) => {
|
|
667
|
+
log.warn('Failed to update commissioning file:', error);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
// Update previous fabrics map
|
|
671
|
+
this.previousFabrics = currentFabricMap;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
log.error('Error checking fabric changes:', error);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Update commissioning info file when commissioning state changes
|
|
679
|
+
*/
|
|
680
|
+
async updateCommissioningFile() {
|
|
681
|
+
try {
|
|
682
|
+
if (!this.matterStoragePath) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const commissioningFilePath = path.join(this.matterStoragePath, 'commissioning.json');
|
|
686
|
+
const commissioningData = {
|
|
687
|
+
qrCode: this.commissioningInfo.qrCode,
|
|
688
|
+
manualPairingCode: this.commissioningInfo.manualPairingCode,
|
|
689
|
+
serialNumber: this.serialNumber,
|
|
690
|
+
passcode: this.passcode,
|
|
691
|
+
discriminator: this.discriminator,
|
|
692
|
+
commissioned: this.isCommissioned(),
|
|
693
|
+
fabricCount: this.getCommissionedFabricCount(),
|
|
694
|
+
fabrics: this.getFabricInfo(),
|
|
695
|
+
};
|
|
696
|
+
await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
|
|
697
|
+
log.debug('Updated commissioning info file');
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
log.debug(`Failed to update commissioning info file: ${error.message}`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
511
703
|
/**
|
|
512
704
|
* Register a Matter accessory (Plugin API)
|
|
513
705
|
*/
|
|
@@ -515,44 +707,127 @@ export class MatterServer {
|
|
|
515
707
|
if (!this.serverNode || !this.aggregator) {
|
|
516
708
|
throw new MatterDeviceError('Matter server not started');
|
|
517
709
|
}
|
|
518
|
-
// Validate required fields
|
|
710
|
+
// Validate required fields with helpful error messages
|
|
519
711
|
if (!accessory.deviceType) {
|
|
520
|
-
throw new MatterDeviceError(`
|
|
521
|
-
+ '
|
|
712
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName || 'unknown'}" is missing required field 'deviceType'. `
|
|
713
|
+
+ 'Example: deviceType: api.matterDeviceTypes.OnOffLight\n'
|
|
714
|
+
+ 'Available device types: OnOffLight, DimmableLight, TemperatureSensor, etc.\n'
|
|
715
|
+
+ 'See the Matter types documentation for the full list.');
|
|
522
716
|
}
|
|
523
717
|
if (!accessory.uuid) {
|
|
524
|
-
throw new MatterDeviceError('
|
|
718
|
+
throw new MatterDeviceError('Matter accessory is missing required field \'uuid\'.\n'
|
|
719
|
+
+ 'Generate a unique UUID for your accessory:\n'
|
|
720
|
+
+ ' const uuid = api.hap.uuid.generate(\'my-unique-id\')');
|
|
525
721
|
}
|
|
526
722
|
if (!accessory.displayName) {
|
|
527
|
-
throw new MatterDeviceError(
|
|
723
|
+
throw new MatterDeviceError(`Matter accessory (${accessory.uuid}) is missing required field 'displayName'.\n`
|
|
724
|
+
+ 'Example: displayName: \'Living Room Light\'');
|
|
528
725
|
}
|
|
529
726
|
if (!accessory.serialNumber) {
|
|
530
|
-
throw new MatterDeviceError(`
|
|
727
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'serialNumber'.\n`
|
|
728
|
+
+ 'Example: serialNumber: \'ABC123\' or serialNumber: accessory.UUID');
|
|
531
729
|
}
|
|
532
730
|
if (!accessory.manufacturer) {
|
|
533
|
-
throw new MatterDeviceError(`
|
|
731
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'manufacturer'.\n`
|
|
732
|
+
+ 'Example: manufacturer: \'Homebridge\' or manufacturer: \'My Plugin Name\'');
|
|
534
733
|
}
|
|
535
734
|
if (!accessory.model) {
|
|
536
|
-
throw new MatterDeviceError(`
|
|
735
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'model'.\n`
|
|
736
|
+
+ 'Example: model: \'v1.0\' or model: \'Smart Light\'');
|
|
537
737
|
}
|
|
538
738
|
if (!accessory.clusters || typeof accessory.clusters !== 'object') {
|
|
539
|
-
throw new MatterDeviceError(`
|
|
739
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing or has invalid 'clusters' field.\n`
|
|
740
|
+
+ 'Clusters define the functionality of your device. Example:\n'
|
|
741
|
+
+ ' clusters: {\n'
|
|
742
|
+
+ ' onOff: { onOff: false },\n'
|
|
743
|
+
+ ' levelControl: { currentLevel: 0, minLevel: 0, maxLevel: 254 }\n'
|
|
744
|
+
+ ' }');
|
|
540
745
|
}
|
|
541
|
-
// Check if already registered
|
|
746
|
+
// Check if already registered (during this session)
|
|
542
747
|
if (this.accessories.has(accessory.uuid)) {
|
|
543
|
-
|
|
748
|
+
const existing = this.accessories.get(accessory.uuid);
|
|
749
|
+
throw new MatterDeviceError(`Matter accessory with UUID "${accessory.uuid}" is already registered.\n`
|
|
750
|
+
+ `Existing accessory: "${existing?.displayName}"\n`
|
|
751
|
+
+ `New accessory: "${accessory.displayName}"\n`
|
|
752
|
+
+ 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
|
|
753
|
+
}
|
|
754
|
+
// Check if there's a cached version - merge cached cluster states with new registration
|
|
755
|
+
if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
|
|
756
|
+
const cached = this.accessoryCache.getCached(accessory.uuid);
|
|
757
|
+
if (cached && cached.clusters) {
|
|
758
|
+
// Merge cached cluster states with new ones (prefer cached state to persist values across restarts)
|
|
759
|
+
for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
|
|
760
|
+
if (!accessory.clusters[clusterName]) {
|
|
761
|
+
// Cluster exists in cache but not in new registration - preserve it
|
|
762
|
+
accessory.clusters[clusterName] = cachedAttrs;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
// Cluster exists in both - merge (prefer cached state over initial values)
|
|
766
|
+
accessory.clusters[clusterName] = {
|
|
767
|
+
...accessory.clusters[clusterName],
|
|
768
|
+
...cachedAttrs,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Restore context if available
|
|
773
|
+
if (cached.context && !accessory.context) {
|
|
774
|
+
accessory.context = cached.context;
|
|
775
|
+
}
|
|
776
|
+
log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
|
|
777
|
+
}
|
|
544
778
|
}
|
|
545
779
|
// Check device limit
|
|
546
780
|
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
547
|
-
throw new MatterDeviceError(`
|
|
781
|
+
throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
|
|
782
|
+
+ `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
|
|
783
|
+
+ `Current registered devices: ${this.accessories.size}`);
|
|
548
784
|
}
|
|
549
785
|
try {
|
|
550
|
-
//
|
|
551
|
-
|
|
786
|
+
// Modify device type with custom behaviors if handlers are defined
|
|
787
|
+
let deviceType = accessory.deviceType;
|
|
788
|
+
if (accessory.handlers) {
|
|
789
|
+
// Map cluster names to custom behavior classes
|
|
790
|
+
// Only clusters with user-triggered commands need custom behaviors
|
|
791
|
+
const behaviorMap = {
|
|
792
|
+
// Core controls
|
|
793
|
+
onOff: HomebridgeOnOffServer,
|
|
794
|
+
levelControl: HomebridgeLevelControlServer,
|
|
795
|
+
colorControl: HomebridgeColorControlServer,
|
|
796
|
+
// Coverings & locks
|
|
797
|
+
windowCovering: HomebridgeWindowCoveringServer,
|
|
798
|
+
doorLock: HomebridgeDoorLockServer,
|
|
799
|
+
// Climate control
|
|
800
|
+
thermostat: HomebridgeThermostatServer,
|
|
801
|
+
// Identification
|
|
802
|
+
identify: HomebridgeIdentifyServer,
|
|
803
|
+
};
|
|
804
|
+
// Build array of custom behaviors to apply based on what handlers are defined
|
|
805
|
+
const customBehaviors = [];
|
|
806
|
+
for (const clusterName of Object.keys(accessory.handlers)) {
|
|
807
|
+
const behaviorClass = behaviorMap[clusterName];
|
|
808
|
+
if (behaviorClass) {
|
|
809
|
+
customBehaviors.push(behaviorClass);
|
|
810
|
+
log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (customBehaviors.length > 0) {
|
|
817
|
+
// Cast to any to bypass TypeScript limitations
|
|
818
|
+
deviceType = deviceType.with(...customBehaviors);
|
|
819
|
+
log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// Create endpoint with the modified device type
|
|
823
|
+
const endpoint = new Endpoint(deviceType, {
|
|
552
824
|
id: accessory.uuid,
|
|
553
825
|
});
|
|
554
826
|
// Add to aggregator FIRST (required before we can configure it)
|
|
555
827
|
await this.aggregator.add(endpoint);
|
|
828
|
+
if (this.config.debugModeEnabled) {
|
|
829
|
+
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
830
|
+
}
|
|
556
831
|
// NOW configure the endpoint
|
|
557
832
|
await this.configureEndpoint(endpoint, accessory);
|
|
558
833
|
// Store accessory
|
|
@@ -563,6 +838,17 @@ export class MatterServer {
|
|
|
563
838
|
};
|
|
564
839
|
this.accessories.set(accessory.uuid, internalAccessory);
|
|
565
840
|
log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
|
|
841
|
+
if (this.config.debugModeEnabled) {
|
|
842
|
+
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
843
|
+
}
|
|
844
|
+
// Emit accessory-registered event
|
|
845
|
+
this.emit('accessory-registered', accessory);
|
|
846
|
+
// Save to cache asynchronously (don't block registration)
|
|
847
|
+
if (this.accessoryCache) {
|
|
848
|
+
this.accessoryCache.save(this.accessories).catch((error) => {
|
|
849
|
+
log.warn('Failed to save accessory cache:', error);
|
|
850
|
+
});
|
|
851
|
+
}
|
|
566
852
|
}
|
|
567
853
|
catch (error) {
|
|
568
854
|
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
@@ -585,6 +871,15 @@ export class MatterServer {
|
|
|
585
871
|
}
|
|
586
872
|
this.accessories.delete(uuid);
|
|
587
873
|
log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
|
|
874
|
+
// Emit accessory-unregistered event
|
|
875
|
+
this.emit('accessory-unregistered', uuid);
|
|
876
|
+
// Update cache (remove the accessory)
|
|
877
|
+
if (this.accessoryCache) {
|
|
878
|
+
this.accessoryCache.removeCached(uuid);
|
|
879
|
+
this.accessoryCache.save(this.accessories).catch((error) => {
|
|
880
|
+
log.warn('Failed to save accessory cache:', error);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
588
883
|
}
|
|
589
884
|
catch (error) {
|
|
590
885
|
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
@@ -593,27 +888,113 @@ export class MatterServer {
|
|
|
593
888
|
}
|
|
594
889
|
/**
|
|
595
890
|
* Update a Matter accessory's state (Plugin API)
|
|
891
|
+
*
|
|
892
|
+
* This method can be called from anywhere, including from within handlers.
|
|
893
|
+
* State updates are automatically deferred to avoid transaction conflicts.
|
|
596
894
|
*/
|
|
597
895
|
async updateAccessoryState(uuid, cluster, attributes) {
|
|
598
896
|
const accessory = this.accessories.get(uuid);
|
|
599
897
|
if (!accessory || !accessory.endpoint) {
|
|
600
898
|
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
601
899
|
}
|
|
900
|
+
// Defer the update to avoid "read-only transaction" errors when called from handlers
|
|
901
|
+
// Matter.js uses transactions, and we need to wait until the transaction fully completes
|
|
902
|
+
// Use setTimeout with a delay to ensure we're completely outside the transaction
|
|
903
|
+
return new Promise((resolve, reject) => {
|
|
904
|
+
setTimeout(async () => {
|
|
905
|
+
try {
|
|
906
|
+
// Use endpoint.set() method which is the proper way to update state
|
|
907
|
+
// This handles transactions correctly
|
|
908
|
+
const endpoint = accessory.endpoint;
|
|
909
|
+
// Construct the update object
|
|
910
|
+
const updateObject = { [cluster]: attributes };
|
|
911
|
+
// Use endpoint.set() which properly handles state updates
|
|
912
|
+
await endpoint.set(updateObject);
|
|
913
|
+
// CRITICAL: Also update the cached clusters object so state persists across restarts
|
|
914
|
+
// Merge the new attributes into the existing cluster state
|
|
915
|
+
if (!accessory.clusters[cluster]) {
|
|
916
|
+
accessory.clusters[cluster] = {};
|
|
917
|
+
}
|
|
918
|
+
accessory.clusters[cluster] = {
|
|
919
|
+
...accessory.clusters[cluster],
|
|
920
|
+
...attributes,
|
|
921
|
+
};
|
|
922
|
+
log.debug(`Updated ${cluster} state for ${accessory.displayName}:`, attributes);
|
|
923
|
+
resolve();
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
log.error(`Failed to update state for accessory ${uuid}:`, error);
|
|
927
|
+
reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
|
|
928
|
+
}
|
|
929
|
+
}, 50); // 50ms delay to ensure we're completely outside the transaction
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get a Matter accessory's current state (Plugin API)
|
|
934
|
+
*
|
|
935
|
+
* Returns the current cluster attribute values that are exposed to Matter controllers.
|
|
936
|
+
* This is useful for:
|
|
937
|
+
* - Reading state after plugin restart (when local variables are lost)
|
|
938
|
+
* - Verifying current state before making changes
|
|
939
|
+
* - Multiple parts of code that need to read state
|
|
940
|
+
* - Debugging and logging
|
|
941
|
+
*
|
|
942
|
+
* @param uuid - The UUID of the accessory
|
|
943
|
+
* @param cluster - The cluster name (e.g., 'onOff', 'levelControl')
|
|
944
|
+
* @returns Current cluster attribute values, or undefined if cluster not found
|
|
945
|
+
*/
|
|
946
|
+
getAccessoryState(uuid, cluster) {
|
|
947
|
+
const accessory = this.accessories.get(uuid);
|
|
948
|
+
if (!accessory || !accessory.endpoint) {
|
|
949
|
+
log.debug(`Accessory ${uuid} not found or not registered`);
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
602
952
|
try {
|
|
603
|
-
// Update the endpoint's cluster state
|
|
604
|
-
// Note: Endpoint types from Matter.js don't expose state properly, needs runtime check
|
|
605
953
|
const endpoint = accessory.endpoint;
|
|
606
|
-
if (endpoint.state
|
|
607
|
-
|
|
608
|
-
|
|
954
|
+
if (!endpoint.state) {
|
|
955
|
+
log.debug(`endpoint.state is undefined for ${accessory.displayName}`);
|
|
956
|
+
return undefined;
|
|
609
957
|
}
|
|
610
|
-
|
|
611
|
-
|
|
958
|
+
if (!endpoint.state[cluster]) {
|
|
959
|
+
const availableClusters = Object.keys(endpoint.state || {});
|
|
960
|
+
log.debug(`Cluster '${cluster}' not found on ${accessory.displayName}. Available: ${availableClusters.join(', ')}`);
|
|
961
|
+
return undefined;
|
|
612
962
|
}
|
|
963
|
+
const clusterState = endpoint.state[cluster];
|
|
964
|
+
// Build result object by reading each property directly
|
|
965
|
+
const result = {};
|
|
966
|
+
// Get list of properties to read - use both approaches for maximum compatibility
|
|
967
|
+
const allKeys = new Set([
|
|
968
|
+
...Object.keys(clusterState),
|
|
969
|
+
...Object.getOwnPropertyNames(clusterState),
|
|
970
|
+
]);
|
|
971
|
+
for (const key of allKeys) {
|
|
972
|
+
try {
|
|
973
|
+
// Skip internal properties, methods, and symbols
|
|
974
|
+
if (key.startsWith('_') || key.startsWith('$')) {
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
// Try to read the value directly
|
|
978
|
+
const value = clusterState[key];
|
|
979
|
+
// Skip functions and undefined values
|
|
980
|
+
if (typeof value === 'function' || value === undefined) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
result[key] = value;
|
|
984
|
+
}
|
|
985
|
+
catch (propError) {
|
|
986
|
+
log.debug(`Could not read property ${key} from ${cluster}:`, propError);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (Object.keys(result).length === 0) {
|
|
990
|
+
log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`);
|
|
991
|
+
return undefined;
|
|
992
|
+
}
|
|
993
|
+
return result;
|
|
613
994
|
}
|
|
614
995
|
catch (error) {
|
|
615
|
-
log.error(`Failed to
|
|
616
|
-
|
|
996
|
+
log.error(`Failed to get state for accessory ${uuid}:`, error);
|
|
997
|
+
return undefined;
|
|
617
998
|
}
|
|
618
999
|
}
|
|
619
1000
|
/**
|
|
@@ -673,54 +1054,13 @@ export class MatterServer {
|
|
|
673
1054
|
// Set up command handlers if provided
|
|
674
1055
|
if (accessory.handlers) {
|
|
675
1056
|
log.info(`Setting up handlers for accessory ${accessory.uuid}`);
|
|
676
|
-
//
|
|
677
|
-
|
|
678
|
-
log.info(`
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
}
|
|
1057
|
+
// Register handlers with the custom behavior classes
|
|
1058
|
+
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1059
|
+
log.info(` Processing cluster: ${clusterName}`);
|
|
1060
|
+
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1061
|
+
registerHandler(accessory.uuid, clusterName, commandName, handler);
|
|
722
1062
|
}
|
|
723
|
-
}
|
|
1063
|
+
}
|
|
724
1064
|
}
|
|
725
1065
|
}
|
|
726
1066
|
/**
|
|
@@ -733,9 +1073,14 @@ export class MatterServer {
|
|
|
733
1073
|
}
|
|
734
1074
|
this.isRunning = false;
|
|
735
1075
|
// Stop monitoring
|
|
1076
|
+
this.stopFabricMonitoring();
|
|
736
1077
|
networkMonitor.stopMonitoring();
|
|
737
|
-
diagnostics.stopDiagnostics();
|
|
738
1078
|
try {
|
|
1079
|
+
// Save accessory cache before shutting down (BEFORE clearing accessories!)
|
|
1080
|
+
if (this.accessoryCache && this.accessories.size > 0) {
|
|
1081
|
+
await this.accessoryCache.save(this.accessories);
|
|
1082
|
+
log.debug('Saved accessory cache before shutdown');
|
|
1083
|
+
}
|
|
739
1084
|
// Clean up all accessories
|
|
740
1085
|
for (const accessory of this.accessories.values()) {
|
|
741
1086
|
try {
|
|
@@ -882,5 +1227,40 @@ export class MatterServer {
|
|
|
882
1227
|
getClusters() {
|
|
883
1228
|
return clusters;
|
|
884
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Remove a specific fabric (controller) from the bridge
|
|
1232
|
+
* This decommissions a single controller while leaving others intact
|
|
1233
|
+
*
|
|
1234
|
+
* @param fabricIndex - The fabric index to remove
|
|
1235
|
+
* @returns Promise that resolves when the fabric is removed
|
|
1236
|
+
*/
|
|
1237
|
+
async removeFabric(fabricIndex) {
|
|
1238
|
+
if (!this.serverNode) {
|
|
1239
|
+
throw new MatterDeviceError('Matter server not started');
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
log.info(`Removing fabric ${fabricIndex}...`);
|
|
1243
|
+
const serverState = this.serverNode;
|
|
1244
|
+
const removeFabric = serverState?.state?.commissioning?.removeFabric;
|
|
1245
|
+
if (typeof removeFabric !== 'function') {
|
|
1246
|
+
throw new MatterDeviceError('Fabric removal not supported by Matter.js version');
|
|
1247
|
+
}
|
|
1248
|
+
// Remove the fabric
|
|
1249
|
+
await removeFabric(fabricIndex);
|
|
1250
|
+
log.info(`Fabric ${fabricIndex} removed successfully`);
|
|
1251
|
+
// The fabric monitoring will detect this change and emit the appropriate events
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
log.error(`Failed to remove fabric ${fabricIndex}:`, error);
|
|
1255
|
+
throw new MatterDeviceError(`Failed to remove fabric: ${error.message}`, error);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Check if a specific fabric exists
|
|
1260
|
+
*/
|
|
1261
|
+
hasFabric(fabricIndex) {
|
|
1262
|
+
const fabrics = this.getFabricInfo();
|
|
1263
|
+
return fabrics.some(f => f.fabricIndex === fabricIndex);
|
|
1264
|
+
}
|
|
885
1265
|
}
|
|
886
1266
|
//# sourceMappingURL=matterServer.js.map
|