homebridge 2.0.0-alpha.42 → 2.0.0-alpha.44
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 +64 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +54 -0
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +1 -4
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +5 -10
- package/dist/bridgeService.js.map +1 -1
- package/dist/bridgeTypes.d.ts +54 -0
- package/dist/bridgeTypes.d.ts.map +1 -0
- package/dist/bridgeTypes.js +8 -0
- package/dist/bridgeTypes.js.map +1 -0
- package/dist/childBridgeFork.d.ts +2 -6
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +34 -93
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +9 -7
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +9 -2
- 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/ipcService.d.ts +2 -12
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js +0 -7
- 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 +80 -5
- package/dist/matter/matterServer.d.ts.map +1 -1
- package/dist/matter/matterServer.js +512 -60
- package/dist/matter/matterServer.js.map +1 -1
- package/dist/matter/matterSharedTypes.d.ts +27 -21
- package/dist/matter/matterSharedTypes.d.ts.map +1 -1
- package/dist/matter/matterSharedTypes.js +3 -0
- package/dist/matter/matterSharedTypes.js.map +1 -1
- package/dist/matter/matterStorage.d.ts +17 -3
- package/dist/matter/matterStorage.d.ts.map +1 -1
- package/dist/matter/matterStorage.js +135 -38
- package/dist/matter/matterStorage.js.map +1 -1
- package/dist/matter/matterTypes.d.ts +68 -19
- 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 +100 -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 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +69 -52
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- 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
|
-
import { access } from 'node:fs/promises';
|
|
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;
|
|
@@ -53,19 +59,88 @@ export class MatterServer {
|
|
|
53
59
|
serialNumber;
|
|
54
60
|
cleanupHandlers = [];
|
|
55
61
|
storageManager = null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
matterStoragePath;
|
|
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
|
+
}
|
|
66
75
|
// Initialize commissioning values (will be loaded from storage in start())
|
|
67
76
|
this.vendorId = DEFAULT_VENDOR_ID;
|
|
68
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
|
+
};
|
|
69
144
|
}
|
|
70
145
|
/**
|
|
71
146
|
* Generate a secure random passcode
|
|
@@ -166,8 +241,21 @@ export class MatterServer {
|
|
|
166
241
|
}
|
|
167
242
|
/**
|
|
168
243
|
* Create ServerNode with automatic recovery from corrupted storage
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
|
171
259
|
*/
|
|
172
260
|
async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
|
|
173
261
|
try {
|
|
@@ -247,17 +335,14 @@ export class MatterServer {
|
|
|
247
335
|
// Start network monitoring
|
|
248
336
|
networkMonitor.startMonitoring();
|
|
249
337
|
this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
|
|
250
|
-
// Start diagnostics
|
|
251
|
-
diagnostics.startDiagnostics();
|
|
252
|
-
this.cleanupHandlers.push(() => diagnostics.stopDiagnostics());
|
|
253
338
|
// Create commissioning options
|
|
254
339
|
const commissioningOptions = {
|
|
255
340
|
passcode: this.passcode,
|
|
256
341
|
discriminator: this.discriminator,
|
|
257
342
|
};
|
|
258
343
|
log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
|
|
259
|
-
//
|
|
260
|
-
const bridgeName =
|
|
344
|
+
// Use default name for the Matter bridge
|
|
345
|
+
const bridgeName = 'Homebridge Matter Bridge';
|
|
261
346
|
// Sanitize the uniqueId to ensure it's filesystem-safe
|
|
262
347
|
// Replace any characters that could cause issues (colons, slashes, etc.)
|
|
263
348
|
// Use only alphanumeric and hyphens, collapse multiple hyphens, trim leading/trailing hyphens
|
|
@@ -277,15 +362,15 @@ export class MatterServer {
|
|
|
277
362
|
basicInformation: {
|
|
278
363
|
nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
|
|
279
364
|
vendorId: VendorId(this.vendorId),
|
|
280
|
-
vendorName: 'Homebridge'.slice(0, 32),
|
|
365
|
+
vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
|
|
281
366
|
productId: this.productId,
|
|
282
|
-
productName: 'Homebridge Matter Bridge'.slice(0, 32),
|
|
367
|
+
productName: (this.config.model || 'Homebridge Matter Bridge').slice(0, 32),
|
|
283
368
|
productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
|
|
284
|
-
serialNumber: this.serialNumber = this.generateSerialNumber(),
|
|
369
|
+
serialNumber: this.serialNumber = this.config.serialNumber || this.generateSerialNumber(),
|
|
285
370
|
hardwareVersion: 1,
|
|
286
371
|
hardwareVersionString: os.release(),
|
|
287
372
|
softwareVersion: 1,
|
|
288
|
-
softwareVersionString: getVersion(),
|
|
373
|
+
softwareVersionString: this.config.firmwareRevision || getVersion(),
|
|
289
374
|
reachable: true,
|
|
290
375
|
},
|
|
291
376
|
};
|
|
@@ -316,6 +401,12 @@ export class MatterServer {
|
|
|
316
401
|
});
|
|
317
402
|
// Wait for server to be ready
|
|
318
403
|
await this.waitForServerReady();
|
|
404
|
+
// Load cached accessories (don't restore them yet - wait for plugins to re-register)
|
|
405
|
+
if (this.accessoryCache) {
|
|
406
|
+
await this.accessoryCache.load();
|
|
407
|
+
}
|
|
408
|
+
// Start fabric monitoring to emit commissioning events
|
|
409
|
+
this.startFabricMonitoring();
|
|
319
410
|
this.isRunning = true;
|
|
320
411
|
log.info(`Matter server started successfully on port ${this.config.port}`);
|
|
321
412
|
log.info('Plugins can now register Matter accessories via the API');
|
|
@@ -357,14 +448,16 @@ export class MatterServer {
|
|
|
357
448
|
// Create bridge-specific storage directory
|
|
358
449
|
// Use only alphanumeric characters and hyphens for maximum compatibility
|
|
359
450
|
const bridgeId = this.config.uniqueId?.replace(/[^a-z0-9]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'default';
|
|
360
|
-
|
|
361
|
-
await fse.ensureDir(matterStoragePath);
|
|
451
|
+
this.matterStoragePath = path.join(normalizedPath, bridgeId);
|
|
452
|
+
await fse.ensureDir(this.matterStoragePath);
|
|
362
453
|
// Create storage manager
|
|
363
|
-
this.storageManager = new MatterStorageManager(matterStoragePath);
|
|
454
|
+
this.storageManager = new MatterStorageManager(this.matterStoragePath);
|
|
455
|
+
// Create accessory cache
|
|
456
|
+
this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
|
|
364
457
|
// Configure environment to use our custom storage
|
|
365
458
|
const environment = Environment.default;
|
|
366
459
|
const storageService = environment.get(StorageService);
|
|
367
|
-
storageService.location = matterStoragePath;
|
|
460
|
+
storageService.location = this.matterStoragePath;
|
|
368
461
|
// CRITICAL: Override storage factory with custom implementation
|
|
369
462
|
// This ensures fabric data is properly persisted
|
|
370
463
|
storageService.factory = (namespace) => {
|
|
@@ -385,7 +478,7 @@ export class MatterServer {
|
|
|
385
478
|
await this.storageManager.closeAll();
|
|
386
479
|
}
|
|
387
480
|
});
|
|
388
|
-
log.info(`Matter storage initialized at: ${matterStoragePath}`);
|
|
481
|
+
log.info(`Matter storage initialized at: ${this.matterStoragePath}`);
|
|
389
482
|
}
|
|
390
483
|
/**
|
|
391
484
|
* Load or generate commissioning credentials (passcode and discriminator)
|
|
@@ -395,7 +488,8 @@ export class MatterServer {
|
|
|
395
488
|
if (!this.storageManager) {
|
|
396
489
|
throw new Error('Storage manager not initialized');
|
|
397
490
|
}
|
|
398
|
-
|
|
491
|
+
// Use 'credentials' namespace
|
|
492
|
+
const storage = this.storageManager.getStorage('credentials');
|
|
399
493
|
// CRITICAL: Initialize storage before reading to avoid race condition
|
|
400
494
|
await storage.initialize();
|
|
401
495
|
// Try to load existing credentials
|
|
@@ -458,6 +552,26 @@ export class MatterServer {
|
|
|
458
552
|
qrCode: qrCodePayload,
|
|
459
553
|
manualPairingCode,
|
|
460
554
|
};
|
|
555
|
+
// Save commissioning info to disk for UI access
|
|
556
|
+
try {
|
|
557
|
+
if (!this.matterStoragePath) {
|
|
558
|
+
throw new Error('Matter storage path not initialized');
|
|
559
|
+
}
|
|
560
|
+
const commissioningFilePath = path.join(this.matterStoragePath, 'commissioning.json');
|
|
561
|
+
const commissioningData = {
|
|
562
|
+
qrCode: qrCodePayload,
|
|
563
|
+
manualPairingCode,
|
|
564
|
+
serialNumber: this.serialNumber,
|
|
565
|
+
passcode: this.passcode,
|
|
566
|
+
discriminator: this.discriminator,
|
|
567
|
+
commissioned: this.isCommissioned(),
|
|
568
|
+
};
|
|
569
|
+
await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
|
|
570
|
+
log.debug(`Saved commissioning info to ${commissioningFilePath}`);
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
log.warn(`Failed to save commissioning info to disk: ${error.message}`);
|
|
574
|
+
}
|
|
461
575
|
// Display commissioning information
|
|
462
576
|
log.info(`\n${'='.repeat(60)}`);
|
|
463
577
|
log.info('📱 MATTER COMMISSIONING INFORMATION');
|
|
@@ -487,6 +601,115 @@ export class MatterServer {
|
|
|
487
601
|
// Additional small delay to ensure everything is initialized
|
|
488
602
|
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
489
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Start monitoring fabric changes to emit commissioning events
|
|
606
|
+
*/
|
|
607
|
+
startFabricMonitoring() {
|
|
608
|
+
// Stop any existing monitor
|
|
609
|
+
this.stopFabricMonitoring();
|
|
610
|
+
// Initialize with current fabrics
|
|
611
|
+
const initialFabrics = this.getFabricInfo();
|
|
612
|
+
for (const fabric of initialFabrics) {
|
|
613
|
+
this.previousFabrics.set(fabric.fabricIndex, fabric);
|
|
614
|
+
}
|
|
615
|
+
log.debug('Starting fabric monitoring for commissioning events');
|
|
616
|
+
// Set up periodic monitoring
|
|
617
|
+
this.fabricMonitorInterval = setInterval(() => {
|
|
618
|
+
this.checkFabricChanges();
|
|
619
|
+
}, FABRIC_MONITOR_INTERVAL_MS);
|
|
620
|
+
// Add to clean up handlers
|
|
621
|
+
this.cleanupHandlers.push(() => this.stopFabricMonitoring());
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Stop fabric monitoring
|
|
625
|
+
*/
|
|
626
|
+
stopFabricMonitoring() {
|
|
627
|
+
if (this.fabricMonitorInterval) {
|
|
628
|
+
clearInterval(this.fabricMonitorInterval);
|
|
629
|
+
this.fabricMonitorInterval = null;
|
|
630
|
+
log.debug('Stopped fabric monitoring');
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Check for fabric changes and emit appropriate events
|
|
635
|
+
*/
|
|
636
|
+
checkFabricChanges() {
|
|
637
|
+
try {
|
|
638
|
+
const currentFabrics = this.getFabricInfo();
|
|
639
|
+
const currentFabricMap = new Map();
|
|
640
|
+
// Build map of current fabrics
|
|
641
|
+
for (const fabric of currentFabrics) {
|
|
642
|
+
currentFabricMap.set(fabric.fabricIndex, fabric);
|
|
643
|
+
}
|
|
644
|
+
const previousCount = this.previousFabrics.size;
|
|
645
|
+
const currentCount = currentFabricMap.size;
|
|
646
|
+
// Check for added fabrics
|
|
647
|
+
for (const [fabricIndex, fabric] of currentFabricMap) {
|
|
648
|
+
if (!this.previousFabrics.has(fabricIndex)) {
|
|
649
|
+
log.info(`Fabric added: ${fabric.fabricId} (index: ${fabricIndex})`);
|
|
650
|
+
this.emit('fabric-added', fabric);
|
|
651
|
+
// If this is the first fabric, emit 'commissioned' event
|
|
652
|
+
if (previousCount === 0 && currentCount === 1) {
|
|
653
|
+
log.info('Bridge commissioned for the first time');
|
|
654
|
+
this.emit('commissioned', fabric);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Check for removed fabrics
|
|
659
|
+
for (const [fabricIndex, fabric] of this.previousFabrics) {
|
|
660
|
+
if (!currentFabricMap.has(fabricIndex)) {
|
|
661
|
+
log.info(`Fabric removed: ${fabric.fabricId} (index: ${fabricIndex})`);
|
|
662
|
+
this.emit('fabric-removed', fabric);
|
|
663
|
+
// If this was the last fabric, emit 'decommissioned' event
|
|
664
|
+
if (previousCount === 1 && currentCount === 0) {
|
|
665
|
+
log.info('Bridge decommissioned (last fabric removed)');
|
|
666
|
+
this.emit('decommissioned');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Emit general commissioning-changed event if count changed
|
|
671
|
+
if (previousCount !== currentCount) {
|
|
672
|
+
const commissioned = currentCount > 0;
|
|
673
|
+
log.debug(`Commissioning state changed: commissioned=${commissioned}, fabricCount=${currentCount}`);
|
|
674
|
+
this.emit('commissioning-changed', commissioned, currentCount);
|
|
675
|
+
// Update commissioning info file
|
|
676
|
+
this.updateCommissioningFile().catch((error) => {
|
|
677
|
+
log.warn('Failed to update commissioning file:', error);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// Update previous fabrics map
|
|
681
|
+
this.previousFabrics = currentFabricMap;
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
log.error('Error checking fabric changes:', error);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Update commissioning info file when commissioning state changes
|
|
689
|
+
*/
|
|
690
|
+
async updateCommissioningFile() {
|
|
691
|
+
try {
|
|
692
|
+
if (!this.matterStoragePath) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const commissioningFilePath = path.join(this.matterStoragePath, 'commissioning.json');
|
|
696
|
+
const commissioningData = {
|
|
697
|
+
qrCode: this.commissioningInfo.qrCode,
|
|
698
|
+
manualPairingCode: this.commissioningInfo.manualPairingCode,
|
|
699
|
+
serialNumber: this.serialNumber,
|
|
700
|
+
passcode: this.passcode,
|
|
701
|
+
discriminator: this.discriminator,
|
|
702
|
+
commissioned: this.isCommissioned(),
|
|
703
|
+
fabricCount: this.getCommissionedFabricCount(),
|
|
704
|
+
fabrics: this.getFabricInfo(),
|
|
705
|
+
};
|
|
706
|
+
await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
|
|
707
|
+
log.debug('Updated commissioning info file');
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
log.debug(`Failed to update commissioning info file: ${error.message}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
490
713
|
/**
|
|
491
714
|
* Register a Matter accessory (Plugin API)
|
|
492
715
|
*/
|
|
@@ -494,44 +717,127 @@ export class MatterServer {
|
|
|
494
717
|
if (!this.serverNode || !this.aggregator) {
|
|
495
718
|
throw new MatterDeviceError('Matter server not started');
|
|
496
719
|
}
|
|
497
|
-
// Validate required fields
|
|
720
|
+
// Validate required fields with helpful error messages
|
|
498
721
|
if (!accessory.deviceType) {
|
|
499
|
-
throw new MatterDeviceError(`
|
|
500
|
-
+ '
|
|
722
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName || 'unknown'}" is missing required field 'deviceType'. `
|
|
723
|
+
+ 'Example: deviceType: api.matterDeviceTypes.OnOffLight\n'
|
|
724
|
+
+ 'Available device types: OnOffLight, DimmableLight, TemperatureSensor, etc.\n'
|
|
725
|
+
+ 'See the Matter types documentation for the full list.');
|
|
501
726
|
}
|
|
502
727
|
if (!accessory.uuid) {
|
|
503
|
-
throw new MatterDeviceError('
|
|
728
|
+
throw new MatterDeviceError('Matter accessory is missing required field \'uuid\'.\n'
|
|
729
|
+
+ 'Generate a unique UUID for your accessory:\n'
|
|
730
|
+
+ ' const uuid = api.hap.uuid.generate(\'my-unique-id\')');
|
|
504
731
|
}
|
|
505
732
|
if (!accessory.displayName) {
|
|
506
|
-
throw new MatterDeviceError(
|
|
733
|
+
throw new MatterDeviceError(`Matter accessory (${accessory.uuid}) is missing required field 'displayName'.\n`
|
|
734
|
+
+ 'Example: displayName: \'Living Room Light\'');
|
|
507
735
|
}
|
|
508
736
|
if (!accessory.serialNumber) {
|
|
509
|
-
throw new MatterDeviceError(`
|
|
737
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'serialNumber'.\n`
|
|
738
|
+
+ 'Example: serialNumber: \'ABC123\' or serialNumber: accessory.UUID');
|
|
510
739
|
}
|
|
511
740
|
if (!accessory.manufacturer) {
|
|
512
|
-
throw new MatterDeviceError(`
|
|
741
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'manufacturer'.\n`
|
|
742
|
+
+ 'Example: manufacturer: \'Homebridge\' or manufacturer: \'My Plugin Name\'');
|
|
513
743
|
}
|
|
514
744
|
if (!accessory.model) {
|
|
515
|
-
throw new MatterDeviceError(`
|
|
745
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing required field 'model'.\n`
|
|
746
|
+
+ 'Example: model: \'v1.0\' or model: \'Smart Light\'');
|
|
516
747
|
}
|
|
517
748
|
if (!accessory.clusters || typeof accessory.clusters !== 'object') {
|
|
518
|
-
throw new MatterDeviceError(`
|
|
749
|
+
throw new MatterDeviceError(`Matter accessory "${accessory.displayName}" is missing or has invalid 'clusters' field.\n`
|
|
750
|
+
+ 'Clusters define the functionality of your device. Example:\n'
|
|
751
|
+
+ ' clusters: {\n'
|
|
752
|
+
+ ' onOff: { onOff: false },\n'
|
|
753
|
+
+ ' levelControl: { currentLevel: 0, minLevel: 0, maxLevel: 254 }\n'
|
|
754
|
+
+ ' }');
|
|
519
755
|
}
|
|
520
|
-
// Check if already registered
|
|
756
|
+
// Check if already registered (during this session)
|
|
521
757
|
if (this.accessories.has(accessory.uuid)) {
|
|
522
|
-
|
|
758
|
+
const existing = this.accessories.get(accessory.uuid);
|
|
759
|
+
throw new MatterDeviceError(`Matter accessory with UUID "${accessory.uuid}" is already registered.\n`
|
|
760
|
+
+ `Existing accessory: "${existing?.displayName}"\n`
|
|
761
|
+
+ `New accessory: "${accessory.displayName}"\n`
|
|
762
|
+
+ 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
|
|
763
|
+
}
|
|
764
|
+
// Check if there's a cached version - merge cached cluster states with new registration
|
|
765
|
+
if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
|
|
766
|
+
const cached = this.accessoryCache.getCached(accessory.uuid);
|
|
767
|
+
if (cached && cached.clusters) {
|
|
768
|
+
// Merge cached cluster states with new ones (prefer cached state to persist values across restarts)
|
|
769
|
+
for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
|
|
770
|
+
if (!accessory.clusters[clusterName]) {
|
|
771
|
+
// Cluster exists in cache but not in new registration - preserve it
|
|
772
|
+
accessory.clusters[clusterName] = cachedAttrs;
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
// Cluster exists in both - merge (prefer cached state over initial values)
|
|
776
|
+
accessory.clusters[clusterName] = {
|
|
777
|
+
...accessory.clusters[clusterName],
|
|
778
|
+
...cachedAttrs,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Restore context if available
|
|
783
|
+
if (cached.context && !accessory.context) {
|
|
784
|
+
accessory.context = cached.context;
|
|
785
|
+
}
|
|
786
|
+
log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
|
|
787
|
+
}
|
|
523
788
|
}
|
|
524
789
|
// Check device limit
|
|
525
790
|
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
526
|
-
throw new MatterDeviceError(`
|
|
791
|
+
throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
|
|
792
|
+
+ `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
|
|
793
|
+
+ `Current registered devices: ${this.accessories.size}`);
|
|
527
794
|
}
|
|
528
795
|
try {
|
|
529
|
-
//
|
|
530
|
-
|
|
796
|
+
// Modify device type with custom behaviors if handlers are defined
|
|
797
|
+
let deviceType = accessory.deviceType;
|
|
798
|
+
if (accessory.handlers) {
|
|
799
|
+
// Map cluster names to custom behavior classes
|
|
800
|
+
// Only clusters with user-triggered commands need custom behaviors
|
|
801
|
+
const behaviorMap = {
|
|
802
|
+
// Core controls
|
|
803
|
+
onOff: HomebridgeOnOffServer,
|
|
804
|
+
levelControl: HomebridgeLevelControlServer,
|
|
805
|
+
colorControl: HomebridgeColorControlServer,
|
|
806
|
+
// Coverings & locks
|
|
807
|
+
windowCovering: HomebridgeWindowCoveringServer,
|
|
808
|
+
doorLock: HomebridgeDoorLockServer,
|
|
809
|
+
// Climate control
|
|
810
|
+
thermostat: HomebridgeThermostatServer,
|
|
811
|
+
// Identification
|
|
812
|
+
identify: HomebridgeIdentifyServer,
|
|
813
|
+
};
|
|
814
|
+
// Build array of custom behaviors to apply based on what handlers are defined
|
|
815
|
+
const customBehaviors = [];
|
|
816
|
+
for (const clusterName of Object.keys(accessory.handlers)) {
|
|
817
|
+
const behaviorClass = behaviorMap[clusterName];
|
|
818
|
+
if (behaviorClass) {
|
|
819
|
+
customBehaviors.push(behaviorClass);
|
|
820
|
+
log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (customBehaviors.length > 0) {
|
|
827
|
+
// Cast to any to bypass TypeScript limitations
|
|
828
|
+
deviceType = deviceType.with(...customBehaviors);
|
|
829
|
+
log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Create endpoint with the modified device type
|
|
833
|
+
const endpoint = new Endpoint(deviceType, {
|
|
531
834
|
id: accessory.uuid,
|
|
532
835
|
});
|
|
533
836
|
// Add to aggregator FIRST (required before we can configure it)
|
|
534
837
|
await this.aggregator.add(endpoint);
|
|
838
|
+
if (this.config.debugModeEnabled) {
|
|
839
|
+
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
840
|
+
}
|
|
535
841
|
// NOW configure the endpoint
|
|
536
842
|
await this.configureEndpoint(endpoint, accessory);
|
|
537
843
|
// Store accessory
|
|
@@ -542,6 +848,17 @@ export class MatterServer {
|
|
|
542
848
|
};
|
|
543
849
|
this.accessories.set(accessory.uuid, internalAccessory);
|
|
544
850
|
log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
|
|
851
|
+
if (this.config.debugModeEnabled) {
|
|
852
|
+
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
853
|
+
}
|
|
854
|
+
// Emit accessory-registered event
|
|
855
|
+
this.emit('accessory-registered', accessory);
|
|
856
|
+
// Save to cache asynchronously (don't block registration)
|
|
857
|
+
if (this.accessoryCache) {
|
|
858
|
+
this.accessoryCache.save(this.accessories).catch((error) => {
|
|
859
|
+
log.warn('Failed to save accessory cache:', error);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
545
862
|
}
|
|
546
863
|
catch (error) {
|
|
547
864
|
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
@@ -564,6 +881,15 @@ export class MatterServer {
|
|
|
564
881
|
}
|
|
565
882
|
this.accessories.delete(uuid);
|
|
566
883
|
log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
|
|
884
|
+
// Emit accessory-unregistered event
|
|
885
|
+
this.emit('accessory-unregistered', uuid);
|
|
886
|
+
// Update cache (remove the accessory)
|
|
887
|
+
if (this.accessoryCache) {
|
|
888
|
+
this.accessoryCache.removeCached(uuid);
|
|
889
|
+
this.accessoryCache.save(this.accessories).catch((error) => {
|
|
890
|
+
log.warn('Failed to save accessory cache:', error);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
567
893
|
}
|
|
568
894
|
catch (error) {
|
|
569
895
|
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
@@ -572,27 +898,113 @@ export class MatterServer {
|
|
|
572
898
|
}
|
|
573
899
|
/**
|
|
574
900
|
* Update a Matter accessory's state (Plugin API)
|
|
901
|
+
*
|
|
902
|
+
* This method can be called from anywhere, including from within handlers.
|
|
903
|
+
* State updates are automatically deferred to avoid transaction conflicts.
|
|
575
904
|
*/
|
|
576
905
|
async updateAccessoryState(uuid, cluster, attributes) {
|
|
577
906
|
const accessory = this.accessories.get(uuid);
|
|
578
907
|
if (!accessory || !accessory.endpoint) {
|
|
579
908
|
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
580
909
|
}
|
|
910
|
+
// Defer the update to avoid "read-only transaction" errors when called from handlers
|
|
911
|
+
// Matter.js uses transactions, and we need to wait until the transaction fully completes
|
|
912
|
+
// Use setTimeout with a delay to ensure we're completely outside the transaction
|
|
913
|
+
return new Promise((resolve, reject) => {
|
|
914
|
+
setTimeout(async () => {
|
|
915
|
+
try {
|
|
916
|
+
// Use endpoint.set() method which is the proper way to update state
|
|
917
|
+
// This handles transactions correctly
|
|
918
|
+
const endpoint = accessory.endpoint;
|
|
919
|
+
// Construct the update object
|
|
920
|
+
const updateObject = { [cluster]: attributes };
|
|
921
|
+
// Use endpoint.set() which properly handles state updates
|
|
922
|
+
await endpoint.set(updateObject);
|
|
923
|
+
// CRITICAL: Also update the cached clusters object so state persists across restarts
|
|
924
|
+
// Merge the new attributes into the existing cluster state
|
|
925
|
+
if (!accessory.clusters[cluster]) {
|
|
926
|
+
accessory.clusters[cluster] = {};
|
|
927
|
+
}
|
|
928
|
+
accessory.clusters[cluster] = {
|
|
929
|
+
...accessory.clusters[cluster],
|
|
930
|
+
...attributes,
|
|
931
|
+
};
|
|
932
|
+
log.debug(`Updated ${cluster} state for ${accessory.displayName}:`, attributes);
|
|
933
|
+
resolve();
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
log.error(`Failed to update state for accessory ${uuid}:`, error);
|
|
937
|
+
reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
|
|
938
|
+
}
|
|
939
|
+
}, 50); // 50ms delay to ensure we're completely outside the transaction
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Get a Matter accessory's current state (Plugin API)
|
|
944
|
+
*
|
|
945
|
+
* Returns the current cluster attribute values that are exposed to Matter controllers.
|
|
946
|
+
* This is useful for:
|
|
947
|
+
* - Reading state after plugin restart (when local variables are lost)
|
|
948
|
+
* - Verifying current state before making changes
|
|
949
|
+
* - Multiple parts of code that need to read state
|
|
950
|
+
* - Debugging and logging
|
|
951
|
+
*
|
|
952
|
+
* @param uuid - The UUID of the accessory
|
|
953
|
+
* @param cluster - The cluster name (e.g., 'onOff', 'levelControl')
|
|
954
|
+
* @returns Current cluster attribute values, or undefined if cluster not found
|
|
955
|
+
*/
|
|
956
|
+
getAccessoryState(uuid, cluster) {
|
|
957
|
+
const accessory = this.accessories.get(uuid);
|
|
958
|
+
if (!accessory || !accessory.endpoint) {
|
|
959
|
+
log.debug(`Accessory ${uuid} not found or not registered`);
|
|
960
|
+
return undefined;
|
|
961
|
+
}
|
|
581
962
|
try {
|
|
582
|
-
// Update the endpoint's cluster state
|
|
583
|
-
// Note: Endpoint types from Matter.js don't expose state properly, needs runtime check
|
|
584
963
|
const endpoint = accessory.endpoint;
|
|
585
|
-
if (endpoint.state
|
|
586
|
-
|
|
587
|
-
|
|
964
|
+
if (!endpoint.state) {
|
|
965
|
+
log.debug(`endpoint.state is undefined for ${accessory.displayName}`);
|
|
966
|
+
return undefined;
|
|
967
|
+
}
|
|
968
|
+
if (!endpoint.state[cluster]) {
|
|
969
|
+
const availableClusters = Object.keys(endpoint.state || {});
|
|
970
|
+
log.debug(`Cluster '${cluster}' not found on ${accessory.displayName}. Available: ${availableClusters.join(', ')}`);
|
|
971
|
+
return undefined;
|
|
972
|
+
}
|
|
973
|
+
const clusterState = endpoint.state[cluster];
|
|
974
|
+
// Build result object by reading each property directly
|
|
975
|
+
const result = {};
|
|
976
|
+
// Get list of properties to read - use both approaches for maximum compatibility
|
|
977
|
+
const allKeys = new Set([
|
|
978
|
+
...Object.keys(clusterState),
|
|
979
|
+
...Object.getOwnPropertyNames(clusterState),
|
|
980
|
+
]);
|
|
981
|
+
for (const key of allKeys) {
|
|
982
|
+
try {
|
|
983
|
+
// Skip internal properties, methods, and symbols
|
|
984
|
+
if (key.startsWith('_') || key.startsWith('$')) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
// Try to read the value directly
|
|
988
|
+
const value = clusterState[key];
|
|
989
|
+
// Skip functions and undefined values
|
|
990
|
+
if (typeof value === 'function' || value === undefined) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
result[key] = value;
|
|
994
|
+
}
|
|
995
|
+
catch (propError) {
|
|
996
|
+
log.debug(`Could not read property ${key} from ${cluster}:`, propError);
|
|
997
|
+
}
|
|
588
998
|
}
|
|
589
|
-
|
|
590
|
-
log.
|
|
999
|
+
if (Object.keys(result).length === 0) {
|
|
1000
|
+
log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`);
|
|
1001
|
+
return undefined;
|
|
591
1002
|
}
|
|
1003
|
+
return result;
|
|
592
1004
|
}
|
|
593
1005
|
catch (error) {
|
|
594
|
-
log.error(`Failed to
|
|
595
|
-
|
|
1006
|
+
log.error(`Failed to get state for accessory ${uuid}:`, error);
|
|
1007
|
+
return undefined;
|
|
596
1008
|
}
|
|
597
1009
|
}
|
|
598
1010
|
/**
|
|
@@ -651,12 +1063,12 @@ export class MatterServer {
|
|
|
651
1063
|
}
|
|
652
1064
|
// Set up command handlers if provided
|
|
653
1065
|
if (accessory.handlers) {
|
|
1066
|
+
log.info(`Setting up handlers for accessory ${accessory.uuid}`);
|
|
1067
|
+
// Register handlers with the custom behavior classes
|
|
654
1068
|
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1069
|
+
log.info(` Processing cluster: ${clusterName}`);
|
|
655
1070
|
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
656
|
-
|
|
657
|
-
if (endpointWithEvents.events?.[clusterName]?.[`${commandName}$Action`]) {
|
|
658
|
-
endpointWithEvents.events[clusterName][`${commandName}$Action`].on(handler);
|
|
659
|
-
}
|
|
1071
|
+
registerHandler(accessory.uuid, clusterName, commandName, handler);
|
|
660
1072
|
}
|
|
661
1073
|
}
|
|
662
1074
|
}
|
|
@@ -671,9 +1083,14 @@ export class MatterServer {
|
|
|
671
1083
|
}
|
|
672
1084
|
this.isRunning = false;
|
|
673
1085
|
// Stop monitoring
|
|
1086
|
+
this.stopFabricMonitoring();
|
|
674
1087
|
networkMonitor.stopMonitoring();
|
|
675
|
-
diagnostics.stopDiagnostics();
|
|
676
1088
|
try {
|
|
1089
|
+
// Save accessory cache before shutting down (BEFORE clearing accessories!)
|
|
1090
|
+
if (this.accessoryCache && this.accessories.size > 0) {
|
|
1091
|
+
await this.accessoryCache.save(this.accessories);
|
|
1092
|
+
log.debug('Saved accessory cache before shutdown');
|
|
1093
|
+
}
|
|
677
1094
|
// Clean up all accessories
|
|
678
1095
|
for (const accessory of this.accessories.values()) {
|
|
679
1096
|
try {
|
|
@@ -820,5 +1237,40 @@ export class MatterServer {
|
|
|
820
1237
|
getClusters() {
|
|
821
1238
|
return clusters;
|
|
822
1239
|
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Remove a specific fabric (controller) from the bridge
|
|
1242
|
+
* This decommissions a single controller while leaving others intact
|
|
1243
|
+
*
|
|
1244
|
+
* @param fabricIndex - The fabric index to remove
|
|
1245
|
+
* @returns Promise that resolves when the fabric is removed
|
|
1246
|
+
*/
|
|
1247
|
+
async removeFabric(fabricIndex) {
|
|
1248
|
+
if (!this.serverNode) {
|
|
1249
|
+
throw new MatterDeviceError('Matter server not started');
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
log.info(`Removing fabric ${fabricIndex}...`);
|
|
1253
|
+
const serverState = this.serverNode;
|
|
1254
|
+
const removeFabric = serverState?.state?.commissioning?.removeFabric;
|
|
1255
|
+
if (typeof removeFabric !== 'function') {
|
|
1256
|
+
throw new MatterDeviceError('Fabric removal not supported by Matter.js version');
|
|
1257
|
+
}
|
|
1258
|
+
// Remove the fabric
|
|
1259
|
+
await removeFabric(fabricIndex);
|
|
1260
|
+
log.info(`Fabric ${fabricIndex} removed successfully`);
|
|
1261
|
+
// The fabric monitoring will detect this change and emit the appropriate events
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
log.error(`Failed to remove fabric ${fabricIndex}:`, error);
|
|
1265
|
+
throw new MatterDeviceError(`Failed to remove fabric: ${error.message}`, error);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Check if a specific fabric exists
|
|
1270
|
+
*/
|
|
1271
|
+
hasFabric(fabricIndex) {
|
|
1272
|
+
const fabrics = this.getFabricInfo();
|
|
1273
|
+
return fabrics.some(f => f.fabricIndex === fabricIndex);
|
|
1274
|
+
}
|
|
823
1275
|
}
|
|
824
1276
|
//# sourceMappingURL=matterServer.js.map
|