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