matterbridge 1.2.6 → 1.2.8

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 (38) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +2 -2
  3. package/TODO.md +0 -4
  4. package/dist/AirQualityCluster.d.ts +1 -1
  5. package/dist/AirQualityCluster.d.ts.map +1 -1
  6. package/dist/TvocCluster.d.ts +1 -1
  7. package/dist/TvocCluster.d.ts.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/matterbridge.d.ts +21 -7
  13. package/dist/matterbridge.d.ts.map +1 -1
  14. package/dist/matterbridge.js +259 -107
  15. package/dist/matterbridge.js.map +1 -1
  16. package/dist/matterbridgeDevice.d.ts +50 -17
  17. package/dist/matterbridgeDevice.d.ts.map +1 -1
  18. package/dist/matterbridgeDevice.js +100 -18
  19. package/dist/matterbridgeDevice.js.map +1 -1
  20. package/dist/utils.d.ts +52 -1
  21. package/dist/utils.d.ts.map +1 -1
  22. package/dist/utils.js +121 -1
  23. package/dist/utils.js.map +1 -1
  24. package/frontend/build/asset-manifest.json +6 -6
  25. package/frontend/build/index.html +1 -1
  26. package/frontend/build/static/css/{main.61f6cf42.css → main.4c325919.css} +2 -2
  27. package/frontend/build/static/css/main.4c325919.css.map +1 -0
  28. package/frontend/build/static/js/{main.6b861489.js → main.90cbbf6e.js} +3 -3
  29. package/frontend/build/static/js/main.90cbbf6e.js.map +1 -0
  30. package/matterbridge.service +1 -1
  31. package/package.json +13 -11
  32. package/dist/EveHistoryCluster.d.ts +0 -426
  33. package/dist/EveHistoryCluster.d.ts.map +0 -1
  34. package/dist/EveHistoryCluster.js +0 -162
  35. package/dist/EveHistoryCluster.js.map +0 -1
  36. package/frontend/build/static/css/main.61f6cf42.css.map +0 -1
  37. package/frontend/build/static/js/main.6b861489.js.map +0 -1
  38. /package/frontend/build/static/js/{main.6b861489.js.LICENSE.txt → main.90cbbf6e.js.LICENSE.txt} +0 -0
@@ -22,7 +22,7 @@
22
22
  */
23
23
  import { MatterbridgeDevice } from './matterbridgeDevice.js';
24
24
  import { NodeStorageManager } from 'node-persist-manager';
25
- import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr } from 'node-ansi-logger';
25
+ import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr, RED, GREEN } from 'node-ansi-logger';
26
26
  import { fileURLToPath, pathToFileURL } from 'url';
27
27
  import { promises as fs } from 'fs';
28
28
  import { exec, spawn } from 'child_process';
@@ -30,6 +30,7 @@ import EventEmitter from 'events';
30
30
  import express from 'express';
31
31
  import os from 'os';
32
32
  import path from 'path';
33
+ import WebSocket, { WebSocketServer } from 'ws';
33
34
  import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
34
35
  import { BasicInformationCluster, BooleanStateCluster,
35
36
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -90,6 +91,7 @@ export class Matterbridge extends EventEmitter {
90
91
  nodeContext;
91
92
  expressApp;
92
93
  expressServer;
94
+ webSocketServer;
93
95
  storageManager;
94
96
  matterbridgeContext;
95
97
  mattercontrollerContext;
@@ -112,13 +114,74 @@ export class Matterbridge extends EventEmitter {
112
114
  static async loadInstance(initialize = false) {
113
115
  if (!Matterbridge.instance) {
114
116
  // eslint-disable-next-line no-console
115
- console.log(wr + 'Matterbridge instance does not exists!', initialize ? 'Initializing...' : 'Not initializing...', rs);
117
+ if (hasParameter('debug'))
118
+ console.log(wr + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
116
119
  Matterbridge.instance = new Matterbridge();
117
120
  if (initialize)
118
121
  await Matterbridge.instance.initialize();
119
122
  }
120
123
  return Matterbridge.instance;
121
124
  }
125
+ /**
126
+ * Initializes the Matterbridge instance as extension for zigbee2mqtt.
127
+ *
128
+ * @returns A Promise that resolves when the initialization is complete.
129
+ */
130
+ async initializeAsExtension(dataPath, debugEnabled) {
131
+ // Set the first port to use
132
+ this.port = 5560;
133
+ // Set Matterbridge logger
134
+ this.debugEnabled = debugEnabled;
135
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
136
+ this.log.debug('Matterbridge extension is starting...');
137
+ // Initialize NodeStorage
138
+ this.matterbridgeDirectory = dataPath;
139
+ this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
140
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
141
+ this.log.debug('Creating node storage context for matterbridge: matterbridge');
142
+ this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
143
+ // Log system info and create .matterbridge directory
144
+ await this.logNodeAndSystemInfo();
145
+ this.matterbridgeDirectory = dataPath;
146
+ // Set matter.js logger level and format
147
+ Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
148
+ Logger.format = Format.ANSI;
149
+ // Start the storage and create matterbridgeContext
150
+ await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
151
+ this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
152
+ if (!this.storageManager || !this.matterbridgeContext)
153
+ return;
154
+ await this.matterbridgeContext.set('softwareVersion', 1);
155
+ await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
156
+ await this.matterbridgeContext.set('hardwareVersion', 0);
157
+ await this.matterbridgeContext.set('hardwareVersionString', '0.0.1');
158
+ this.createMatterServer(this.storageManager);
159
+ this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
160
+ this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
161
+ this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
162
+ this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext);
163
+ this.log.debug('Adding matterbridge aggregator to commissioning server');
164
+ this.commissioningServer.addDevice(this.matterAggregator);
165
+ this.log.debug('Adding matterbridge commissioning server to matter server');
166
+ await this.matterServer?.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
167
+ this.log.debug(`Setting reachability to true for ${plg}Matterbridge${db}`);
168
+ this.commissioningServer.setReachability(true);
169
+ this.log.debug('Starting matter server...');
170
+ await this.startMatterServer();
171
+ this.log.info('Matter server started');
172
+ await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
173
+ }
174
+ /**
175
+ * Close the Matterbridge instance as extension for zigbee2mqtt.
176
+ *
177
+ * @returns A Promise that resolves when the initialization is complete.
178
+ */
179
+ async closeAsExtension() {
180
+ // Closing matter
181
+ await this.stopMatter();
182
+ // Closing storage
183
+ await this.stopStorage();
184
+ }
122
185
  /**
123
186
  * Initializes the Matterbridge application.
124
187
  *
@@ -199,72 +262,9 @@ export class Matterbridge extends EventEmitter {
199
262
  // Set matter.js logger level and format
200
263
  Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
201
264
  Logger.format = Format.ANSI;
202
- // Initialize NodeStorage
203
- /*
204
- this.log.debug('Creating node storage manager');
205
- this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'storage'), logging: false });
206
- this.log.debug('Creating node storage context for matterbridge');
207
- this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
208
- // Get the plugins from node storage
209
- this.registeredPlugins = await this.nodeContext.get<RegisteredPlugin[]>('plugins', []);
210
- for (const plugin of this.registeredPlugins) {
211
- this.log.debug(`Creating node storage context for plugin ${plugin.name}`);
212
- plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
213
- await plugin.nodeContext.set<string>('name', plugin.name);
214
- await plugin.nodeContext.set<string>('type', plugin.type);
215
- await plugin.nodeContext.set<string>('path', plugin.path);
216
- await plugin.nodeContext.set<string>('version', plugin.version);
217
- await plugin.nodeContext.set<string>('description', plugin.description);
218
- await plugin.nodeContext.set<string>('author', plugin.author);
219
- }
220
- */
221
265
  // Parse command line
222
266
  this.parseCommandLine();
223
267
  }
224
- /**
225
- * Spawns a child process with the given command and arguments.
226
- * @param command - The command to execute.
227
- * @param args - The arguments to pass to the command (default: []).
228
- * @returns A promise that resolves when the child process exits successfully, or rejects if there is an error.
229
- */
230
- async spawnCommand(command, args = []) {
231
- /*
232
- npm > npm.cmd on windows
233
- */
234
- if (process.platform === 'win32' && command === 'npm') {
235
- command = command + '.cmd';
236
- }
237
- if (process.platform === 'linux' && command === 'npm') {
238
- args.unshift(command);
239
- command = 'sudo';
240
- }
241
- return new Promise((resolve, reject) => {
242
- const childProcess = spawn(command, args, {
243
- stdio: 'inherit',
244
- });
245
- childProcess.on('error', (err) => {
246
- this.log.error(`Failed to start child process: ${err.message}`);
247
- reject(err); // Reject the promise on error
248
- });
249
- childProcess.on('close', (code) => {
250
- if (code === 0) {
251
- this.log.info(`Child process stdio streams have closed with code ${code}`);
252
- resolve();
253
- }
254
- else {
255
- this.log.error(`Child process stdio streams have closed with code ${code}`);
256
- reject(new Error(`Process exited with code ${code}`));
257
- }
258
- });
259
- // The 'exit' event might be redundant here since 'close' is also being handled
260
- childProcess.on('exit', (code, signal) => {
261
- this.log.info(`Child process exited with code ${code} and signal ${signal}`);
262
- });
263
- childProcess.on('disconnect', () => {
264
- this.log.info('Child process has been disconnected from the parent');
265
- });
266
- });
267
- }
268
268
  /**
269
269
  * Parses the command line arguments and performs the corresponding actions.
270
270
  * @private
@@ -272,15 +272,27 @@ export class Matterbridge extends EventEmitter {
272
272
  */
273
273
  async parseCommandLine() {
274
274
  if (hasParameter('list')) {
275
- this.log.info('Registered plugins:');
275
+ this.log.info('Registered plugins');
276
276
  this.registeredPlugins.forEach((plugin, index) => {
277
- if (index === this.registeredPlugins.length - 1) {
278
- this.log.info(`└─┬─ ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${YELLOW}${plugin.enabled ? 'enabled' : 'disabled'}${nf}`);
279
- this.log.info(` └─ ${db}${plugin.path}${db}`);
277
+ if (index !== this.registeredPlugins.length - 1) {
278
+ this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
279
+ this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
280
280
  }
281
281
  else {
282
- this.log.info(`├─┬─ ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${YELLOW}${plugin.enabled ? 'enabled' : 'disabled'}${nf}`);
283
- this.log.info(`│ └─ ${db}${plugin.path}${db}`);
282
+ this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
283
+ this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
284
+ }
285
+ });
286
+ const serializedRegisteredDevices = await this.nodeContext?.get('devices', []);
287
+ this.log.info('│ Registered devices');
288
+ serializedRegisteredDevices?.forEach((device, index) => {
289
+ if (index !== serializedRegisteredDevices.length - 1) {
290
+ this.log.info(`├─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
291
+ this.log.info(`│ └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
292
+ }
293
+ else {
294
+ this.log.info(`└─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
295
+ this.log.info(` └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
284
296
  }
285
297
  });
286
298
  this.emit('shutdown');
@@ -631,6 +643,24 @@ export class Matterbridge extends EventEmitter {
631
643
  this.expressApp.removeAllListeners();
632
644
  this.expressApp = undefined;
633
645
  }
646
+ // Close the WebSocket server
647
+ if (this.webSocketServer) {
648
+ // Close all active connections
649
+ this.webSocketServer.clients.forEach((client) => {
650
+ if (client.readyState === WebSocket.OPEN) {
651
+ client.close();
652
+ }
653
+ });
654
+ this.webSocketServer.close((error) => {
655
+ if (error) {
656
+ this.log.error(`Error closing WebSocket server: ${error}`);
657
+ }
658
+ else {
659
+ this.log.debug('WebSocket server closed successfully');
660
+ }
661
+ });
662
+ this.webSocketServer = undefined;
663
+ }
634
664
  /*const cleanupTimeout1 =*/ setTimeout(async () => {
635
665
  // Closing matter
636
666
  await this.stopMatter();
@@ -641,7 +671,9 @@ export class Matterbridge extends EventEmitter {
641
671
  this.log.info('Saving registered devices...');
642
672
  const serializedRegisteredDevices = [];
643
673
  this.registeredDevices.forEach((registeredDevice) => {
644
- serializedRegisteredDevices.push(registeredDevice.device.serialize(registeredDevice.plugin));
674
+ const serializedMatterbridgeDevice = registeredDevice.device.serialize(registeredDevice.plugin);
675
+ //this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
676
+ serializedRegisteredDevices.push(serializedMatterbridgeDevice);
645
677
  });
646
678
  await this.nodeContext.set('devices', serializedRegisteredDevices);
647
679
  this.log.info('Saved registered devices');
@@ -677,7 +709,7 @@ export class Matterbridge extends EventEmitter {
677
709
  Matterbridge.instance = undefined;
678
710
  this.emit('shutdown');
679
711
  }
680
- }, 1 * 1000);
712
+ }, 2 * 1000);
681
713
  //cleanupTimeout2.unref();
682
714
  }, 3 * 1000);
683
715
  //cleanupTimeout1.unref();
@@ -812,10 +844,10 @@ export class Matterbridge extends EventEmitter {
812
844
  if (plugin.addedDevices !== undefined)
813
845
  plugin.addedDevices--;
814
846
  }
815
- // Only register the device in childbridge mode
847
+ // Remove the device in childbridge mode
816
848
  if (this.bridgeMode === 'childbridge') {
817
849
  if (plugin.type === 'AccessoryPlatform') {
818
- this.log.warn(`Removing bridged device ${dev}${device.deviceName}${wr} (${dev}${device.name}${wr}) for plugin ${plg}${pluginName}${wr}: AccessoryPlatform not supported in childbridge mode`);
850
+ this.log.info(`Removing bridged device ${dev}${device.deviceName}${nf} (${dev}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}: AccessoryPlatform not supported in childbridge mode`);
819
851
  }
820
852
  else if (plugin.type === 'DynamicPlatform') {
821
853
  this.registeredDevices.forEach((registeredDevice, index) => {
@@ -844,7 +876,7 @@ export class Matterbridge extends EventEmitter {
844
876
  async removeAllBridgedDevices(pluginName) {
845
877
  const plugin = this.findPlugin(pluginName);
846
878
  if (this.bridgeMode === 'childbridge' && plugin?.type === 'AccessoryPlatform') {
847
- this.log.warn(`Removing devices for plugin ${plg}${pluginName}${wr} error: AccessoryPlatform not supported in childbridge mode`);
879
+ this.log.info(`Removing devices for plugin ${plg}${pluginName}${nf}: AccessoryPlatform not supported in childbridge mode`);
848
880
  return;
849
881
  }
850
882
  const devicesToRemove = [];
@@ -1209,7 +1241,9 @@ export class Matterbridge extends EventEmitter {
1209
1241
  // Call the default export function of the plugin, passing this MatterBridge instance
1210
1242
  if (pluginInstance.default) {
1211
1243
  const config = await this.loadPluginConfig(plugin);
1212
- const platform = pluginInstance.default(this, new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled }), config);
1244
+ const log = new AnsiLogger({ logName: plugin.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
1245
+ //log.setCallback(this.wssSendMessage.bind(this));
1246
+ const platform = pluginInstance.default(this, log, config);
1213
1247
  platform.name = packageJson.name;
1214
1248
  platform.config = config;
1215
1249
  plugin.name = packageJson.name;
@@ -2017,11 +2051,6 @@ export class Matterbridge extends EventEmitter {
2017
2051
  this.matterbridgeInformation.rootDirectory = this.rootDirectory;
2018
2052
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
2019
2053
  // Global node_modules directory
2020
- /*
2021
- this.globalModulesDirectory = await this.getGlobalNodeModules();
2022
- this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
2023
- this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
2024
- */
2025
2054
  if (this.nodeContext)
2026
2055
  this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
2027
2056
  this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
@@ -2089,11 +2118,6 @@ export class Matterbridge extends EventEmitter {
2089
2118
  this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
2090
2119
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
2091
2120
  // Matterbridge latest version
2092
- /*
2093
- this.matterbridgeLatestVersion = await this.getLatestVersion('matterbridge');
2094
- this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeLatestVersion;
2095
- this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
2096
- */
2097
2121
  if (this.nodeContext)
2098
2122
  this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
2099
2123
  this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
@@ -2163,15 +2187,121 @@ export class Matterbridge extends EventEmitter {
2163
2187
  }));
2164
2188
  return baseRegisteredPlugins;
2165
2189
  }
2190
+ /**
2191
+ * Spawns a child process with the given command and arguments.
2192
+ * @param command - The command to execute.
2193
+ * @param args - The arguments to pass to the command (default: []).
2194
+ * @returns A promise that resolves when the child process exits successfully, or rejects if there is an error.
2195
+ */
2196
+ async spawnCommand(command, args = []) {
2197
+ /*
2198
+ npm > npm.cmd on windows
2199
+ cmd.exe ['dir'] on windows
2200
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2201
+ process.on('unhandledRejection', (reason, promise) => {
2202
+ this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
2203
+ });
2204
+ */
2205
+ if (process.platform === 'win32' && command === 'npm') {
2206
+ // Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
2207
+ const argstring = 'npm ' + args.join(' ');
2208
+ args.splice(0, args.length, '/c', argstring);
2209
+ command = 'cmd.exe';
2210
+ }
2211
+ if (process.platform === 'linux' && command === 'npm' && !hasParameter('docker')) {
2212
+ args.unshift(command);
2213
+ command = 'sudo';
2214
+ }
2215
+ this.log.debug(`Spawning command ${command} with ${debugStringify(args)}`);
2216
+ return new Promise((resolve, reject) => {
2217
+ const childProcess = spawn(command, args, {
2218
+ stdio: ['inherit', 'pipe', 'pipe'],
2219
+ });
2220
+ childProcess.on('error', (err) => {
2221
+ this.log.error(`Failed to start child process: ${err.message}`);
2222
+ reject(err); // Reject the promise on error
2223
+ });
2224
+ childProcess.on('close', (code) => {
2225
+ if (code === 0) {
2226
+ this.log.debug(`Child process stdio streams have closed with code ${code}`);
2227
+ resolve();
2228
+ }
2229
+ else {
2230
+ this.log.error(`Child process stdio streams have closed with code ${code}`);
2231
+ reject(new Error(`Child process stdio streams have closed with code ${code}`));
2232
+ }
2233
+ });
2234
+ childProcess.on('exit', (code, signal) => {
2235
+ if (code === 0) {
2236
+ this.log.debug(`Child process exited with code ${code} and signal ${signal}`);
2237
+ resolve();
2238
+ }
2239
+ else {
2240
+ this.log.error(`Child process exited with code ${code} and signal ${signal}`);
2241
+ reject(new Error(`Child process exited with code ${code} and signal ${signal}`));
2242
+ }
2243
+ });
2244
+ childProcess.on('disconnect', () => {
2245
+ this.log.debug('Child process has been disconnected from the parent');
2246
+ resolve();
2247
+ });
2248
+ if (childProcess.stdout) {
2249
+ childProcess.stdout.on('data', (data) => {
2250
+ const message = data.toString().trim();
2251
+ //this.log.info('\n' + message);
2252
+ this.wssSendMessage('spawn', 'stdout', message);
2253
+ });
2254
+ }
2255
+ if (childProcess.stderr) {
2256
+ childProcess.stderr.on('data', (data) => {
2257
+ const message = data.toString().trim();
2258
+ //this.log.debug('\n' + message);
2259
+ this.wssSendMessage('spawn', 'stderr', message);
2260
+ });
2261
+ }
2262
+ });
2263
+ }
2264
+ wssSendMessage(type, subType, message) {
2265
+ // Remove ANSI escape codes from the message
2266
+ // eslint-disable-next-line no-control-regex
2267
+ const cleanMessage = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2268
+ // Remove leading asterisks from the message
2269
+ const finalMessage = cleanMessage.replace(/^\*+/, '');
2270
+ this.webSocketServer?.clients.forEach((client) => {
2271
+ if (client.readyState === WebSocket.OPEN) {
2272
+ client.send(JSON.stringify({ type, subType, message: finalMessage }));
2273
+ }
2274
+ });
2275
+ }
2166
2276
  /**
2167
2277
  * Initializes the frontend of Matterbridge.
2168
2278
  *
2169
2279
  * @param port The port number to run the frontend server on. Default is 3000.
2170
2280
  */
2171
- async initializeFrontend(port = 3000) {
2281
+ async initializeFrontend(port = 8283) {
2172
2282
  this.log.debug(`Initializing the frontend on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
2173
- this.expressApp = express();
2283
+ // Create a WebSocket server
2284
+ this.webSocketServer = new WebSocketServer({ port: 8284 });
2285
+ this.webSocketServer.on('connection', (ws) => {
2286
+ this.log.debug('WebSocketServer client connected');
2287
+ this.log.setGlobalCallback(this.wssSendMessage.bind(this));
2288
+ this.log.debug('WebSocketServer activated logger callback');
2289
+ ws.on('message', (message) => {
2290
+ this.log.debug(`WebSocketServer received message => ${message}`);
2291
+ });
2292
+ ws.on('close', () => {
2293
+ this.log.debug('WebSocketServer client disconnected');
2294
+ if (this.webSocketServer?.clients.size === 0) {
2295
+ this.log.setGlobalCallback(undefined);
2296
+ this.log.debug('WebSocketServer deactivated logger callback');
2297
+ }
2298
+ });
2299
+ ws.on('error', (error) => {
2300
+ this.log.error(`WebSocketServer error: ${error}`);
2301
+ });
2302
+ });
2174
2303
  // Serve React build directory
2304
+ this.expressApp = express();
2175
2305
  this.expressApp.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
2176
2306
  // Endpoint to provide login code
2177
2307
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
@@ -2193,6 +2323,34 @@ export class Matterbridge extends EventEmitter {
2193
2323
  res.json({ valid: false });
2194
2324
  }
2195
2325
  });
2326
+ // Endpoint to provide host
2327
+ this.expressApp.get('/api/wsshost', express.json(), async (req, res) => {
2328
+ this.log.debug('The frontend sent /api/wsshost');
2329
+ res.json({ host: os.hostname() });
2330
+ });
2331
+ // Endpoint to provide port
2332
+ this.expressApp.get('/api/wssport', express.json(), async (req, res) => {
2333
+ this.log.debug('The frontend sent /api/wssport');
2334
+ res.json({ port: 8284 });
2335
+ });
2336
+ // Endpoint to provide manual pairing code
2337
+ this.expressApp.get('/api/pairing-code', (req, res) => {
2338
+ this.log.debug('The frontend sent /api/pairing-code');
2339
+ if (!this.matterbridgeContext) {
2340
+ this.log.error('/api/pairing-code matterbridgeContext not found');
2341
+ res.json([]);
2342
+ return;
2343
+ }
2344
+ try {
2345
+ const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode'), manualPairingCode: this.matterbridgeContext.get('manualPairingCode') };
2346
+ res.json(qrData);
2347
+ }
2348
+ catch (error) {
2349
+ if (this.bridgeMode === 'bridge')
2350
+ this.log.error('qrPairingCode for /api/qr-code not found');
2351
+ res.json({});
2352
+ }
2353
+ });
2196
2354
  // Endpoint to provide QR pairing code
2197
2355
  this.expressApp.get('/api/qr-code', (req, res) => {
2198
2356
  this.log.debug('The frontend sent /api/qr-code');
@@ -2307,7 +2465,7 @@ export class Matterbridge extends EventEmitter {
2307
2465
  res.status(400).json({ error: 'No command provided' });
2308
2466
  return;
2309
2467
  }
2310
- this.log.debug(`*Received frontend command: ${command}:${param}`);
2468
+ this.log.debug(`Received frontend command: ${command}:${param}`);
2311
2469
  // Handle the command setpassword from Settings
2312
2470
  if (command === 'setpassword') {
2313
2471
  const password = param.slice(1, -1); // Remove the first and last characters
@@ -2345,32 +2503,34 @@ export class Matterbridge extends EventEmitter {
2345
2503
  }
2346
2504
  // Handle the command update from Header
2347
2505
  if (command === 'update') {
2348
- this.log.warn(`***Updating matterbridge ${plg}${param}${db}`);
2506
+ this.log.info('Updating matterbridge...');
2507
+ //this.wssSendMessage('cmd', 'update', 'Updating matterbridge');
2349
2508
  try {
2350
- await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2509
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge', '--loglevel=verbose']);
2351
2510
  this.log.info('Matterbridge has been updated. Full restart required.');
2511
+ //this.wssSendMessage('cmd', 'update', 'Matterbridge has been updated. Full restart required.');
2352
2512
  }
2353
2513
  catch (error) {
2354
2514
  this.log.error('Error updating matterbridge');
2515
+ //this.wssSendMessage('cmd', 'update', 'Error updating matterbridge');
2355
2516
  res.json({ message: 'Command received' });
2356
2517
  return;
2357
2518
  }
2358
2519
  this.updateProcess();
2359
2520
  }
2360
- // Handle the command update from Header
2361
- if (command === 'update') {
2362
- this.log.warn(`The /api/command/${command} is not yet implemented`);
2363
- }
2364
2521
  // Handle the command installplugin from Home
2365
2522
  if (command === 'installplugin') {
2366
2523
  param = param.replace(/\*/g, '\\');
2367
- this.log.warn(`***Installing plugin ${plg}${param}${db}`);
2524
+ this.log.info(`Installing plugin ${plg}${param}${db}...`);
2525
+ //this.wssSendMessage('cmd', 'installplugin', `Installing plugin ${param}`);
2368
2526
  try {
2369
- await this.spawnCommand('npm', ['install', '-g', param]);
2370
- this.log.info(`Plugin ${plg}${param}${nf} installed. Restart required.`);
2527
+ await this.spawnCommand('npm', ['install', '-g', param, '--loglevel=verbose']);
2528
+ this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
2529
+ //this.wssSendMessage('cmd', 'installplugin', `Plugin ${param} installed. Full restart required.`);
2371
2530
  }
2372
2531
  catch (error) {
2373
2532
  this.log.error(`Error installing plugin ${plg}${param}${er}`);
2533
+ //this.wssSendMessage('cmd', 'installplugin', `Error installing plugin${param}`);
2374
2534
  res.json({ message: 'Command received' });
2375
2535
  return;
2376
2536
  }
@@ -2563,7 +2723,6 @@ function restartProcess() {
2563
2723
  }
2564
2724
 
2565
2725
  import * as WebSocket from 'ws';
2566
- const globalModulesDir = require('global-modules');
2567
2726
 
2568
2727
  const wss = new WebSocket.Server({ port: 8080 });
2569
2728
 
@@ -2582,13 +2741,6 @@ ws.onmessage = (event) => {
2582
2741
  console.log(`Received message => ${event.data}`);
2583
2742
  };
2584
2743
 
2585
- */
2586
- /*
2587
- // In Matterbridge
2588
- global.matterbridgeInstance = Matterbridge.loadInstance();
2589
-
2590
- // In plugins
2591
- const matterbridge = global.matterbridgeInstance;
2592
2744
  */
2593
2745
  /*
2594
2746
  npx create-react-app matterbridge-frontend