homebridge 2.0.0-alpha.40 → 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/config-sample.json +1 -12
- package/dist/api.d.ts +46 -35
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +41 -35
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +7 -15
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +10 -7
- 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 +28 -1
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +43 -0
- 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 +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/ipcService.d.ts +21 -3
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js +15 -0
- package/dist/ipcService.js.map +1 -1
- package/dist/matter/index.d.ts +11 -0
- package/dist/matter/index.d.ts.map +1 -0
- package/dist/matter/index.js +11 -0
- package/dist/matter/index.js.map +1 -0
- package/dist/matter/matterConfigValidator.d.ts +26 -0
- package/dist/matter/matterConfigValidator.d.ts.map +1 -0
- package/dist/matter/matterConfigValidator.js +171 -0
- package/dist/matter/matterConfigValidator.js.map +1 -0
- package/dist/matter/matterDiagnostics.d.ts +121 -0
- package/dist/matter/matterDiagnostics.d.ts.map +1 -0
- package/dist/matter/matterDiagnostics.js +323 -0
- package/dist/matter/matterDiagnostics.js.map +1 -0
- package/dist/matter/matterErrorHandler.d.ts +94 -0
- package/dist/matter/matterErrorHandler.d.ts.map +1 -0
- package/dist/matter/matterErrorHandler.js +472 -0
- package/dist/matter/matterErrorHandler.js.map +1 -0
- package/dist/matter/matterNetworkMonitor.d.ts +65 -0
- package/dist/matter/matterNetworkMonitor.d.ts.map +1 -0
- package/dist/matter/matterNetworkMonitor.js +227 -0
- package/dist/matter/matterNetworkMonitor.js.map +1 -0
- package/dist/matter/matterServer.d.ts +196 -0
- package/dist/matter/matterServer.d.ts.map +1 -0
- package/dist/matter/matterServer.js +824 -0
- package/dist/matter/matterServer.js.map +1 -0
- package/dist/matter/matterSharedTypes.d.ts +147 -0
- package/dist/matter/matterSharedTypes.d.ts.map +1 -0
- package/dist/matter/matterSharedTypes.js +51 -0
- package/dist/matter/matterSharedTypes.js.map +1 -0
- 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 +163 -0
- package/dist/matter/matterTypes.d.ts.map +1 -0
- package/dist/matter/matterTypes.js +106 -0
- package/dist/matter/matterTypes.js.map +1 -0
- 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 +361 -51
- 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 +12 -13
- package/dist/matterConfigValidator.d.ts +0 -34
- package/dist/matterConfigValidator.d.ts.map +0 -1
- package/dist/matterConfigValidator.js +0 -249
- package/dist/matterConfigValidator.js.map +0 -1
- package/dist/matterService.d.ts +0 -168
- package/dist/matterService.d.ts.map +0 -1
- package/dist/matterService.js +0 -677
- package/dist/matterService.js.map +0 -1
- package/dist/matterTypes.d.ts +0 -20
- package/dist/matterTypes.d.ts.map +0 -1
- package/dist/matterTypes.js +0 -278
- package/dist/matterTypes.js.map +0 -1
- package/dist/util/matter-cli.d.ts +0 -3
- package/dist/util/matter-cli.d.ts.map +0 -1
- package/dist/util/matter-cli.js +0 -211
- package/dist/util/matter-cli.js.map +0 -1
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matter.js Server Implementation for Homebridge Plugin API
|
|
3
|
+
*
|
|
4
|
+
* This provides a Matter bridge that plugins can use to register
|
|
5
|
+
* Matter accessories via the Homebridge API.
|
|
6
|
+
*/
|
|
7
|
+
import * as crypto from 'node:crypto';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import { access } from 'node:fs/promises';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
import { Endpoint, Environment, ServerNode, StorageService, VendorId, } from '@matter/main';
|
|
14
|
+
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
15
|
+
import { ManualPairingCodeCodec, QrPairingCodeCodec, } from '@matter/types/schema';
|
|
16
|
+
import * as fse from 'fs-extra';
|
|
17
|
+
import QRCode from 'qrcode-terminal';
|
|
18
|
+
import { Logger } from '../logger.js';
|
|
19
|
+
import getVersion from '../version.js';
|
|
20
|
+
import { diagnostics } from './matterDiagnostics.js';
|
|
21
|
+
import { errorHandler } from './matterErrorHandler.js';
|
|
22
|
+
import { networkMonitor } from './matterNetworkMonitor.js';
|
|
23
|
+
import { MatterStorageManager } from './matterStorage.js';
|
|
24
|
+
import { clusters, deviceTypes, MatterDeviceError, } from './matterTypes.js';
|
|
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;
|
|
35
|
+
/**
|
|
36
|
+
* Matter Server for Homebridge Plugin API
|
|
37
|
+
* Allows plugins to register Matter accessories explicitly
|
|
38
|
+
*/
|
|
39
|
+
export class MatterServer {
|
|
40
|
+
config;
|
|
41
|
+
serverNode = null;
|
|
42
|
+
aggregator = null;
|
|
43
|
+
accessories = new Map();
|
|
44
|
+
isRunning = false;
|
|
45
|
+
MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
|
|
46
|
+
shutdownHandler = null;
|
|
47
|
+
// Internal commissioning values (generated, not user-configurable)
|
|
48
|
+
passcode = 0;
|
|
49
|
+
discriminator = 0;
|
|
50
|
+
vendorId;
|
|
51
|
+
productId;
|
|
52
|
+
commissioningInfo = {};
|
|
53
|
+
serialNumber;
|
|
54
|
+
cleanupHandlers = [];
|
|
55
|
+
storageManager = null;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
// Store the user config with defaults
|
|
59
|
+
this.config = {
|
|
60
|
+
port: config.port || DEFAULT_MATTER_PORT,
|
|
61
|
+
name: config.name || 'Homebridge Matter Bridge',
|
|
62
|
+
// Use a consistent uniqueId based on the name to ensure storage persistence
|
|
63
|
+
uniqueId: config.uniqueId || `homebridge-matter-${config.name?.replace(/[^a-z0-9]/gi, '-') || 'bridge'}`,
|
|
64
|
+
storagePath: config.storagePath,
|
|
65
|
+
};
|
|
66
|
+
// Initialize commissioning values (will be loaded from storage in start())
|
|
67
|
+
this.vendorId = DEFAULT_VENDOR_ID;
|
|
68
|
+
this.productId = DEFAULT_PRODUCT_ID;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
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
|
|
76
|
+
*/
|
|
77
|
+
generateSecurePasscode() {
|
|
78
|
+
let passcode;
|
|
79
|
+
const maxAttempts = MAX_PASSCODE_ATTEMPTS;
|
|
80
|
+
let attempts = 0;
|
|
81
|
+
const invalidPasscodes = [
|
|
82
|
+
0,
|
|
83
|
+
11111111,
|
|
84
|
+
22222222,
|
|
85
|
+
33333333,
|
|
86
|
+
44444444,
|
|
87
|
+
55555555,
|
|
88
|
+
66666666,
|
|
89
|
+
77777777,
|
|
90
|
+
88888888,
|
|
91
|
+
99999999,
|
|
92
|
+
12345678,
|
|
93
|
+
87654321,
|
|
94
|
+
];
|
|
95
|
+
do {
|
|
96
|
+
// Use cryptographically secure random number generation
|
|
97
|
+
const randomBytes = crypto.randomBytes(4);
|
|
98
|
+
const randomValue = randomBytes.readUInt32BE(0);
|
|
99
|
+
// Generate a value between 1 and 99999998
|
|
100
|
+
passcode = (randomValue % 99999998) + 1;
|
|
101
|
+
attempts++;
|
|
102
|
+
if (attempts > maxAttempts) {
|
|
103
|
+
throw new Error('Failed to generate secure passcode after maximum attempts');
|
|
104
|
+
}
|
|
105
|
+
} while (invalidPasscodes.includes(passcode)
|
|
106
|
+
|| !this.isValidPasscode(passcode));
|
|
107
|
+
return passcode;
|
|
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
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Generate a random discriminator
|
|
153
|
+
* According to Matter spec, discriminator must be:
|
|
154
|
+
* - 12 bits (0-4095)
|
|
155
|
+
* - Should be random for security
|
|
156
|
+
*/
|
|
157
|
+
generateRandomDiscriminator() {
|
|
158
|
+
// Generate cryptographically secure random 12-bit discriminator (0-4095)
|
|
159
|
+
const randomBytes = crypto.randomBytes(2);
|
|
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
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Start the Matter server
|
|
233
|
+
*/
|
|
234
|
+
async start() {
|
|
235
|
+
if (this.isRunning) {
|
|
236
|
+
log.warn('Matter server is already running');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
log.info('Starting Matter.js server...');
|
|
241
|
+
// IMPORTANT: Storage must be configured BEFORE any Matter.js operations
|
|
242
|
+
// This ensures persistent fabric data across restarts
|
|
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}`);
|
|
247
|
+
// Start network monitoring
|
|
248
|
+
networkMonitor.startMonitoring();
|
|
249
|
+
this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
|
|
250
|
+
// Start diagnostics
|
|
251
|
+
diagnostics.startDiagnostics();
|
|
252
|
+
this.cleanupHandlers.push(() => diagnostics.stopDiagnostics());
|
|
253
|
+
// Create commissioning options
|
|
254
|
+
const commissioningOptions = {
|
|
255
|
+
passcode: this.passcode,
|
|
256
|
+
discriminator: this.discriminator,
|
|
257
|
+
};
|
|
258
|
+
log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
|
|
259
|
+
// Ensure we have a name for the bridge
|
|
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, '');
|
|
265
|
+
// Create node options with proper typing
|
|
266
|
+
const nodeOptions = {
|
|
267
|
+
id: sanitizedId,
|
|
268
|
+
network: {
|
|
269
|
+
port: this.config.port,
|
|
270
|
+
ipv4: true, // Always enable IPv4 for Matter
|
|
271
|
+
},
|
|
272
|
+
commissioning: commissioningOptions,
|
|
273
|
+
productDescription: {
|
|
274
|
+
name: bridgeName,
|
|
275
|
+
deviceType: AggregatorEndpoint.deviceType,
|
|
276
|
+
},
|
|
277
|
+
basicInformation: {
|
|
278
|
+
nodeLabel: bridgeName.slice(0, 32), // Maximum 32 characters
|
|
279
|
+
vendorId: VendorId(this.vendorId),
|
|
280
|
+
vendorName: 'Homebridge'.slice(0, 32),
|
|
281
|
+
productId: this.productId,
|
|
282
|
+
productName: 'Homebridge Matter Bridge'.slice(0, 32),
|
|
283
|
+
productLabel: bridgeName.slice(0, 64), // Maximum 64 characters
|
|
284
|
+
serialNumber: this.serialNumber = this.generateSerialNumber(),
|
|
285
|
+
hardwareVersion: 1,
|
|
286
|
+
hardwareVersionString: os.release(),
|
|
287
|
+
softwareVersion: 1,
|
|
288
|
+
softwareVersionString: getVersion(),
|
|
289
|
+
reachable: true,
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
// Create server node with automatic recovery from corrupted storage
|
|
293
|
+
this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId);
|
|
294
|
+
// Create aggregator endpoint for bridge pattern
|
|
295
|
+
this.aggregator = new Endpoint(AggregatorEndpoint, {
|
|
296
|
+
id: 'homebridge-aggregator',
|
|
297
|
+
});
|
|
298
|
+
// Add aggregator to server
|
|
299
|
+
await this.serverNode.add(this.aggregator);
|
|
300
|
+
// Generate and display commissioning information
|
|
301
|
+
await this.generateCommissioningInfo();
|
|
302
|
+
// Set up graceful shutdown handler
|
|
303
|
+
this.shutdownHandler = async () => {
|
|
304
|
+
log.info('Shutting down Matter server...');
|
|
305
|
+
await this.stop();
|
|
306
|
+
};
|
|
307
|
+
// Register shutdown handlers
|
|
308
|
+
process.on('SIGINT', this.shutdownHandler);
|
|
309
|
+
process.on('SIGTERM', this.shutdownHandler);
|
|
310
|
+
// Start the server in a non-blocking way
|
|
311
|
+
this.serverNode.run().then(() => {
|
|
312
|
+
log.info('Matter server stopped normally');
|
|
313
|
+
}, (error) => {
|
|
314
|
+
log.error('Matter server stopped with error:', error);
|
|
315
|
+
errorHandler.handleError(error, 'server-runtime');
|
|
316
|
+
});
|
|
317
|
+
// Wait for server to be ready
|
|
318
|
+
await this.waitForServerReady();
|
|
319
|
+
this.isRunning = true;
|
|
320
|
+
log.info(`Matter server started successfully on port ${this.config.port}`);
|
|
321
|
+
log.info('Plugins can now register Matter accessories via the API');
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
log.error('Failed to start Matter server:', error);
|
|
325
|
+
await this.cleanup();
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Set up and validate storage
|
|
331
|
+
*/
|
|
332
|
+
async setupStorage() {
|
|
333
|
+
if (!this.config.storagePath) {
|
|
334
|
+
throw new Error('Storage path is required for Matter server');
|
|
335
|
+
}
|
|
336
|
+
// Resolve to absolute path and validate
|
|
337
|
+
const storagePath = path.resolve(this.config.storagePath);
|
|
338
|
+
const normalizedPath = path.normalize(storagePath);
|
|
339
|
+
// Ensure path is within allowed directories
|
|
340
|
+
const allowedBasePaths = [
|
|
341
|
+
path.resolve(os.homedir(), '.homebridge'),
|
|
342
|
+
path.resolve(process.cwd()),
|
|
343
|
+
'/var/lib/homebridge', // Common system location
|
|
344
|
+
];
|
|
345
|
+
const isAllowed = allowedBasePaths.some(basePath => normalizedPath.startsWith(basePath));
|
|
346
|
+
if (!isAllowed || normalizedPath.includes('..')) {
|
|
347
|
+
throw new Error(`Storage path not allowed: ${normalizedPath}. Must be within homebridge directories.`);
|
|
348
|
+
}
|
|
349
|
+
// Ensure the storage directory exists with proper permissions
|
|
350
|
+
try {
|
|
351
|
+
await fse.ensureDir(normalizedPath);
|
|
352
|
+
await access(normalizedPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
throw new Error(`Storage path not accessible: ${error}`);
|
|
356
|
+
}
|
|
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);
|
|
361
|
+
await fse.ensureDir(matterStoragePath);
|
|
362
|
+
// Create storage manager
|
|
363
|
+
this.storageManager = new MatterStorageManager(matterStoragePath);
|
|
364
|
+
// Configure environment to use our custom storage
|
|
365
|
+
const environment = Environment.default;
|
|
366
|
+
const storageService = environment.get(StorageService);
|
|
367
|
+
storageService.location = matterStoragePath;
|
|
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
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Generate serial number for the bridge
|
|
423
|
+
*/
|
|
424
|
+
generateSerialNumber() {
|
|
425
|
+
const timestamp = Date.now().toString(36).toUpperCase();
|
|
426
|
+
const random = crypto.randomBytes(2).toString('hex').toUpperCase();
|
|
427
|
+
return `HB-${timestamp}-${random}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Generate and display commissioning information
|
|
431
|
+
*/
|
|
432
|
+
async generateCommissioningInfo() {
|
|
433
|
+
const passcode = this.passcode.toString().padStart(8, '0');
|
|
434
|
+
const discriminator = this.discriminator;
|
|
435
|
+
const vendorId = this.vendorId;
|
|
436
|
+
const productId = this.productId;
|
|
437
|
+
// Use Matter.js library to generate pairing codes properly
|
|
438
|
+
const manualCode = ManualPairingCodeCodec.encode({
|
|
439
|
+
discriminator,
|
|
440
|
+
passcode: this.passcode,
|
|
441
|
+
});
|
|
442
|
+
// Format as XXXX-XXX-XXXX for display
|
|
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}`);
|
|
445
|
+
const qrCodePayload = QrPairingCodeCodec.encode([{
|
|
446
|
+
version: 0,
|
|
447
|
+
vendorId,
|
|
448
|
+
productId,
|
|
449
|
+
flowType: 0, // Standard commissioning flow
|
|
450
|
+
discoveryCapabilities: 4, // OnNetwork=4
|
|
451
|
+
discriminator,
|
|
452
|
+
passcode: this.passcode,
|
|
453
|
+
}]);
|
|
454
|
+
log.info(`Generated QR code: ${qrCodePayload}`);
|
|
455
|
+
log.info(`Generated manual code: ${manualPairingCode}`);
|
|
456
|
+
// Store commissioning info
|
|
457
|
+
this.commissioningInfo = {
|
|
458
|
+
qrCode: qrCodePayload,
|
|
459
|
+
manualPairingCode,
|
|
460
|
+
};
|
|
461
|
+
// Display commissioning information
|
|
462
|
+
log.info(`\n${'='.repeat(60)}`);
|
|
463
|
+
log.info('📱 MATTER COMMISSIONING INFORMATION');
|
|
464
|
+
log.info('='.repeat(60));
|
|
465
|
+
log.info(`Manual Pairing Code: ${manualPairingCode}`);
|
|
466
|
+
log.info(`Passcode: ${passcode}`);
|
|
467
|
+
log.info(`Discriminator: ${discriminator}`);
|
|
468
|
+
log.info('\nQR Code for commissioning:');
|
|
469
|
+
// Generate and display QR code in terminal
|
|
470
|
+
QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
|
|
471
|
+
// eslint-disable-next-line no-console
|
|
472
|
+
console.log(qrcode);
|
|
473
|
+
});
|
|
474
|
+
log.info(`${'='.repeat(60)}\n`);
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Wait for the server to be ready
|
|
478
|
+
*/
|
|
479
|
+
async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS) {
|
|
480
|
+
const startTime = Date.now();
|
|
481
|
+
while (!this.serverNode || !this.aggregator) {
|
|
482
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
483
|
+
throw new Error('Server failed to become ready within timeout');
|
|
484
|
+
}
|
|
485
|
+
await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS));
|
|
486
|
+
}
|
|
487
|
+
// Additional small delay to ensure everything is initialized
|
|
488
|
+
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Register a Matter accessory (Plugin API)
|
|
492
|
+
*/
|
|
493
|
+
async registerAccessory(accessory) {
|
|
494
|
+
if (!this.serverNode || !this.aggregator) {
|
|
495
|
+
throw new MatterDeviceError('Matter server not started');
|
|
496
|
+
}
|
|
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');
|
|
504
|
+
}
|
|
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`);
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
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)
|
|
534
|
+
await this.aggregator.add(endpoint);
|
|
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})`);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
548
|
+
throw new MatterDeviceError(`Failed to register accessory: ${error}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Unregister a Matter accessory (Plugin API)
|
|
553
|
+
*/
|
|
554
|
+
async unregisterAccessory(uuid) {
|
|
555
|
+
const accessory = this.accessories.get(uuid);
|
|
556
|
+
if (!accessory) {
|
|
557
|
+
log.debug(`Accessory ${uuid} not found, ignoring unregister request`);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
if (accessory.endpoint && this.aggregator) {
|
|
562
|
+
await accessory.endpoint.close();
|
|
563
|
+
log.debug(`Removed endpoint for ${accessory.displayName}`);
|
|
564
|
+
}
|
|
565
|
+
this.accessories.delete(uuid);
|
|
566
|
+
log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
570
|
+
throw new MatterDeviceError(`Failed to unregister accessory: ${error}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Update a Matter accessory's state (Plugin API)
|
|
575
|
+
*/
|
|
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`);
|
|
580
|
+
}
|
|
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);
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
log.warn(`Cluster ${cluster} not found on accessory ${accessory.displayName}`);
|
|
591
|
+
}
|
|
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
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get all registered accessories (Plugin API)
|
|
600
|
+
*/
|
|
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
|
+
});
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get a specific accessory by UUID (Plugin API)
|
|
611
|
+
*/
|
|
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;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Configure a Matter endpoint after it's been added to the aggregator
|
|
624
|
+
*/
|
|
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 });
|
|
651
|
+
}
|
|
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
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Stop the Matter server
|
|
666
|
+
*/
|
|
667
|
+
async stop() {
|
|
668
|
+
if (!this.isRunning) {
|
|
669
|
+
log.debug('Matter server is not running');
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.isRunning = false;
|
|
673
|
+
// Stop monitoring
|
|
674
|
+
networkMonitor.stopMonitoring();
|
|
675
|
+
diagnostics.stopDiagnostics();
|
|
676
|
+
try {
|
|
677
|
+
// Clean up all accessories
|
|
678
|
+
for (const accessory of this.accessories.values()) {
|
|
679
|
+
try {
|
|
680
|
+
if (accessory.endpoint) {
|
|
681
|
+
await accessory.endpoint.close();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
log.error('Failed to clean up accessory:', error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
this.accessories.clear();
|
|
689
|
+
// Stop server
|
|
690
|
+
if (this.serverNode) {
|
|
691
|
+
await this.serverNode.close();
|
|
692
|
+
}
|
|
693
|
+
await this.cleanup();
|
|
694
|
+
log.info('Matter server stopped');
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
log.error('Error stopping Matter server:', error);
|
|
698
|
+
await errorHandler.handleError(error, 'server-stop');
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
this.isRunning = false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Cleanup resources
|
|
707
|
+
*/
|
|
708
|
+
async cleanup() {
|
|
709
|
+
// Remove signal handlers
|
|
710
|
+
if (this.shutdownHandler) {
|
|
711
|
+
process.off('SIGINT', this.shutdownHandler);
|
|
712
|
+
process.off('SIGTERM', this.shutdownHandler);
|
|
713
|
+
this.shutdownHandler = null;
|
|
714
|
+
}
|
|
715
|
+
// Run all cleanup handlers
|
|
716
|
+
for (const handler of this.cleanupHandlers) {
|
|
717
|
+
try {
|
|
718
|
+
await handler();
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
log.debug('Error during cleanup handler:', error);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
this.cleanupHandlers = [];
|
|
725
|
+
// Clear references
|
|
726
|
+
this.serverNode = null;
|
|
727
|
+
this.aggregator = null;
|
|
728
|
+
this.isRunning = false;
|
|
729
|
+
this.commissioningInfo = {};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Get fabric information for commissioned controllers
|
|
733
|
+
*/
|
|
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;
|
|
802
|
+
}
|
|
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;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
//# sourceMappingURL=matterServer.js.map
|