matterbridge 1.0.6

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.
Files changed (56) hide show
  1. package/.eslintrc.json +45 -0
  2. package/.gitattributes +2 -0
  3. package/.prettierignore +2 -0
  4. package/.prettierrc.json +12 -0
  5. package/LICENSE +21 -0
  6. package/README.md +19 -0
  7. package/dist/AirQualityCluster.d.ts +147 -0
  8. package/dist/AirQualityCluster.d.ts.map +1 -0
  9. package/dist/AirQualityCluster.js +73 -0
  10. package/dist/AirQualityCluster.js.map +1 -0
  11. package/dist/ColorControlServer.d.ts +69 -0
  12. package/dist/ColorControlServer.d.ts.map +1 -0
  13. package/dist/ColorControlServer.js +85 -0
  14. package/dist/ColorControlServer.js.map +1 -0
  15. package/dist/index.d.ts +7 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +9 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/matterbridge.d.ts +70 -0
  20. package/dist/matterbridge.d.ts.map +1 -0
  21. package/dist/matterbridge.js +719 -0
  22. package/dist/matterbridge.js.map +1 -0
  23. package/dist/matterbridgeAccessoryPlatform.d.ts +20 -0
  24. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  25. package/dist/matterbridgeAccessoryPlatform.js +53 -0
  26. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  27. package/dist/matterbridgeDevice.d.ts +57 -0
  28. package/dist/matterbridgeDevice.d.ts.map +1 -0
  29. package/dist/matterbridgeDevice.js +289 -0
  30. package/dist/matterbridgeDevice.js.map +1 -0
  31. package/dist/matterbridgeDynamicPlatform.d.ts +20 -0
  32. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  33. package/dist/matterbridgeDynamicPlatform.js +53 -0
  34. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  35. package/frontend/README.md +70 -0
  36. package/frontend/build/asset-manifest.json +15 -0
  37. package/frontend/build/favicon.ico +0 -0
  38. package/frontend/build/index.html +1 -0
  39. package/frontend/build/manifest.json +15 -0
  40. package/frontend/build/matter.png +0 -0
  41. package/frontend/build/robots.txt +3 -0
  42. package/frontend/build/static/css/main.8b969fd5.css +2 -0
  43. package/frontend/build/static/css/main.8b969fd5.css.map +1 -0
  44. package/frontend/build/static/js/453.8ab44547.chunk.js +2 -0
  45. package/frontend/build/static/js/453.8ab44547.chunk.js.map +1 -0
  46. package/frontend/build/static/js/main.a000062f.js +3 -0
  47. package/frontend/build/static/js/main.a000062f.js.LICENSE.txt +78 -0
  48. package/frontend/build/static/js/main.a000062f.js.map +1 -0
  49. package/frontend/package-lock.json +18351 -0
  50. package/frontend/package.json +40 -0
  51. package/frontend/public/favicon.ico +0 -0
  52. package/frontend/public/index.html +15 -0
  53. package/frontend/public/manifest.json +15 -0
  54. package/frontend/public/matter.png +0 -0
  55. package/frontend/public/robots.txt +3 -0
  56. package/package.json +65 -0
@@ -0,0 +1,719 @@
1
+ import { NodeStorageManager } from 'node-persist-manager';
2
+ import { AnsiLogger, BLUE, BRIGHT, GREEN, RESET, REVERSE, REVERSEOFF, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, nf, rs, } from 'node-ansi-logger';
3
+ import { fileURLToPath, pathToFileURL } from 'url';
4
+ import { promises as fs } from 'fs';
5
+ import EventEmitter from 'events';
6
+ import express from 'express';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ import { CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
10
+ import { VendorId } from '@project-chip/matter-node.js/datatype';
11
+ import { Aggregator, DeviceTypes } from '@project-chip/matter-node.js/device';
12
+ import { Format, Level, Logger } from '@project-chip/matter-node.js/log';
13
+ import { QrCodeSchema } from '@project-chip/matter-node.js/schema';
14
+ import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
15
+ import { requireMinNodeVersion, getParameter, hasParameter } from '@project-chip/matter-node.js/util';
16
+ import { CryptoNode } from '@project-chip/matter-node.js/crypto';
17
+ import { BasicInformationCluster, BridgedDeviceBasicInformationCluster } from '@project-chip/matter-node.js/cluster';
18
+ export class Matterbridge extends EventEmitter {
19
+ systemInformation = {
20
+ ipv4Address: '',
21
+ ipv6Address: '',
22
+ nodeVersion: '',
23
+ hostname: '',
24
+ osType: '',
25
+ osRelease: '',
26
+ osPlatform: '',
27
+ osArch: '',
28
+ totalMemory: '',
29
+ freeMemory: '',
30
+ systemUptime: '',
31
+ };
32
+ rootDirectory;
33
+ bridgeMode = '';
34
+ log;
35
+ hasCleanupStarted = false;
36
+ registeredPlugins = [];
37
+ registeredDevices = [];
38
+ storage = undefined;
39
+ context = undefined;
40
+ app;
41
+ storageManager;
42
+ matterbridgeContext;
43
+ mattercontrollerContext;
44
+ matterServer;
45
+ matterAggregator;
46
+ commissioningServer;
47
+ commissioningController;
48
+ constructor() {
49
+ super();
50
+ // set Matterbridge logger
51
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ });
52
+ this.log.info('Matterbridge is running...');
53
+ // log system info
54
+ this.logNodeAndSystemInfo();
55
+ // check node version and throw error
56
+ requireMinNodeVersion(18);
57
+ // register SIGINT SIGTERM signal handlers
58
+ this.registerSignalHandlers();
59
+ // set matter.js logger level and format
60
+ Logger.defaultLogLevel = Level.DEBUG;
61
+ Logger.format = Format.ANSI;
62
+ this.initialize();
63
+ }
64
+ async initialize() {
65
+ // Initialize NodeStorage
66
+ this.storage = new NodeStorageManager();
67
+ this.context = await this.storage.createStorage('matterbridge');
68
+ this.registeredPlugins = await this.context?.get('plugins', []);
69
+ // Initialize frontend
70
+ this.initializeFrontend();
71
+ // Parse command line
72
+ await this.parseCommandLine();
73
+ }
74
+ async parseCommandLine() {
75
+ if (hasParameter('help')) {
76
+ this.log.info(`\nmatterbridge -help -bridge -add <plugin path> -remove <plugin path>
77
+ - help: show the help
78
+ - bridge: start the bridge
79
+ - list: list the registered plugin
80
+ - add <plugin path>: register the plugin
81
+ - remove <plugin path>: remove the plugin\n`);
82
+ process.exit(0);
83
+ }
84
+ if (hasParameter('list')) {
85
+ this.log.info('Registered plugins:');
86
+ this.registeredPlugins.forEach((plugin) => {
87
+ this.log.info(`- ${BLUE}${plugin.name}${nf} '${BLUE}${BRIGHT}${plugin.description}${RESET}${nf}' type: ${GREEN}${plugin.type}${nf}`);
88
+ });
89
+ process.exit(0);
90
+ }
91
+ if (getParameter('add')) {
92
+ this.log.debug(`Registering plugin ${getParameter('add')}`);
93
+ await this.loadPlugin(getParameter('add'), 'add');
94
+ process.exit(0);
95
+ }
96
+ if (getParameter('remove')) {
97
+ this.log.debug(`Unregistering plugin ${getParameter('remove')}`);
98
+ await this.loadPlugin(getParameter('remove'), 'remove');
99
+ process.exit(0);
100
+ }
101
+ await this.startStorage('json', '.matterbridge.json');
102
+ if (hasParameter('childbridge')) {
103
+ this.bridgeMode = 'childbridge';
104
+ this.registeredPlugins.forEach(async (plugin) => {
105
+ this.log.info(`Loading plugin ${BLUE}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
106
+ await this.loadPlugin(plugin.path, 'load');
107
+ });
108
+ await this.startMatterBridge();
109
+ }
110
+ if (hasParameter('bridge')) {
111
+ this.bridgeMode = 'bridge';
112
+ this.registeredPlugins.forEach(async (plugin) => {
113
+ this.log.info(`Loading plugin ${BLUE}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
114
+ await this.loadPlugin(plugin.path, 'load');
115
+ });
116
+ await this.startMatterBridge();
117
+ }
118
+ }
119
+ // Typed method for emitting events
120
+ emit(event, ...args) {
121
+ return super.emit(event, ...args);
122
+ }
123
+ // Typed method for listening to events
124
+ on(event, listener) {
125
+ super.on(event, listener);
126
+ return this;
127
+ }
128
+ async loadPlugin(packageJsonPath, mode = 'load') {
129
+ if (!packageJsonPath.endsWith('package.json'))
130
+ packageJsonPath = path.join(packageJsonPath, 'package.json');
131
+ this.log.debug(`Loading plugin ${BLUE}${packageJsonPath}${RESET}`);
132
+ try {
133
+ // Load the package.json of the plugin
134
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
135
+ // Resolve the main module path relative to package.json
136
+ const pluginPath = path.resolve(path.dirname(packageJsonPath), packageJson.main);
137
+ // Convert the file path to a URL
138
+ const pluginUrl = pathToFileURL(pluginPath);
139
+ // Dynamically import the plugin
140
+ this.log.debug(`Importing plugin ${BLUE}${pluginUrl.href}${RESET}`);
141
+ const plugin = await import(pluginUrl.href);
142
+ // Call the default export function of the plugin, passing this MatterBridge instance
143
+ if (plugin.default) {
144
+ const platform = plugin.default(this, new AnsiLogger({ logName: packageJson.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ }));
145
+ platform.name = packageJson.name;
146
+ if (mode === 'load') {
147
+ this.log.info(`Plugin ${BLUE}${packageJsonPath}${RESET} type ${GREEN}${platform.type}${RESET} loaded (entrypoint ${UNDERLINE}${pluginPath}${UNDERLINEOFF})`);
148
+ // Update plugin info
149
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === packageJson.name);
150
+ if (plugin) {
151
+ plugin.name = packageJson.name;
152
+ plugin.description = packageJson.description;
153
+ plugin.version = packageJson.version;
154
+ plugin.author = packageJson.author;
155
+ plugin.type = platform.type;
156
+ plugin.loaded = true;
157
+ }
158
+ else {
159
+ this.log.error(`Plugin ${packageJson.name} not found`);
160
+ }
161
+ // Register handlers
162
+ if (platform.type === 'AccessoryPlatform') {
163
+ platform.on('registerDeviceAccessoryPlatform', (device) => {
164
+ this.log.debug(`Received ${REVERSE}registerDeviceAccessoryPlatform${REVERSEOFF} for device ${device.name}`);
165
+ });
166
+ }
167
+ else if (platform.type === 'DynamicPlatform') {
168
+ platform.on('registerDeviceDynamicPlatform', (device) => {
169
+ this.log.debug(`Received ${REVERSE}registerDeviceDynamicPlatform${REVERSEOFF} for device ${device.name}`);
170
+ });
171
+ }
172
+ else {
173
+ this.log.error(`loadPlugin error platform.type ${REVERSE}${platform.type}${REVERSEOFF} for plugin ${packageJson.name}`);
174
+ }
175
+ }
176
+ else if (mode === 'add') {
177
+ if (!this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
178
+ this.registeredPlugins.push({
179
+ path: packageJsonPath,
180
+ type: platform.type,
181
+ name: packageJson.name,
182
+ version: packageJson.version,
183
+ description: packageJson.description,
184
+ author: packageJson.author,
185
+ });
186
+ await this.context?.set('plugins', this.registeredPlugins);
187
+ this.log.info(`Plugin ${packageJsonPath} type ${platform.type} added to matterbridge`);
188
+ }
189
+ else {
190
+ this.log.warn(`Plugin ${packageJsonPath} already added to matterbridge`);
191
+ }
192
+ }
193
+ else if (mode === 'remove') {
194
+ if (this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
195
+ this.registeredPlugins.splice(this.registeredPlugins.findIndex((plugin) => plugin.name === packageJson.name));
196
+ await this.context?.set('plugins', this.registeredPlugins);
197
+ this.log.info(`Plugin ${packageJsonPath} removed from matterbridge`);
198
+ }
199
+ else {
200
+ this.log.warn(`Plugin ${packageJsonPath} not registerd in matterbridge`);
201
+ }
202
+ }
203
+ }
204
+ else {
205
+ this.log.error(`Plugin at ${pluginPath} does not provide a default export`);
206
+ }
207
+ }
208
+ catch (err) {
209
+ this.log.error(`Failed to load plugin from ${packageJsonPath}: ${err}`);
210
+ }
211
+ }
212
+ registerSignalHandlers() {
213
+ process.on('SIGINT', async () => {
214
+ await this.cleanup('SIGINT received, cleaning up...');
215
+ });
216
+ process.on('SIGTERM', async () => {
217
+ await this.cleanup('SIGTERM received, cleaning up...');
218
+ });
219
+ }
220
+ async cleanup(message) {
221
+ if (!this.hasCleanupStarted) {
222
+ this.hasCleanupStarted = true;
223
+ this.log.debug(message);
224
+ // Emitting the shutdown event with a reason
225
+ this.emit('shutdown', 'Matterbridge is closing: ' + message);
226
+ // Set reachability to false
227
+ this.log.debug(`*Changing reachability for ${this.registeredDevices.length} devices:`);
228
+ this.registeredDevices.forEach((device) => {
229
+ this.log.debug(`*--child device: ${device.device.name}`);
230
+ if (this.bridgeMode === 'bridge')
231
+ device.device.setBridgedDeviceReachability(false);
232
+ });
233
+ setTimeout(async () => {
234
+ // Closing matter
235
+ await this.stopMatter();
236
+ // Closing storage
237
+ await this.stopStorage();
238
+ this.log.debug('Cleanup completed.');
239
+ process.exit(0);
240
+ }, this.bridgeMode === 'bridge' ? 2000 : 0);
241
+ }
242
+ }
243
+ async addDevice(pluginName, device) {
244
+ if (this.bridgeMode === 'bridge') {
245
+ const basic = device.getClusterServerById(BasicInformationCluster.id);
246
+ if (!basic) {
247
+ this.log.error('addDevice error: cannot find the BasicInformationCluster');
248
+ return;
249
+ }
250
+ device.createDefaultBridgedDeviceBasicInformationClusterServer(basic.getNodeLabelAttribute(), basic.getUniqueIdAttribute(), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductNameAttribute());
251
+ this.matterAggregator.addBridgedDevice(device);
252
+ this.registeredDevices.push({ plugin: pluginName, device });
253
+ this.log.debug(`addDevice called from plugin ${pluginName}`);
254
+ }
255
+ if (this.bridgeMode === 'childbridge') {
256
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
257
+ if (plugin) {
258
+ plugin.started = true;
259
+ this.registeredDevices.push({ plugin: pluginName, device });
260
+ this.log.debug(`addDevice called from plugin ${pluginName}`);
261
+ }
262
+ else {
263
+ this.log.error(`addDevice error: plugin ${pluginName} not found`);
264
+ }
265
+ }
266
+ }
267
+ async addBridgedDevice(pluginName, device) {
268
+ if (this.bridgeMode === 'bridge') {
269
+ this.matterAggregator.addBridgedDevice(device);
270
+ this.registeredDevices.push({ plugin: pluginName, device });
271
+ this.log.debug(`addBridgedDevice called from plugin ${pluginName}`);
272
+ }
273
+ if (this.bridgeMode === 'childbridge') {
274
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
275
+ if (plugin) {
276
+ plugin.started = true;
277
+ this.registeredDevices.push({ plugin: pluginName, device });
278
+ this.log.debug(`addBridgedDevice called from plugin ${pluginName}`);
279
+ }
280
+ else {
281
+ this.log.error(`addBridgedDevice error: plugin ${pluginName} not found`);
282
+ }
283
+ }
284
+ }
285
+ async startStorage(storageType, storageName) {
286
+ if (!storageName.endsWith('.json')) {
287
+ storageName += '.json';
288
+ }
289
+ this.log.debug(`Starting storage ${storageType} ${storageName}`);
290
+ if (storageType === 'disk') {
291
+ const storageDisk = new StorageBackendDisk(storageName);
292
+ this.storageManager = new StorageManager(storageDisk);
293
+ }
294
+ if (storageType === 'json') {
295
+ const storageJson = new StorageBackendJsonFile(storageName);
296
+ this.storageManager = new StorageManager(storageJson);
297
+ }
298
+ try {
299
+ await this.storageManager.initialize();
300
+ this.log.debug('Storage initialized');
301
+ if (storageType === 'json') {
302
+ this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
303
+ }
304
+ }
305
+ catch (error) {
306
+ this.log.error('Storage initialize() error!');
307
+ process.exit(1);
308
+ }
309
+ }
310
+ async backupJsonStorage(storageName, backupName) {
311
+ try {
312
+ this.log.debug(`Making backup copy of ${storageName}`);
313
+ await fs.copyFile(storageName, backupName);
314
+ this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
315
+ }
316
+ catch (err) {
317
+ if (err instanceof Error && 'code' in err) {
318
+ if (err.code === 'ENOENT') {
319
+ this.log.info(`No existing file to back up for ${storageName}. This is expected on the first run.`);
320
+ }
321
+ else {
322
+ this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
323
+ }
324
+ }
325
+ else {
326
+ this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
327
+ }
328
+ }
329
+ }
330
+ async stopStorage() {
331
+ this.log.debug('Stopping storage');
332
+ await this.storageManager?.close();
333
+ this.log.debug('Storage closed');
334
+ }
335
+ async startMatterBridge() {
336
+ this.log.debug('Starting matterbridge in mode', this.bridgeMode);
337
+ await this.createMatterServer(this.storageManager);
338
+ this.log.debug('Creating matterbridge context: matterbridge');
339
+ this.matterbridgeContext = this.storageManager.createContext('matterbridge');
340
+ this.matterbridgeContext.set('port', 5500);
341
+ this.matterbridgeContext.set('passcode', 20232024);
342
+ this.matterbridgeContext.set('discriminator', 3940);
343
+ this.matterbridgeContext.set('deviceName', 'matterbridge aggregator');
344
+ this.matterbridgeContext.set('deviceType', DeviceTypes.AGGREGATOR.code);
345
+ this.matterbridgeContext.set('vendorId', 0xfff1);
346
+ this.matterbridgeContext.set('vendorName', 'matterbridge');
347
+ this.matterbridgeContext.set('productId', 0x8000);
348
+ this.matterbridgeContext.set('productName', 'node-matterbridge');
349
+ this.matterbridgeContext.set('uniqueId', this.matterbridgeContext.get('uniqueId', CryptoNode.getRandomData(8).toHex()));
350
+ this.log.debug('Creating matterbridge commissioning server');
351
+ this.commissioningServer = await this.createMatterCommisioningServer(this.matterbridgeContext, 'Matterbridge');
352
+ if (this.bridgeMode === 'bridge') {
353
+ this.log.debug('Creating matter aggregator for matterbridge');
354
+ this.matterAggregator = await this.createMatterAggregator();
355
+ this.log.debug('Adding matter aggregator to commissioning server');
356
+ this.commissioningServer.addDevice(this.matterAggregator);
357
+ this.matterServer.addCommissioningServer(this.commissioningServer);
358
+ this.log.debug('Starting matter server');
359
+ await this.matterServer.start();
360
+ this.log.debug('Started matter server');
361
+ this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
362
+ }
363
+ if (this.bridgeMode === 'childbridge') {
364
+ this.log.debug('Creating matter aggregator for matterbridge');
365
+ this.matterAggregator = await this.createMatterAggregator();
366
+ this.log.debug('Adding matter aggregator to commissioning server');
367
+ this.commissioningServer.addDevice(this.matterAggregator);
368
+ this.matterServer.addCommissioningServer(this.commissioningServer);
369
+ this.registeredPlugins.forEach(async (plugin) => {
370
+ // Start the interval to check if all plugins are loaded
371
+ const loadedInterval = setInterval(async () => {
372
+ this.log.debug(`Waiting for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
373
+ if (!plugin.loaded)
374
+ return;
375
+ this.log.debug(`Sending start platform for plugin ${BLUE}${plugin.name}${db}`);
376
+ this.emit('startAccessoryPlatform', 'Matterbridge is commissioned and controllers are connected');
377
+ this.emit('startDynamicPlatform', 'Matterbridge is commissioned and controllers are connected');
378
+ clearInterval(loadedInterval);
379
+ }, 1000);
380
+ // Start the interval to check if all plugins are started
381
+ const startedInterval = setInterval(async () => {
382
+ this.log.debug(`**Waiting for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
383
+ if (!plugin.started)
384
+ return;
385
+ this.log.debug(`**Creating storage context for plugin ${BLUE}${plugin.name}${db}`);
386
+ plugin.storageContext = this.storageManager.createContext(plugin.name);
387
+ //plugin.storageContext.set('port', undefined);
388
+ //plugin.storageContext.set('passcode', undefined);
389
+ //plugin.storageContext.set('discriminator', undefined);
390
+ plugin.storageContext.set('deviceName', 'matterbridge aggregator');
391
+ plugin.storageContext.set('deviceType', DeviceTypes.AGGREGATOR.code);
392
+ plugin.storageContext.set('vendorId', 0xfff1);
393
+ plugin.storageContext.set('vendorName', 'matterbridge');
394
+ plugin.storageContext.set('productId', 0x8000);
395
+ plugin.storageContext.set('productName', plugin.name.slice(0, 32));
396
+ plugin.storageContext.set('uniqueId', plugin.storageContext.get('uniqueId', CryptoNode.getRandomData(8).toHex()));
397
+ this.log.debug(`**Creating commissioning server for plugin ${BLUE}${plugin.name}${db}`);
398
+ plugin.commissioningServer = await this.createMatterCommisioningServer(plugin.storageContext, plugin.name);
399
+ if (plugin.type === 'AccessoryPlatform') {
400
+ this.registeredDevices.forEach((registeredDevice) => {
401
+ if (registeredDevice.plugin === plugin.name) {
402
+ plugin.commissioningServer?.addDevice(registeredDevice.device);
403
+ this.matterServer.addCommissioningServer(plugin.commissioningServer);
404
+ return;
405
+ }
406
+ });
407
+ }
408
+ if (plugin.type === 'DynamicPlatform') {
409
+ this.log.debug(`**Creating aggregator for plugin ${BLUE}${plugin.name}${db}`);
410
+ plugin.aggregator = await this.createMatterAggregator();
411
+ this.log.debug(`**Adding matter aggregator to commissioning server for plugin ${BLUE}${plugin.name}${db}`);
412
+ plugin.commissioningServer?.addDevice(plugin.aggregator);
413
+ this.registeredDevices.forEach((registeredDevice) => {
414
+ if (registeredDevice.plugin === plugin.name)
415
+ plugin.aggregator?.addBridgedDevice(registeredDevice.device);
416
+ });
417
+ this.matterServer.addCommissioningServer(plugin.commissioningServer);
418
+ }
419
+ clearInterval(startedInterval);
420
+ }, 1000);
421
+ });
422
+ // Start the interval to check if all plugins are loaded and started and so start the matter server
423
+ const startMatterInterval = setInterval(async () => {
424
+ let allStarted = true;
425
+ this.registeredPlugins.forEach(async (plugin) => {
426
+ this.log.debug(`***Waiting in startMatter interval for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
427
+ if (!plugin.loaded || !plugin.started)
428
+ allStarted = false;
429
+ });
430
+ if (!allStarted)
431
+ return;
432
+ this.log.debug('***Starting matter server from startMatter interval');
433
+ await this.matterServer.start();
434
+ this.log.debug('***Started matter server from startMatter interval');
435
+ this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
436
+ this.registeredPlugins.forEach(async (plugin) => {
437
+ this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.name);
438
+ });
439
+ clearInterval(startMatterInterval);
440
+ }, 1000);
441
+ return;
442
+ }
443
+ }
444
+ showCommissioningQRCode(commissioningServer, storageContext, name) {
445
+ if (!commissioningServer || !storageContext || !name)
446
+ return;
447
+ if (!commissioningServer.isCommissioned()) {
448
+ this.log.info(`***The commissioning server for ${BLUE}${name}${nf} is not commissioned. Pair it and restart the process to run matterbridge.`);
449
+ const { qrPairingCode, manualPairingCode } = commissioningServer.getPairingCode();
450
+ storageContext.set('qrPairingCode', qrPairingCode);
451
+ storageContext.set('manualPairingCode', manualPairingCode);
452
+ const QrCode = new QrCodeSchema();
453
+ this.log.debug(`Pairing code\n\n${QrCode.encode(qrPairingCode)}\nManual pairing code: ${manualPairingCode}\n`);
454
+ }
455
+ else {
456
+ this.log.info(`***The commissioning server for ${BLUE}${name}${nf} is already commissioned. Waiting for controllers to connect ...`);
457
+ }
458
+ }
459
+ async createMatterCommisioningServer(context, name) {
460
+ //const port = context.get('port') as number;
461
+ //const passcode = context.get('passcode') as number;
462
+ //const discriminator = context.get('discriminator') as number;
463
+ const deviceName = context.get('deviceName');
464
+ const deviceType = context.get('deviceType');
465
+ const vendorId = context.get('vendorId');
466
+ const vendorName = context.get('vendorName'); // Home app = Manufacturer
467
+ const productId = context.get('productId');
468
+ const productName = context.get('productName'); // Home app = Model
469
+ const uniqueId = context.get('uniqueId');
470
+ // eslint-disable-next-line max-len
471
+ //this.log.debug(`Creating matter commissioning server with port ${port} passcode ${passcode} discriminator ${discriminator} deviceName ${deviceName} deviceType ${deviceType}`);
472
+ this.log.debug(`Creating matter commissioning server with deviceName ${deviceName} deviceType ${deviceType}`);
473
+ const commissioningServer = new CommissioningServer({
474
+ port: undefined,
475
+ passcode: undefined,
476
+ discriminator: undefined,
477
+ deviceName,
478
+ deviceType,
479
+ basicInformation: {
480
+ vendorId: VendorId(vendorId),
481
+ vendorName,
482
+ productId,
483
+ productName,
484
+ nodeLabel: productName,
485
+ productLabel: productName,
486
+ softwareVersion: 1,
487
+ softwareVersionString: '1.0.0', // Home app = Firmware Revision
488
+ hardwareVersion: 1,
489
+ hardwareVersionString: '1.0.0',
490
+ uniqueId,
491
+ serialNumber: `matterbridge-${uniqueId}`,
492
+ reachable: true,
493
+ },
494
+ activeSessionsChangedCallback: (fabricIndex) => {
495
+ const info = commissioningServer.getActiveSessionInformation(fabricIndex);
496
+ this.log.debug(`Active sessions changed on fabric ${fabricIndex}`, debugStringify(info));
497
+ if (info && info[0]?.isPeerActive === true && info[0]?.secure === true && info[0]?.numberOfActiveSubscriptions >= 1) {
498
+ this.log.info(`Controller connected to ${BLUE}${name}${nf} ready to start...`);
499
+ Logger.defaultLogLevel = Level.INFO;
500
+ setTimeout(() => {
501
+ if (this.bridgeMode === 'bridge') {
502
+ this.emit('startAccessoryPlatform', 'Matterbridge is commissioned and controllers are connected');
503
+ this.emit('startDynamicPlatform', 'Matterbridge is commissioned and controllers are connected');
504
+ }
505
+ }, 2000);
506
+ }
507
+ },
508
+ commissioningChangedCallback: (fabricIndex) => {
509
+ const info = commissioningServer.getCommissionedFabricInformation(fabricIndex);
510
+ this.log.debug(`Commissioning changed on fabric ${fabricIndex}`, debugStringify(info));
511
+ },
512
+ });
513
+ commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
514
+ return commissioningServer;
515
+ }
516
+ async createMatterServer(storageManager) {
517
+ this.log.debug('Creating matter server');
518
+ this.matterServer = new MatterServer(storageManager, { mdnsAnnounceInterface: undefined });
519
+ }
520
+ async createMatterAggregator() {
521
+ const matterAggregator = new Aggregator();
522
+ return matterAggregator;
523
+ }
524
+ async stopMatter() {
525
+ this.log.debug('Stopping matter commissioningServer');
526
+ await this.commissioningServer?.close();
527
+ this.log.debug('Stopping matter commissioningController');
528
+ await this.commissioningController?.close();
529
+ this.log.debug('Stopping matter server');
530
+ await this.matterServer?.close();
531
+ this.log.debug('Matter server closed');
532
+ }
533
+ logNodeAndSystemInfo() {
534
+ // IP address information
535
+ const networkInterfaces = os.networkInterfaces();
536
+ this.systemInformation.ipv4Address = 'Not found';
537
+ this.systemInformation.ipv6Address = 'Not found';
538
+ for (const interfaceDetails of Object.values(networkInterfaces)) {
539
+ if (!interfaceDetails) {
540
+ break;
541
+ }
542
+ for (const detail of interfaceDetails) {
543
+ if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === 'Not found') {
544
+ this.systemInformation.ipv4Address = detail.address;
545
+ }
546
+ else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === 'Not found') {
547
+ this.systemInformation.ipv6Address = detail.address;
548
+ }
549
+ }
550
+ // Break if both addresses are found to improve efficiency
551
+ if (this.systemInformation.ipv4Address !== 'Not found' && this.systemInformation.ipv6Address !== 'Not found') {
552
+ break;
553
+ }
554
+ }
555
+ // Node information
556
+ this.systemInformation.nodeVersion = process.versions.node;
557
+ const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
558
+ const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
559
+ const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
560
+ // Host system information
561
+ this.systemInformation.hostname = os.hostname();
562
+ this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
563
+ this.systemInformation.osRelease = os.release(); // Kernel version
564
+ this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
565
+ this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
566
+ this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
567
+ this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
568
+ this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
569
+ // Log the system information
570
+ this.log.debug('Host System Information:');
571
+ this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
572
+ this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
573
+ this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
574
+ this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
575
+ this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
576
+ this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
577
+ this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
578
+ this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
579
+ this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
580
+ this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
581
+ this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
582
+ // Command line arguments (excluding 'node' and the script name)
583
+ const cmdArgs = process.argv.slice(2).join(' ');
584
+ this.log.debug(`Command Line Arguments: ${cmdArgs}`);
585
+ // Current working directory
586
+ const currentDir = process.cwd();
587
+ this.log.debug(`Current Working Directory: ${currentDir}`);
588
+ // Package root directory
589
+ const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
590
+ this.rootDirectory = path.resolve(currentFileDirectory, '../');
591
+ this.log.debug(`Root Directory: ${this.rootDirectory}`);
592
+ }
593
+ /**
594
+ * Initializes the frontend of Matterbridge.
595
+ *
596
+ * @param port The port number to run the frontend server on. Default is 3000.
597
+ */
598
+ async initializeFrontend(port = 3000) {
599
+ //const __filename = fileURLToPath(import.meta.url);
600
+ //const __dirname = path.dirname(__filename);
601
+ this.log.debug(`Initializing the frontend on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${rs}`);
602
+ this.app = express();
603
+ // Serve React build directory
604
+ this.app.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
605
+ // Endpoint to provide QR pairing code
606
+ this.app.get('/api/qr-code', (req, res) => {
607
+ this.log.debug('The frontend sent /api/qr-code');
608
+ const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode') };
609
+ res.json(qrData);
610
+ });
611
+ // Endpoint to provide system information
612
+ this.app.get('/api/system-info', (req, res) => {
613
+ this.log.debug('The frontend sent /api/system-info');
614
+ res.json(this.systemInformation);
615
+ });
616
+ // Endpoint to provide plugins
617
+ this.app.get('/api/plugins', (req, res) => {
618
+ this.log.debug('The frontend sent /api/plugins');
619
+ const data = [];
620
+ this.registeredPlugins.forEach((plugin) => {
621
+ data.push({ name: plugin.name, description: plugin.description, version: plugin.version, author: plugin.author, type: plugin.type });
622
+ });
623
+ res.json(data);
624
+ });
625
+ // Endpoint to provide devices
626
+ this.app.get('/api/devices', (req, res) => {
627
+ this.log.debug('The frontend sent /api/devices');
628
+ const data = [];
629
+ this.registeredDevices.forEach((device) => {
630
+ let name = device.device.getClusterServer(BasicInformationCluster)?.getNodeLabelAttribute();
631
+ if (!name && device.device.getClusterServer(BridgedDeviceBasicInformationCluster)) {
632
+ const bridgeInfo = device.device.getClusterServer(BridgedDeviceBasicInformationCluster);
633
+ if (bridgeInfo && bridgeInfo.getNodeLabelAttribute)
634
+ name = bridgeInfo.getNodeLabelAttribute();
635
+ }
636
+ data.push({ pluginName: device.plugin, type: device.device.name, endpoint: device.device.id, name: name ?? 'Unknown', cluster: 'Unknown' });
637
+ });
638
+ res.json(data);
639
+ });
640
+ // Fallback for routing
641
+ this.app.get('*', (req, res) => {
642
+ this.log.warn('The frontend sent *');
643
+ res.sendFile(path.join(this.rootDirectory, 'frontend/build/index.html'));
644
+ });
645
+ this.app.listen(port, () => {
646
+ this.log.debug(`The frontend is running on ${UNDERLINE}http://localhost:${port}${rs}`);
647
+ });
648
+ }
649
+ }
650
+ /*
651
+ npx create-react-app matterbridge-frontend
652
+ cd matterbridge-frontend
653
+ npm install react-router-dom
654
+
655
+ Success! Created frontend at C:\Users\lligu\OneDrive\GitHub\matterbridge\frontend
656
+ Inside that directory, you can run several commands:
657
+
658
+ npm start
659
+ Starts the development server.
660
+
661
+ npm run build
662
+ Bundles the app into static files for production.
663
+
664
+ npm test
665
+ Starts the test runner.
666
+
667
+ npm run eject
668
+ Removes this tool and copies build dependencies, configuration files
669
+ and scripts into the app directory. If you do this, you can’t go back!
670
+
671
+ We suggest that you begin by typing:
672
+
673
+ cd frontend
674
+ npm start
675
+
676
+ Happy hacking!
677
+ PS C:\Users\lligu\OneDrive\GitHub\matterbridge> cd frontend
678
+ PS C:\Users\lligu\OneDrive\GitHub\matterbridge\frontend> npm run build
679
+
680
+ > frontend@0.1.0 build
681
+ > react-scripts build
682
+
683
+ Creating an optimized production build...
684
+ One of your dependencies, babel-preset-react-app, is importing the
685
+ "@babel/plugin-proposal-private-property-in-object" package without
686
+ declaring it in its dependencies. This is currently working because
687
+ "@babel/plugin-proposal-private-property-in-object" is already in your
688
+ node_modules folder for unrelated reasons, but it may break at any time.
689
+
690
+ babel-preset-react-app is part of the create-react-app project, which
691
+ is not maintianed anymore. It is thus unlikely that this bug will
692
+ ever be fixed. Add "@babel/plugin-proposal-private-property-in-object" to
693
+ your devDependencies to work around this error. This will make this message
694
+ go away.
695
+
696
+ Compiled successfully.
697
+
698
+ File sizes after gzip:
699
+
700
+ 46.65 kB build\static\js\main.9b7ec296.js
701
+ 1.77 kB build\static\js\453.8ab44547.chunk.js
702
+ 513 B build\static\css\main.f855e6bc.css
703
+
704
+ The project was built assuming it is hosted at /.
705
+ You can control this with the homepage field in your package.json.
706
+
707
+ The build folder is ready to be deployed.
708
+ You may serve it with a static server:
709
+
710
+ npm install -g serve
711
+ serve -s build
712
+
713
+ Find out more about deployment here:
714
+
715
+ https://cra.link/deployment
716
+
717
+ PS C:\Users\lligu\OneDrive\GitHub\matterbridge\frontend>
718
+ */
719
+ //# sourceMappingURL=matterbridge.js.map