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