homebridge 2.0.0-alpha.43 → 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 +30 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +26 -0
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +3 -13
- 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 +0 -8
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +26 -127
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +0 -12
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +9 -25
- 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 +79 -5
- package/dist/matter/matterServer.d.ts.map +1 -1
- package/dist/matter/matterServer.js +487 -97
- package/dist/matter/matterServer.js.map +1 -1
- package/dist/matter/matterSharedTypes.d.ts +12 -4
- package/dist/matter/matterSharedTypes.d.ts.map +1 -1
- 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 +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 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -112
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- 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,17 +335,14 @@ 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 =
|
|
344
|
+
// Use default name for the Matter bridge
|
|
345
|
+
const bridgeName = 'Homebridge Matter Bridge';
|
|
262
346
|
// Sanitize the uniqueId to ensure it's filesystem-safe
|
|
263
347
|
// Replace any characters that could cause issues (colons, slashes, etc.)
|
|
264
348
|
// Use only alphanumeric and hyphens, collapse multiple hyphens, trim leading/trailing hyphens
|
|
@@ -278,15 +362,15 @@ export class MatterServer {
|
|
|
278
362
|
basicInformation: {
|
|
279
363
|
nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
|
|
280
364
|
vendorId: VendorId(this.vendorId),
|
|
281
|
-
vendorName: 'Homebridge'.slice(0, 32),
|
|
365
|
+
vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
|
|
282
366
|
productId: this.productId,
|
|
283
|
-
productName: 'Homebridge Matter Bridge'.slice(0, 32),
|
|
367
|
+
productName: (this.config.model || 'Homebridge Matter Bridge').slice(0, 32),
|
|
284
368
|
productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
|
|
285
|
-
serialNumber: this.serialNumber = this.generateSerialNumber(),
|
|
369
|
+
serialNumber: this.serialNumber = this.config.serialNumber || this.generateSerialNumber(),
|
|
286
370
|
hardwareVersion: 1,
|
|
287
371
|
hardwareVersionString: os.release(),
|
|
288
372
|
softwareVersion: 1,
|
|
289
|
-
softwareVersionString: getVersion(),
|
|
373
|
+
softwareVersionString: this.config.firmwareRevision || getVersion(),
|
|
290
374
|
reachable: true,
|
|
291
375
|
},
|
|
292
376
|
};
|
|
@@ -317,6 +401,12 @@ export class MatterServer {
|
|
|
317
401
|
});
|
|
318
402
|
// Wait for server to be ready
|
|
319
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();
|
|
320
410
|
this.isRunning = true;
|
|
321
411
|
log.info(`Matter server started successfully on port ${this.config.port}`);
|
|
322
412
|
log.info('Plugins can now register Matter accessories via the API');
|
|
@@ -362,6 +452,8 @@ export class MatterServer {
|
|
|
362
452
|
await fse.ensureDir(this.matterStoragePath);
|
|
363
453
|
// Create storage manager
|
|
364
454
|
this.storageManager = new MatterStorageManager(this.matterStoragePath);
|
|
455
|
+
// Create accessory cache
|
|
456
|
+
this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
|
|
365
457
|
// Configure environment to use our custom storage
|
|
366
458
|
const environment = Environment.default;
|
|
367
459
|
const storageService = environment.get(StorageService);
|
|
@@ -396,7 +488,8 @@ export class MatterServer {
|
|
|
396
488
|
if (!this.storageManager) {
|
|
397
489
|
throw new Error('Storage manager not initialized');
|
|
398
490
|
}
|
|
399
|
-
|
|
491
|
+
// Use 'credentials' namespace
|
|
492
|
+
const storage = this.storageManager.getStorage('credentials');
|
|
400
493
|
// CRITICAL: Initialize storage before reading to avoid race condition
|
|
401
494
|
await storage.initialize();
|
|
402
495
|
// Try to load existing credentials
|
|
@@ -508,6 +601,115 @@ export class MatterServer {
|
|
|
508
601
|
// Additional small delay to ensure everything is initialized
|
|
509
602
|
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
510
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
|
+
}
|
|
511
713
|
/**
|
|
512
714
|
* Register a Matter accessory (Plugin API)
|
|
513
715
|
*/
|
|
@@ -515,44 +717,127 @@ export class MatterServer {
|
|
|
515
717
|
if (!this.serverNode || !this.aggregator) {
|
|
516
718
|
throw new MatterDeviceError('Matter server not started');
|
|
517
719
|
}
|
|
518
|
-
// Validate required fields
|
|
720
|
+
// Validate required fields with helpful error messages
|
|
519
721
|
if (!accessory.deviceType) {
|
|
520
|
-
throw new MatterDeviceError(`
|
|
521
|
-
+ '
|
|
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.');
|
|
522
726
|
}
|
|
523
727
|
if (!accessory.uuid) {
|
|
524
|
-
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\')');
|
|
525
731
|
}
|
|
526
732
|
if (!accessory.displayName) {
|
|
527
|
-
throw new MatterDeviceError(
|
|
733
|
+
throw new MatterDeviceError(`Matter accessory (${accessory.uuid}) is missing required field 'displayName'.\n`
|
|
734
|
+
+ 'Example: displayName: \'Living Room Light\'');
|
|
528
735
|
}
|
|
529
736
|
if (!accessory.serialNumber) {
|
|
530
|
-
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');
|
|
531
739
|
}
|
|
532
740
|
if (!accessory.manufacturer) {
|
|
533
|
-
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\'');
|
|
534
743
|
}
|
|
535
744
|
if (!accessory.model) {
|
|
536
|
-
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\'');
|
|
537
747
|
}
|
|
538
748
|
if (!accessory.clusters || typeof accessory.clusters !== 'object') {
|
|
539
|
-
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
|
+
+ ' }');
|
|
540
755
|
}
|
|
541
|
-
// Check if already registered
|
|
756
|
+
// Check if already registered (during this session)
|
|
542
757
|
if (this.accessories.has(accessory.uuid)) {
|
|
543
|
-
|
|
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
|
+
}
|
|
544
788
|
}
|
|
545
789
|
// Check device limit
|
|
546
790
|
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
547
|
-
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}`);
|
|
548
794
|
}
|
|
549
795
|
try {
|
|
550
|
-
//
|
|
551
|
-
|
|
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, {
|
|
552
834
|
id: accessory.uuid,
|
|
553
835
|
});
|
|
554
836
|
// Add to aggregator FIRST (required before we can configure it)
|
|
555
837
|
await this.aggregator.add(endpoint);
|
|
838
|
+
if (this.config.debugModeEnabled) {
|
|
839
|
+
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
840
|
+
}
|
|
556
841
|
// NOW configure the endpoint
|
|
557
842
|
await this.configureEndpoint(endpoint, accessory);
|
|
558
843
|
// Store accessory
|
|
@@ -563,6 +848,17 @@ export class MatterServer {
|
|
|
563
848
|
};
|
|
564
849
|
this.accessories.set(accessory.uuid, internalAccessory);
|
|
565
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
|
+
}
|
|
566
862
|
}
|
|
567
863
|
catch (error) {
|
|
568
864
|
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
@@ -585,6 +881,15 @@ export class MatterServer {
|
|
|
585
881
|
}
|
|
586
882
|
this.accessories.delete(uuid);
|
|
587
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
|
+
}
|
|
588
893
|
}
|
|
589
894
|
catch (error) {
|
|
590
895
|
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
@@ -593,27 +898,113 @@ export class MatterServer {
|
|
|
593
898
|
}
|
|
594
899
|
/**
|
|
595
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.
|
|
596
904
|
*/
|
|
597
905
|
async updateAccessoryState(uuid, cluster, attributes) {
|
|
598
906
|
const accessory = this.accessories.get(uuid);
|
|
599
907
|
if (!accessory || !accessory.endpoint) {
|
|
600
908
|
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
601
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
|
+
}
|
|
602
962
|
try {
|
|
603
|
-
// Update the endpoint's cluster state
|
|
604
|
-
// Note: Endpoint types from Matter.js don't expose state properly, needs runtime check
|
|
605
963
|
const endpoint = accessory.endpoint;
|
|
606
|
-
if (endpoint.state
|
|
607
|
-
|
|
608
|
-
|
|
964
|
+
if (!endpoint.state) {
|
|
965
|
+
log.debug(`endpoint.state is undefined for ${accessory.displayName}`);
|
|
966
|
+
return undefined;
|
|
609
967
|
}
|
|
610
|
-
|
|
611
|
-
|
|
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;
|
|
612
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
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (Object.keys(result).length === 0) {
|
|
1000
|
+
log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`);
|
|
1001
|
+
return undefined;
|
|
1002
|
+
}
|
|
1003
|
+
return result;
|
|
613
1004
|
}
|
|
614
1005
|
catch (error) {
|
|
615
|
-
log.error(`Failed to
|
|
616
|
-
|
|
1006
|
+
log.error(`Failed to get state for accessory ${uuid}:`, error);
|
|
1007
|
+
return undefined;
|
|
617
1008
|
}
|
|
618
1009
|
}
|
|
619
1010
|
/**
|
|
@@ -673,54 +1064,13 @@ export class MatterServer {
|
|
|
673
1064
|
// Set up command handlers if provided
|
|
674
1065
|
if (accessory.handlers) {
|
|
675
1066
|
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
|
-
}
|
|
1067
|
+
// Register handlers with the custom behavior classes
|
|
1068
|
+
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1069
|
+
log.info(` Processing cluster: ${clusterName}`);
|
|
1070
|
+
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1071
|
+
registerHandler(accessory.uuid, clusterName, commandName, handler);
|
|
722
1072
|
}
|
|
723
|
-
}
|
|
1073
|
+
}
|
|
724
1074
|
}
|
|
725
1075
|
}
|
|
726
1076
|
/**
|
|
@@ -733,9 +1083,14 @@ export class MatterServer {
|
|
|
733
1083
|
}
|
|
734
1084
|
this.isRunning = false;
|
|
735
1085
|
// Stop monitoring
|
|
1086
|
+
this.stopFabricMonitoring();
|
|
736
1087
|
networkMonitor.stopMonitoring();
|
|
737
|
-
diagnostics.stopDiagnostics();
|
|
738
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
|
+
}
|
|
739
1094
|
// Clean up all accessories
|
|
740
1095
|
for (const accessory of this.accessories.values()) {
|
|
741
1096
|
try {
|
|
@@ -882,5 +1237,40 @@ export class MatterServer {
|
|
|
882
1237
|
getClusters() {
|
|
883
1238
|
return clusters;
|
|
884
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
|
+
}
|
|
885
1275
|
}
|
|
886
1276
|
//# sourceMappingURL=matterServer.js.map
|