matterbridge 1.2.7 → 1.2.9

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.
@@ -26,10 +26,12 @@ import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugSt
26
26
  import { fileURLToPath, pathToFileURL } from 'url';
27
27
  import { promises as fs } from 'fs';
28
28
  import { exec, spawn } from 'child_process';
29
+ import https from 'https';
29
30
  import EventEmitter from 'events';
30
31
  import express from 'express';
31
32
  import os from 'os';
32
33
  import path from 'path';
34
+ import WebSocket, { WebSocketServer } from 'ws';
33
35
  import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
34
36
  import { BasicInformationCluster, BooleanStateCluster,
35
37
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -90,6 +92,7 @@ export class Matterbridge extends EventEmitter {
90
92
  nodeContext;
91
93
  expressApp;
92
94
  expressServer;
95
+ webSocketServer;
93
96
  storageManager;
94
97
  matterbridgeContext;
95
98
  mattercontrollerContext;
@@ -112,13 +115,96 @@ export class Matterbridge extends EventEmitter {
112
115
  static async loadInstance(initialize = false) {
113
116
  if (!Matterbridge.instance) {
114
117
  // eslint-disable-next-line no-console
115
- console.log(wr + 'Matterbridge instance does not exists!', initialize ? 'Initializing...' : 'Not initializing...', rs);
118
+ if (hasParameter('debug'))
119
+ console.log(wr + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
116
120
  Matterbridge.instance = new Matterbridge();
117
121
  if (initialize)
118
122
  await Matterbridge.instance.initialize();
119
123
  }
120
124
  return Matterbridge.instance;
121
125
  }
126
+ /**
127
+ * Initializes the Matterbridge instance as extension for zigbee2mqtt.
128
+ *
129
+ * @returns A Promise that resolves when the initialization is complete.
130
+ */
131
+ async initializeAsExtension(dataPath, debugEnabled) {
132
+ // Set the bridge mode
133
+ this.bridgeMode = 'bridge';
134
+ // Set the first port to use
135
+ this.port = 5560;
136
+ // Set Matterbridge logger
137
+ this.debugEnabled = debugEnabled;
138
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
139
+ this.log.debug('Matterbridge extension is starting...');
140
+ // Initialize NodeStorage
141
+ this.matterbridgeDirectory = dataPath;
142
+ this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
143
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
144
+ this.log.debug('Creating node storage context for matterbridge: matterbridge');
145
+ this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
146
+ const plugin = {
147
+ path: '',
148
+ type: 'DynamicPlatform',
149
+ name: 'MatterbridgeExtension',
150
+ version: '1.0.0',
151
+ description: 'Matterbridge extension',
152
+ author: 'https://github.com/Luligu',
153
+ enabled: false,
154
+ registeredDevices: 0,
155
+ addedDevices: 0,
156
+ };
157
+ this.registeredPlugins.push(plugin);
158
+ await this.nodeContext?.set('plugins', this.getBaseRegisteredPlugins());
159
+ // Log system info and create .matterbridge directory
160
+ await this.logNodeAndSystemInfo();
161
+ this.matterbridgeDirectory = dataPath;
162
+ // Set matter.js logger level and format
163
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
164
+ Logger.format = Format.ANSI;
165
+ // Start the storage and create matterbridgeContext
166
+ await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
167
+ this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
168
+ if (!this.storageManager || !this.matterbridgeContext)
169
+ return;
170
+ await this.matterbridgeContext.set('softwareVersion', 1);
171
+ await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
172
+ await this.matterbridgeContext.set('hardwareVersion', 0);
173
+ await this.matterbridgeContext.set('hardwareVersionString', '1.0.0'); // Update with the extension version
174
+ this.createMatterServer(this.storageManager);
175
+ this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
176
+ this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
177
+ this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
178
+ this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext);
179
+ this.log.debug('Adding matterbridge aggregator to commissioning server');
180
+ this.commissioningServer.addDevice(this.matterAggregator);
181
+ this.log.debug('Adding matterbridge commissioning server to matter server');
182
+ await this.matterServer?.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
183
+ this.log.debug(`Setting reachability to true for ${plg}Matterbridge${db}`);
184
+ this.commissioningServer.setReachability(true);
185
+ await this.startMatterServer();
186
+ this.log.info('Matter server started');
187
+ await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
188
+ // Set reachability to true and trigger event after 60 seconds
189
+ setTimeout(() => {
190
+ this.log.info(`*Setting reachability to true for ${plg}Matterbridge${db}`);
191
+ if (this.commissioningServer)
192
+ this.setCommissioningServerReachability(this.commissioningServer, true);
193
+ if (this.matterAggregator)
194
+ this.setAggregatorReachability(this.matterAggregator, true);
195
+ }, 60 * 1000);
196
+ }
197
+ /**
198
+ * Close the Matterbridge instance as extension for zigbee2mqtt.
199
+ *
200
+ * @returns A Promise that resolves when the initialization is complete.
201
+ */
202
+ async closeAsExtension() {
203
+ // Closing matter
204
+ await this.stopMatter();
205
+ // Closing storage
206
+ await this.stopStorage();
207
+ }
122
208
  /**
123
209
  * Initializes the Matterbridge application.
124
210
  *
@@ -202,66 +288,6 @@ export class Matterbridge extends EventEmitter {
202
288
  // Parse command line
203
289
  this.parseCommandLine();
204
290
  }
205
- /**
206
- * Spawns a child process with the given command and arguments.
207
- * @param command - The command to execute.
208
- * @param args - The arguments to pass to the command (default: []).
209
- * @returns A promise that resolves when the child process exits successfully, or rejects if there is an error.
210
- */
211
- async spawnCommand(command, args = []) {
212
- /*
213
- npm > npm.cmd on windows
214
- */
215
- if (process.platform === 'win32' && command === 'npm') {
216
- command = command + '.cmd';
217
- }
218
- if (process.platform === 'linux' && command === 'npm' && !hasParameter('docker')) {
219
- args.unshift(command);
220
- command = 'sudo';
221
- }
222
- return new Promise((resolve, reject) => {
223
- const childProcess = spawn(command, args, {
224
- stdio: ['inherit', 'pipe', 'pipe'],
225
- });
226
- childProcess.on('error', (err) => {
227
- this.log.error(`Failed to start child process: ${err.message}`);
228
- reject(err); // Reject the promise on error
229
- });
230
- childProcess.on('close', (code) => {
231
- if (code === 0) {
232
- this.log.info(`Child process stdio streams have closed with code ${code}`);
233
- resolve();
234
- }
235
- else {
236
- this.log.error(`Child process stdio streams have closed with code ${code}`);
237
- reject(new Error(`Process exited with code ${code}`));
238
- }
239
- });
240
- // The 'exit' event might be redundant here since 'close' is also being handled
241
- childProcess.on('exit', (code, signal) => {
242
- this.log.info(`Child process exited with code ${code} and signal ${signal}`);
243
- });
244
- childProcess.on('disconnect', () => {
245
- this.log.info('Child process has been disconnected from the parent');
246
- });
247
- if (childProcess.stdout) {
248
- childProcess.stdout.on('data', (data) => {
249
- // Convert the Buffer data to a string.
250
- const message = data.toString();
251
- this.log.info(message);
252
- // TODO: Send this message to the frontend.
253
- });
254
- }
255
- if (childProcess.stderr) {
256
- childProcess.stderr.on('data', (data) => {
257
- // Convert the Buffer data to a string.
258
- const message = data.toString();
259
- this.log.error(message);
260
- // TODO: Handle the error message.
261
- });
262
- }
263
- });
264
- }
265
291
  /**
266
292
  * Parses the command line arguments and performs the corresponding actions.
267
293
  * @private
@@ -579,6 +605,7 @@ export class Matterbridge extends EventEmitter {
579
605
  this.log.info(message);
580
606
  process.removeAllListeners('SIGINT');
581
607
  process.removeAllListeners('SIGTERM');
608
+ this.log.debug('All listeners removed');
582
609
  // Calling the shutdown functions with a reason
583
610
  for (const plugin of this.registeredPlugins) {
584
611
  if (!plugin.enabled)
@@ -634,11 +661,31 @@ export class Matterbridge extends EventEmitter {
634
661
  if (this.expressServer) {
635
662
  this.expressServer.close();
636
663
  this.expressServer = undefined;
664
+ this.log.debug('Express server closed successfully');
637
665
  }
638
666
  // Remove listeners
639
667
  if (this.expressApp) {
640
668
  this.expressApp.removeAllListeners();
641
669
  this.expressApp = undefined;
670
+ this.log.debug('Frontend closed successfully');
671
+ }
672
+ // Close the WebSocket server
673
+ if (this.webSocketServer) {
674
+ // Close all active connections
675
+ this.webSocketServer.clients.forEach((client) => {
676
+ if (client.readyState === WebSocket.OPEN) {
677
+ client.close();
678
+ }
679
+ });
680
+ this.webSocketServer.close((error) => {
681
+ if (error) {
682
+ this.log.error(`Error closing WebSocket server: ${error}`);
683
+ }
684
+ else {
685
+ this.log.debug('WebSocket server closed successfully');
686
+ }
687
+ });
688
+ this.webSocketServer = undefined;
642
689
  }
643
690
  /*const cleanupTimeout1 =*/ setTimeout(async () => {
644
691
  // Closing matter
@@ -688,26 +735,12 @@ export class Matterbridge extends EventEmitter {
688
735
  Matterbridge.instance = undefined;
689
736
  this.emit('shutdown');
690
737
  }
691
- }, 1 * 1000);
738
+ }, 2 * 1000);
692
739
  //cleanupTimeout2.unref();
693
740
  }, 3 * 1000);
694
741
  //cleanupTimeout1.unref();
695
742
  }
696
743
  }
697
- /**
698
- * Sets the reachable attribute of a device.
699
- *
700
- * @param device - The device for which to set the reachable attribute.
701
- * @param reachable - The value to set for the reachable attribute.
702
- */
703
- setReachableAttribute(device, reachable) {
704
- const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
705
- if (!basicInformationCluster) {
706
- this.log.error('setReachableAttribute BasicInformationCluster needs to be set!');
707
- return;
708
- }
709
- basicInformationCluster.setReachableAttribute(reachable);
710
- }
711
744
  /**
712
745
  * Adds a device to the Matterbridge.
713
746
  * @param pluginName - The name of the plugin.
@@ -803,7 +836,7 @@ export class Matterbridge extends EventEmitter {
803
836
  return;
804
837
  }
805
838
  if (this.bridgeMode === 'childbridge' && !plugin.connected) {
806
- this.log.warn(`Removing bridged device ${dev}${device.deviceName}${wr} (${dev}${device.name}${wr}) plugin ${plg}${pluginName}${wr} not connected`);
839
+ this.log.info(`Removing bridged device ${dev}${device.deviceName}${wr} (${dev}${device.name}${wr}) plugin ${plg}${pluginName}${wr} not connected`);
807
840
  return;
808
841
  }
809
842
  // Remove the device from matterbridge aggregator in bridge mode
@@ -1220,7 +1253,9 @@ export class Matterbridge extends EventEmitter {
1220
1253
  // Call the default export function of the plugin, passing this MatterBridge instance
1221
1254
  if (pluginInstance.default) {
1222
1255
  const config = await this.loadPluginConfig(plugin);
1223
- const platform = pluginInstance.default(this, new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled }), config);
1256
+ const log = new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
1257
+ //log.setCallback(this.wssSendMessage.bind(this));
1258
+ const platform = pluginInstance.default(this, log, config);
1224
1259
  platform.name = packageJson.name;
1225
1260
  platform.config = config;
1226
1261
  plugin.name = packageJson.name;
@@ -1482,20 +1517,17 @@ export class Matterbridge extends EventEmitter {
1482
1517
  await this.matterServer?.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
1483
1518
  this.log.debug(`Setting reachability to true for ${plg}Matterbridge${db}`);
1484
1519
  this.commissioningServer.setReachability(true);
1485
- this.log.debug('Starting matter server...');
1486
1520
  await this.startMatterServer();
1487
1521
  this.log.info('Matter server started');
1488
1522
  await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1489
1523
  //if (hasParameter('advertise')) await this.commissioningServer.advertise();
1490
- /*
1491
- setInterval(() => {
1492
- this.matterAggregator?.getBridgedDevices().forEach((device) => {
1493
- this.log.info(`Bridged device: ${dev}${device.name}${nf}`);
1494
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(true);
1495
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: true });
1496
- });
1497
- }, 30 * 1000);
1498
- */
1524
+ setTimeout(() => {
1525
+ this.log.info(`*Setting reachability to true for ${plg}Matterbridge${db}`);
1526
+ if (this.commissioningServer)
1527
+ this.setCommissioningServerReachability(this.commissioningServer, true);
1528
+ if (this.matterAggregator)
1529
+ this.setAggregatorReachability(this.matterAggregator, true);
1530
+ }, 60 * 1000);
1499
1531
  }, 1000);
1500
1532
  }
1501
1533
  if (this.bridgeMode === 'childbridge') {
@@ -1532,6 +1564,8 @@ export class Matterbridge extends EventEmitter {
1532
1564
  plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1533
1565
  this.log.debug(`Adding device ${dev}${registeredDevice.device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
1534
1566
  plugin.commissioningServer.addDevice(registeredDevice.device);
1567
+ if (!plugin.device)
1568
+ plugin.device = registeredDevice.device;
1535
1569
  }
1536
1570
  this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db}`);
1537
1571
  await this.matterServer?.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
@@ -1572,21 +1606,6 @@ export class Matterbridge extends EventEmitter {
1572
1606
  return;
1573
1607
  clearInterval(startMatterInterval);
1574
1608
  this.log.info('Starting matter server...');
1575
- // Setting reachability to true
1576
- this.registeredPlugins.forEach((plugin) => {
1577
- if (!plugin.enabled)
1578
- return;
1579
- this.log.debug(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1580
- plugin.commissioningServer?.setReachability(true);
1581
- this.registeredDevices.forEach((registeredDevice) => {
1582
- if (registeredDevice.plugin === plugin.name) {
1583
- if (plugin.type === 'AccessoryPlatform')
1584
- this.setReachableAttribute(registeredDevice.device, true);
1585
- if (plugin.type === 'DynamicPlatform')
1586
- registeredDevice.device.setBridgedDeviceReachability(true);
1587
- }
1588
- });
1589
- });
1590
1609
  await this.startMatterServer();
1591
1610
  this.log.info('Matter server started');
1592
1611
  for (const plugin of this.registeredPlugins) {
@@ -1605,6 +1624,14 @@ export class Matterbridge extends EventEmitter {
1605
1624
  continue;
1606
1625
  }
1607
1626
  await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
1627
+ // Setting reachability to true
1628
+ this.log.info(`*Setting reachability to true for ${plg}${plugin.name}${db}`);
1629
+ if (plugin.commissioningServer)
1630
+ this.setCommissioningServerReachability(plugin.commissioningServer, true);
1631
+ if (plugin.type === 'AccessoryPlatform' && plugin.device)
1632
+ this.setDeviceReachability(plugin.device, true);
1633
+ if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
1634
+ this.setAggregatorReachability(plugin.aggregator, true);
1608
1635
  }
1609
1636
  Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
1610
1637
  //clearInterval(startMatterInterval);
@@ -1621,7 +1648,7 @@ export class Matterbridge extends EventEmitter {
1621
1648
  await this.cleanup('No matter server initialized');
1622
1649
  return;
1623
1650
  }
1624
- this.log.debug('Starting matter server');
1651
+ this.log.debug('Starting matter server...');
1625
1652
  await this.matterServer.start();
1626
1653
  this.log.debug('Started matter server');
1627
1654
  }
@@ -1741,6 +1768,49 @@ export class Matterbridge extends EventEmitter {
1741
1768
  }
1742
1769
  return plugin;
1743
1770
  }
1771
+ /**
1772
+ * Sets the reachability of a commissioning server and trigger.
1773
+ *
1774
+ * @param {CommissioningServer} commissioningServer - The commissioning server to set the reachability for.
1775
+ * @param {boolean} reachable - The new reachability status.
1776
+ */
1777
+ setCommissioningServerReachability(commissioningServer, reachable) {
1778
+ const basicInformationCluster = commissioningServer?.getRootClusterServer(BasicInformationCluster);
1779
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
1780
+ basicInformationCluster.setReachableAttribute(reachable);
1781
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
1782
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
1783
+ }
1784
+ /**
1785
+ * Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
1786
+ * @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
1787
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
1788
+ */
1789
+ setAggregatorReachability(matterAggregator, reachable) {
1790
+ const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
1791
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
1792
+ basicInformationCluster.setReachableAttribute(reachable);
1793
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
1794
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
1795
+ matterAggregator.getBridgedDevices().forEach((device) => {
1796
+ this.log.debug(`*Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
1797
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
1798
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
1799
+ });
1800
+ }
1801
+ /**
1802
+ * Sets the reachability of a device and trigger.
1803
+ *
1804
+ * @param {MatterbridgeDevice} device - The device to set the reachability for.
1805
+ * @param {boolean} reachable - The new reachability status of the device.
1806
+ */
1807
+ setDeviceReachability(device, reachable) {
1808
+ const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
1809
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
1810
+ basicInformationCluster.setReachableAttribute(reachable);
1811
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
1812
+ basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
1813
+ }
1744
1814
  /**
1745
1815
  * Creates a matter commissioning server.
1746
1816
  *
@@ -1793,7 +1863,14 @@ export class Matterbridge extends EventEmitter {
1793
1863
  info.forEach((session) => {
1794
1864
  this.log.debug(`***Active session changed on fabric ${fabricIndex} ${session.fabric?.rootVendorId}/${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
1795
1865
  if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
1796
- this.log.info(`***Controller ${session.fabric?.rootVendorId}/${session.fabric?.label} connected to ${plg}${pluginName}${nf}`);
1866
+ let controllerName = '';
1867
+ if (session.fabric?.rootVendorId === 4937)
1868
+ controllerName = 'AppleHome';
1869
+ if (session.fabric?.rootVendorId === 4362)
1870
+ controllerName = 'SmartThings';
1871
+ if (session.fabric?.rootVendorId === 4939)
1872
+ controllerName = 'HomeAssistant';
1873
+ this.log.info(`***Controller ${session.fabric?.rootVendorId}${controllerName !== '' ? '(' + controllerName + ')' : ''}/${session.fabric?.label} connected to ${plg}${pluginName}${nf}`);
1797
1874
  connected = true;
1798
1875
  }
1799
1876
  });
@@ -2028,11 +2105,6 @@ export class Matterbridge extends EventEmitter {
2028
2105
  this.matterbridgeInformation.rootDirectory = this.rootDirectory;
2029
2106
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
2030
2107
  // Global node_modules directory
2031
- /*
2032
- this.globalModulesDirectory = await this.getGlobalNodeModules();
2033
- this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
2034
- this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
2035
- */
2036
2108
  if (this.nodeContext)
2037
2109
  this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
2038
2110
  this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
@@ -2100,11 +2172,6 @@ export class Matterbridge extends EventEmitter {
2100
2172
  this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
2101
2173
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
2102
2174
  // Matterbridge latest version
2103
- /*
2104
- this.matterbridgeLatestVersion = await this.getLatestVersion('matterbridge');
2105
- this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeLatestVersion;
2106
- this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
2107
- */
2108
2175
  if (this.nodeContext)
2109
2176
  this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
2110
2177
  this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
@@ -2174,6 +2241,99 @@ export class Matterbridge extends EventEmitter {
2174
2241
  }));
2175
2242
  return baseRegisteredPlugins;
2176
2243
  }
2244
+ /**
2245
+ * Spawns a child process with the given command and arguments.
2246
+ * @param command - The command to execute.
2247
+ * @param args - The arguments to pass to the command (default: []).
2248
+ * @returns A promise that resolves when the child process exits successfully, or rejects if there is an error.
2249
+ */
2250
+ async spawnCommand(command, args = []) {
2251
+ /*
2252
+ npm > npm.cmd on windows
2253
+ cmd.exe ['dir'] on windows
2254
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2255
+ process.on('unhandledRejection', (reason, promise) => {
2256
+ this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
2257
+ });
2258
+ */
2259
+ if (process.platform === 'win32' && command === 'npm') {
2260
+ // Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
2261
+ const argstring = 'npm ' + args.join(' ');
2262
+ args.splice(0, args.length, '/c', argstring);
2263
+ command = 'cmd.exe';
2264
+ }
2265
+ if (process.platform === 'linux' && command === 'npm' && !hasParameter('docker')) {
2266
+ args.unshift(command);
2267
+ command = 'sudo';
2268
+ }
2269
+ this.log.debug(`Spawning command ${command} with ${debugStringify(args)}`);
2270
+ return new Promise((resolve, reject) => {
2271
+ const childProcess = spawn(command, args, {
2272
+ stdio: ['inherit', 'pipe', 'pipe'],
2273
+ });
2274
+ childProcess.on('error', (err) => {
2275
+ this.log.error(`Failed to start child process: ${err.message}`);
2276
+ reject(err); // Reject the promise on error
2277
+ });
2278
+ childProcess.on('close', (code) => {
2279
+ if (code === 0) {
2280
+ this.log.debug(`Child process stdio streams have closed with code ${code}`);
2281
+ resolve();
2282
+ }
2283
+ else {
2284
+ this.log.error(`Child process stdio streams have closed with code ${code}`);
2285
+ reject(new Error(`Child process stdio streams have closed with code ${code}`));
2286
+ }
2287
+ });
2288
+ childProcess.on('exit', (code, signal) => {
2289
+ if (code === 0) {
2290
+ this.log.debug(`Child process exited with code ${code} and signal ${signal}`);
2291
+ resolve();
2292
+ }
2293
+ else {
2294
+ this.log.error(`Child process exited with code ${code} and signal ${signal}`);
2295
+ reject(new Error(`Child process exited with code ${code} and signal ${signal}`));
2296
+ }
2297
+ });
2298
+ childProcess.on('disconnect', () => {
2299
+ this.log.debug('Child process has been disconnected from the parent');
2300
+ resolve();
2301
+ });
2302
+ if (childProcess.stdout) {
2303
+ childProcess.stdout.on('data', (data) => {
2304
+ const message = data.toString().trim();
2305
+ //this.log.info('\n' + message);
2306
+ this.wssSendMessage('Matterbridge:spawn', 'spawn', message);
2307
+ });
2308
+ }
2309
+ if (childProcess.stderr) {
2310
+ childProcess.stderr.on('data', (data) => {
2311
+ const message = data.toString().trim();
2312
+ //this.log.debug('\n' + message);
2313
+ this.wssSendMessage('Matterbridge:spawn', 'spawn', message);
2314
+ });
2315
+ }
2316
+ });
2317
+ }
2318
+ /**
2319
+ * Sends a WebSocket message to all connected clients.
2320
+ *
2321
+ * @param {string} type - The type of the message: Matterbridge, Plugin, Device, ...
2322
+ * @param {string} subType - The subtype of the message: debug info warn error ....
2323
+ * @param {string} message - The content of the message.
2324
+ */
2325
+ wssSendMessage(type, subType, message) {
2326
+ // Remove ANSI escape codes from the message
2327
+ // eslint-disable-next-line no-control-regex
2328
+ const cleanMessage = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2329
+ // Remove leading asterisks from the message
2330
+ const finalMessage = cleanMessage.replace(/^\*+/, '');
2331
+ this.webSocketServer?.clients.forEach((client) => {
2332
+ if (client.readyState === WebSocket.OPEN) {
2333
+ client.send(JSON.stringify({ type, subType, message: finalMessage }));
2334
+ }
2335
+ });
2336
+ }
2177
2337
  /**
2178
2338
  * Initializes the frontend of Matterbridge.
2179
2339
  *
@@ -2181,10 +2341,57 @@ export class Matterbridge extends EventEmitter {
2181
2341
  */
2182
2342
  async initializeFrontend(port = 8283) {
2183
2343
  this.log.debug(`Initializing the frontend on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
2184
- this.expressApp = express();
2344
+ const wssPort = 8284;
2345
+ const useHttps = false;
2346
+ //const wssHost = (useHttps ? 'wss://' : 'ws://') + `${os.hostname().toLowerCase()}:${wssPort}`;
2347
+ const wssHost = (useHttps ? 'wss://' : 'ws://') + `${this.systemInformation.ipv4Address}:${wssPort}`;
2348
+ if (!useHttps) {
2349
+ // Create a WebSocket server no certificate required
2350
+ this.webSocketServer = new WebSocketServer({ port: wssPort });
2351
+ this.log.info(`WebSocket server listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
2352
+ }
2353
+ else {
2354
+ // Define the options for HTTPS server
2355
+ // openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mykey.key -out mycert.pem -config openssl.cnf
2356
+ // For wss connect the browser to https://laptop5_luca:8284/ https://192.168.1.189/log and accept the certificate
2357
+ const serverOptions = {
2358
+ cert: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/mycert.pem')), // Ensure the path is correct
2359
+ key: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/mykey.key')), // Ensure the path is correct
2360
+ // cert: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/laptop5_luca.pem')), // Ensure the path is correct
2361
+ // key: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/laptop5_luca.key')), // Ensure the path is correct
2362
+ };
2363
+ // Create an HTTPS server
2364
+ const httpsServer = https.createServer(serverOptions);
2365
+ // Attach WebSocket server to HTTPS server
2366
+ this.webSocketServer = new WebSocketServer({ server: httpsServer });
2367
+ // Listen on a specific port
2368
+ httpsServer.listen(wssPort, () => {
2369
+ this.log.info(`WebSocket server listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
2370
+ });
2371
+ }
2372
+ this.webSocketServer.on('connection', (ws) => {
2373
+ this.log.info('WebSocketServer client connected');
2374
+ this.log.setGlobalCallback(this.wssSendMessage.bind(this));
2375
+ this.log.debug('WebSocketServer activated logger callback');
2376
+ this.wssSendMessage('Matterbridge', 'info', 'WebSocketServer client connected to Matterbridge');
2377
+ ws.on('message', (message) => {
2378
+ this.log.info(`WebSocketServer received message => ${message}`);
2379
+ });
2380
+ ws.on('close', () => {
2381
+ this.log.info('WebSocketServer client disconnected');
2382
+ if (this.webSocketServer?.clients.size === 0) {
2383
+ this.log.setGlobalCallback(undefined);
2384
+ this.log.debug('WebSocketServer deactivated logger callback');
2385
+ }
2386
+ });
2387
+ ws.on('error', (error) => {
2388
+ this.log.error(`WebSocketServer error: ${error}`);
2389
+ });
2390
+ });
2185
2391
  // Serve React build directory
2392
+ this.expressApp = express();
2186
2393
  this.expressApp.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
2187
- // Endpoint to provide login code
2394
+ // Endpoint to validate login code
2188
2395
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
2189
2396
  const { password } = req.body;
2190
2397
  this.log.debug('The frontend sent /api/login', password);
@@ -2204,53 +2411,30 @@ export class Matterbridge extends EventEmitter {
2204
2411
  res.json({ valid: false });
2205
2412
  }
2206
2413
  });
2207
- // Endpoint to provide manual pairing code
2208
- this.expressApp.get('/api/pairing-code', (req, res) => {
2209
- this.log.debug('The frontend sent /api/pairing-code');
2414
+ // Endpoint to provide settings
2415
+ this.expressApp.get('/api/settings', express.json(), async (req, res) => {
2210
2416
  if (!this.matterbridgeContext) {
2211
- this.log.error('/api/pairing-code matterbridgeContext not found');
2212
- res.json([]);
2213
- return;
2214
- }
2215
- try {
2216
- const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode'), manualPairingCode: this.matterbridgeContext.get('manualPairingCode') };
2217
- res.json(qrData);
2218
- }
2219
- catch (error) {
2220
- if (this.bridgeMode === 'bridge')
2221
- this.log.error('qrPairingCode for /api/qr-code not found');
2417
+ this.log.error('/api/settings matterbridgeContext not found');
2222
2418
  res.json({});
2223
- }
2224
- });
2225
- // Endpoint to provide QR pairing code
2226
- this.expressApp.get('/api/qr-code', (req, res) => {
2227
- this.log.debug('The frontend sent /api/qr-code');
2228
- if (!this.matterbridgeContext) {
2229
- this.log.error('/api/qr-code matterbridgeContext not found');
2230
- res.json([]);
2231
2419
  return;
2232
2420
  }
2421
+ let qrPairingCode = '';
2422
+ let manualPairingCode = '';
2233
2423
  try {
2234
- const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode'), manualPairingCode: this.matterbridgeContext.get('manualPairingCode') };
2235
- res.json(qrData);
2424
+ qrPairingCode = await this.matterbridgeContext.get('qrPairingCode');
2425
+ manualPairingCode = await this.matterbridgeContext.get('manualPairingCode');
2236
2426
  }
2237
2427
  catch (error) {
2238
2428
  if (this.bridgeMode === 'bridge')
2239
- this.log.error('qrPairingCode for /api/qr-code not found');
2429
+ this.log.error('pairingCode for /api/settings not found');
2240
2430
  res.json({});
2241
2431
  }
2242
- });
2243
- // Endpoint to provide system information
2244
- this.expressApp.get('/api/system-info', (req, res) => {
2245
- this.log.debug('The frontend sent /api/system-info');
2246
- res.json(this.systemInformation);
2247
- });
2248
- // Endpoint to provide matterbridge information
2249
- this.expressApp.get('/api/matterbridge-info', (req, res) => {
2250
- this.log.debug('The frontend sent /api/matterbridge-info');
2251
2432
  this.matterbridgeInformation.bridgeMode = this.bridgeMode;
2252
2433
  this.matterbridgeInformation.debugEnabled = this.debugEnabled;
2253
- res.json(this.matterbridgeInformation);
2434
+ const response = { wssHost, qrPairingCode, manualPairingCode, systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
2435
+ this.log.debug('The frontend sent /api/settings');
2436
+ this.log.debug('Response:', debugStringify(response));
2437
+ res.json(response);
2254
2438
  });
2255
2439
  // Endpoint to provide plugins
2256
2440
  this.expressApp.get('/api/plugins', (req, res) => {
@@ -2336,11 +2520,11 @@ export class Matterbridge extends EventEmitter {
2336
2520
  res.status(400).json({ error: 'No command provided' });
2337
2521
  return;
2338
2522
  }
2339
- this.log.info(`Received frontend command: ${command}:${param}`);
2523
+ this.log.debug(`Received frontend command: ${command}:${param}`);
2340
2524
  // Handle the command setpassword from Settings
2341
2525
  if (command === 'setpassword') {
2342
2526
  const password = param.slice(1, -1); // Remove the first and last characters
2343
- this.log.info('setpassword', param, password);
2527
+ this.log.debug('setpassword', param, password);
2344
2528
  await this.nodeContext?.set('password', password);
2345
2529
  }
2346
2530
  // Handle the command debugLevel from Settings
@@ -2374,9 +2558,9 @@ export class Matterbridge extends EventEmitter {
2374
2558
  }
2375
2559
  // Handle the command update from Header
2376
2560
  if (command === 'update') {
2377
- this.log.warn(`***Updating matterbridge ${plg}${param}${db}`);
2561
+ this.log.info('Updating matterbridge...');
2378
2562
  try {
2379
- await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2563
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge', '--loglevel=verbose']);
2380
2564
  this.log.info('Matterbridge has been updated. Full restart required.');
2381
2565
  }
2382
2566
  catch (error) {
@@ -2389,10 +2573,10 @@ export class Matterbridge extends EventEmitter {
2389
2573
  // Handle the command installplugin from Home
2390
2574
  if (command === 'installplugin') {
2391
2575
  param = param.replace(/\*/g, '\\');
2392
- this.log.warn(`***Installing plugin ${plg}${param}${db}`);
2576
+ this.log.info(`Installing plugin ${plg}${param}${db}...`);
2393
2577
  try {
2394
- await this.spawnCommand('npm', ['install', '-g', param]);
2395
- this.log.info(`Plugin ${plg}${param}${nf} installed. Restart required.`);
2578
+ await this.spawnCommand('npm', ['install', '-g', param, '--loglevel=verbose']);
2579
+ this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
2396
2580
  }
2397
2581
  catch (error) {
2398
2582
  this.log.error(`Error installing plugin ${plg}${param}${er}`);
@@ -2518,9 +2702,26 @@ export class Matterbridge extends EventEmitter {
2518
2702
  this.log.debug('The frontend sent *', req.url);
2519
2703
  res.sendFile(path.join(this.rootDirectory, 'frontend/build/index.html'));
2520
2704
  });
2521
- this.expressServer = this.expressApp.listen(port, () => {
2522
- this.log.info(`The frontend is running on ${UNDERLINE}http://localhost:${port}${UNDERLINEOFF}${rs}`);
2523
- });
2705
+ if (!useHttps) {
2706
+ // Listen on HTTP
2707
+ this.expressServer = this.expressApp.listen(port, () => {
2708
+ this.log.info(`The frontend is running on ${UNDERLINE}http://localhost:${port}${UNDERLINEOFF}${rs}`);
2709
+ });
2710
+ }
2711
+ else {
2712
+ // SSL certificate and private key paths
2713
+ const options = {
2714
+ cert: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/laptop5_luca.pem')), // Ensure the path is correct
2715
+ key: await fs.readFile(path.join(this.rootDirectory, 'frontend/certificates/laptop5_luca.key')), // Ensure the path is correct
2716
+ };
2717
+ // Create HTTPS server
2718
+ const httpsServer = https.createServer(options, this.expressApp);
2719
+ // Specify the port to listen on, for example 443 for default HTTPS
2720
+ const PORT = 443;
2721
+ httpsServer.listen(PORT, () => {
2722
+ this.log.info(`The frontend is running on ${UNDERLINE}https://localhost:${PORT}${UNDERLINEOFF}${rs}`);
2723
+ });
2724
+ }
2524
2725
  this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
2525
2726
  }
2526
2727
  /**
@@ -2588,7 +2789,6 @@ function restartProcess() {
2588
2789
  }
2589
2790
 
2590
2791
  import * as WebSocket from 'ws';
2591
- const globalModulesDir = require('global-modules');
2592
2792
 
2593
2793
  const wss = new WebSocket.Server({ port: 8080 });
2594
2794
 
@@ -2607,13 +2807,6 @@ ws.onmessage = (event) => {
2607
2807
  console.log(`Received message => ${event.data}`);
2608
2808
  };
2609
2809
 
2610
- */
2611
- /*
2612
- // In Matterbridge
2613
- global.matterbridgeInstance = Matterbridge.loadInstance();
2614
-
2615
- // In plugins
2616
- const matterbridge = global.matterbridgeInstance;
2617
2810
  */
2618
2811
  /*
2619
2812
  npx create-react-app matterbridge-frontend