matterbridge 1.7.3 → 2.0.0-edge1

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 (76) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/cli.js +3 -13
  3. package/dist/cli.js.map +1 -1
  4. package/dist/deviceManager.d.ts +7 -7
  5. package/dist/deviceManager.d.ts.map +1 -1
  6. package/dist/deviceManager.js +2 -2
  7. package/dist/deviceManager.js.map +1 -1
  8. package/dist/frontend.d.ts +98 -0
  9. package/dist/frontend.d.ts.map +1 -0
  10. package/dist/frontend.js +1377 -0
  11. package/dist/frontend.js.map +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -6
  14. package/dist/index.js.map +1 -1
  15. package/dist/matter/export.d.ts.map +1 -1
  16. package/dist/matter/export.js +0 -1
  17. package/dist/matter/export.js.map +1 -1
  18. package/dist/matterbridge.d.ts +82 -208
  19. package/dist/matterbridge.d.ts.map +1 -1
  20. package/dist/matterbridge.js +777 -2310
  21. package/dist/matterbridge.js.map +1 -1
  22. package/dist/matterbridgeBehaviors.d.ts +32 -851
  23. package/dist/matterbridgeBehaviors.d.ts.map +1 -1
  24. package/dist/matterbridgeBehaviors.js +22 -2
  25. package/dist/matterbridgeBehaviors.js.map +1 -1
  26. package/dist/matterbridgeEndpoint.d.ts +100 -9089
  27. package/dist/matterbridgeEndpoint.d.ts.map +1 -1
  28. package/dist/matterbridgeEndpoint.js +59 -31
  29. package/dist/matterbridgeEndpoint.js.map +1 -1
  30. package/dist/matterbridgePlatform.d.ts +14 -28
  31. package/dist/matterbridgePlatform.d.ts.map +1 -1
  32. package/dist/matterbridgePlatform.js +7 -23
  33. package/dist/matterbridgePlatform.js.map +1 -1
  34. package/dist/matterbridgeTypes.d.ts.map +1 -1
  35. package/dist/matterbridgeTypes.js +4 -0
  36. package/dist/matterbridgeTypes.js.map +1 -1
  37. package/dist/pluginManager.d.ts +1 -1
  38. package/dist/pluginManager.d.ts.map +1 -1
  39. package/dist/pluginManager.js +5 -11
  40. package/dist/pluginManager.js.map +1 -1
  41. package/dist/utils/utils.d.ts +1 -1
  42. package/dist/utils/utils.d.ts.map +1 -1
  43. package/dist/utils/utils.js +6 -6
  44. package/dist/utils/utils.js.map +1 -1
  45. package/frontend/build/asset-manifest.json +3 -3
  46. package/frontend/build/index.html +1 -1
  47. package/frontend/build/static/js/{main.6bbd1772.js → main.ea28015b.js} +3 -3
  48. package/frontend/build/static/js/main.ea28015b.js.map +1 -0
  49. package/npm-shrinkwrap.json +9 -9
  50. package/package.json +2 -3
  51. package/dist/cli.d.ts +0 -25
  52. package/dist/cluster/export.d.ts +0 -2
  53. package/dist/defaultConfigSchema.d.ts +0 -27
  54. package/dist/index.d.ts +0 -40
  55. package/dist/logger/export.d.ts +0 -2
  56. package/dist/matter/export.d.ts +0 -11
  57. package/dist/matterbridgeAccessoryPlatform.d.ts +0 -39
  58. package/dist/matterbridgeDevice.d.ts +0 -7077
  59. package/dist/matterbridgeDevice.d.ts.map +0 -1
  60. package/dist/matterbridgeDevice.js +0 -2736
  61. package/dist/matterbridgeDevice.js.map +0 -1
  62. package/dist/matterbridgeDynamicPlatform.d.ts +0 -39
  63. package/dist/matterbridgeEdge.d.ts +0 -91
  64. package/dist/matterbridgeEdge.d.ts.map +0 -1
  65. package/dist/matterbridgeEdge.js +0 -1077
  66. package/dist/matterbridgeEdge.js.map +0 -1
  67. package/dist/matterbridgeTypes.d.ts +0 -172
  68. package/dist/matterbridgeWebsocket.d.ts +0 -49
  69. package/dist/matterbridgeWebsocket.d.ts.map +0 -1
  70. package/dist/matterbridgeWebsocket.js +0 -325
  71. package/dist/matterbridgeWebsocket.js.map +0 -1
  72. package/dist/storage/export.d.ts +0 -2
  73. package/dist/utils/colorUtils.d.ts +0 -61
  74. package/dist/utils/export.d.ts +0 -3
  75. package/frontend/build/static/js/main.6bbd1772.js.map +0 -1
  76. /package/frontend/build/static/js/{main.6bbd1772.js.LICENSE.txt → main.ea28015b.js.LICENSE.txt} +0 -0
@@ -0,0 +1,1377 @@
1
+ /**
2
+ * This file contains the class Frontend.
3
+ *
4
+ * @file frontend.ts
5
+ * @author Luca Liguori
6
+ * @date 2025-01-13
7
+ * @version 1.0.0
8
+ *
9
+ * Copyright 2025, 2026, 2027 Luca Liguori.
10
+ *
11
+ * Licensed under the Apache License, Version 2.0 (the "License");
12
+ * you may not use this file except in compliance with the License.
13
+ * You may obtain a copy of the License at
14
+ *
15
+ * http://www.apache.org/licenses/LICENSE-2.0
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License. *
22
+ */
23
+ // @matter
24
+ import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat } from '@matter/main';
25
+ // Node modules
26
+ import { createServer } from 'http';
27
+ import https from 'https';
28
+ import express from 'express';
29
+ import WebSocket, { WebSocketServer } from 'ws';
30
+ import os from 'os';
31
+ import path from 'path';
32
+ import { promises as fs } from 'fs';
33
+ // AnsiLogger module
34
+ import { AnsiLogger, CYAN, db, debugStringify, er, LogLevel, nf, rs, stringify, TimestampFormat, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
35
+ // Matterbridge
36
+ import { createZip, hasParameter, isValidNumber, isValidObject, isValidString } from './utils/utils.js';
37
+ import { plg } from './matterbridgeTypes.js';
38
+ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
39
+ /**
40
+ * Websocket message ID for logging.
41
+ * @constant {number}
42
+ */
43
+ export const WS_ID_LOG = 0;
44
+ /**
45
+ * Websocket message ID indicating a refresh is needed.
46
+ * @constant {number}
47
+ */
48
+ export const WS_ID_REFRESH_NEEDED = 1;
49
+ /**
50
+ * Websocket message ID indicating a restart is needed.
51
+ * @constant {number}
52
+ */
53
+ export const WS_ID_RESTART_NEEDED = 2;
54
+ /**
55
+ * Initializes the frontend of Matterbridge.
56
+ *
57
+ * @param port The port number to run the frontend server on. Default is 8283.
58
+ */
59
+ export class Frontend {
60
+ matterbridge;
61
+ log;
62
+ port = 8283;
63
+ initializeError = false;
64
+ expressApp;
65
+ httpServer;
66
+ httpsServer;
67
+ webSocketServer;
68
+ constructor(matterbridge) {
69
+ this.matterbridge = matterbridge;
70
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: hasParameter('debug') ? LogLevel.DEBUG : LogLevel.INFO });
71
+ }
72
+ async start(port = 8283) {
73
+ this.port = port;
74
+ this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
75
+ // Create the express app that serves the frontend
76
+ this.expressApp = express();
77
+ // Log all requests to the server for debugging
78
+ /*
79
+ this.expressApp.use((req, res, next) => {
80
+ this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`);
81
+ next();
82
+ });
83
+ */
84
+ // Serve static files from '/static' endpoint
85
+ this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
86
+ if (!hasParameter('ssl')) {
87
+ // Create an HTTP server and attach the express app
88
+ this.httpServer = createServer(this.expressApp);
89
+ // Listen on the specified port
90
+ if (hasParameter('ingress')) {
91
+ this.httpServer.listen(this.port, '0.0.0.0', () => {
92
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
93
+ });
94
+ }
95
+ else {
96
+ this.httpServer.listen(this.port, () => {
97
+ if (this.matterbridge.systemInformation.ipv4Address !== '')
98
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
99
+ if (this.matterbridge.systemInformation.ipv6Address !== '')
100
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
101
+ });
102
+ }
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ this.httpServer.on('error', (error) => {
105
+ this.log.error(`Frontend http server error listening on ${this.port}`);
106
+ switch (error.code) {
107
+ case 'EACCES':
108
+ this.log.error(`Port ${this.port} requires elevated privileges`);
109
+ break;
110
+ case 'EADDRINUSE':
111
+ this.log.error(`Port ${this.port} is already in use`);
112
+ break;
113
+ }
114
+ this.initializeError = true;
115
+ return;
116
+ });
117
+ }
118
+ else {
119
+ // Load the SSL certificate, the private key and optionally the CA certificate
120
+ let cert;
121
+ try {
122
+ cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
123
+ this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
124
+ }
125
+ catch (error) {
126
+ this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
127
+ return;
128
+ }
129
+ let key;
130
+ try {
131
+ key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
132
+ this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`);
133
+ }
134
+ catch (error) {
135
+ this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
136
+ return;
137
+ }
138
+ let ca;
139
+ try {
140
+ ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
141
+ this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`);
142
+ }
143
+ catch (error) {
144
+ this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
145
+ }
146
+ const serverOptions = { cert, key, ca };
147
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
148
+ this.httpsServer = https.createServer(serverOptions, this.expressApp);
149
+ // Listen on the specified port
150
+ if (hasParameter('ingress')) {
151
+ this.httpsServer.listen(this.port, '0.0.0.0', () => {
152
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
153
+ });
154
+ }
155
+ else {
156
+ this.httpsServer.listen(this.port, () => {
157
+ if (this.matterbridge.systemInformation.ipv4Address !== '')
158
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
159
+ if (this.matterbridge.systemInformation.ipv6Address !== '')
160
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
161
+ });
162
+ }
163
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ this.httpsServer.on('error', (error) => {
165
+ this.log.error(`Frontend https server error listening on ${this.port}`);
166
+ switch (error.code) {
167
+ case 'EACCES':
168
+ this.log.error(`Port ${this.port} requires elevated privileges`);
169
+ break;
170
+ case 'EADDRINUSE':
171
+ this.log.error(`Port ${this.port} is already in use`);
172
+ break;
173
+ }
174
+ this.initializeError = true;
175
+ return;
176
+ });
177
+ }
178
+ if (this.initializeError)
179
+ return;
180
+ // Createe a WebSocket server and attach it to the http or https server
181
+ const wssPort = this.port;
182
+ const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
183
+ this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
184
+ this.webSocketServer.on('connection', (ws, request) => {
185
+ const clientIp = request.socket.remoteAddress;
186
+ AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), LogLevel.DEBUG);
187
+ this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
188
+ ws.on('message', (message) => {
189
+ this.wsMessageHandler(ws, message);
190
+ });
191
+ ws.on('ping', () => {
192
+ this.log.debug('WebSocket client ping');
193
+ ws.pong();
194
+ });
195
+ ws.on('pong', () => {
196
+ this.log.debug('WebSocket client pong');
197
+ });
198
+ ws.on('close', () => {
199
+ this.log.info('WebSocket client disconnected');
200
+ if (this.webSocketServer?.clients.size === 0) {
201
+ AnsiLogger.setGlobalCallback(undefined);
202
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
203
+ }
204
+ });
205
+ ws.on('error', (error) => {
206
+ this.log.error(`WebSocket client error: ${error}`);
207
+ });
208
+ });
209
+ this.webSocketServer.on('close', () => {
210
+ this.log.debug(`WebSocketServer closed`);
211
+ });
212
+ this.webSocketServer.on('listening', () => {
213
+ this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
214
+ });
215
+ this.webSocketServer.on('error', (ws, error) => {
216
+ this.log.error(`WebSocketServer error: ${error}`);
217
+ });
218
+ // Endpoint to validate login code
219
+ this.expressApp.post('/api/login', express.json(), async (req, res) => {
220
+ const { password } = req.body;
221
+ this.log.debug('The frontend sent /api/login', password);
222
+ if (!this.matterbridge.nodeContext) {
223
+ this.log.error('/api/login nodeContext not found');
224
+ res.json({ valid: false });
225
+ return;
226
+ }
227
+ try {
228
+ const storedPassword = await this.matterbridge.nodeContext.get('password', '');
229
+ if (storedPassword === '' || password === storedPassword) {
230
+ this.log.debug('/api/login password valid');
231
+ res.json({ valid: true });
232
+ }
233
+ else {
234
+ this.log.warn('/api/login error wrong password');
235
+ res.json({ valid: false });
236
+ }
237
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
238
+ }
239
+ catch (error) {
240
+ this.log.error('/api/login error getting password');
241
+ res.json({ valid: false });
242
+ }
243
+ });
244
+ // Endpoint to provide health check
245
+ this.expressApp.get('/health', (req, res) => {
246
+ this.log.debug('Express received /health');
247
+ const healthStatus = {
248
+ status: 'ok', // Indicate service is healthy
249
+ uptime: process.uptime(), // Server uptime in seconds
250
+ timestamp: new Date().toISOString(), // Current timestamp
251
+ };
252
+ res.status(200).json(healthStatus);
253
+ });
254
+ // Endpoint to provide settings
255
+ this.expressApp.get('/api/settings', express.json(), async (req, res) => {
256
+ this.log.debug('The frontend sent /api/settings');
257
+ this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
258
+ this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
259
+ this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
260
+ this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
261
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
262
+ this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
263
+ this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
264
+ this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
265
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
266
+ this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
267
+ this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
268
+ this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
269
+ this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
270
+ this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
271
+ this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
272
+ this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
273
+ this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
274
+ const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
275
+ // this.log.debug('Response:', debugStringify(response));
276
+ res.json(response);
277
+ });
278
+ // Endpoint to provide plugins
279
+ this.expressApp.get('/api/plugins', async (req, res) => {
280
+ this.log.debug('The frontend sent /api/plugins');
281
+ const response = await this.getBaseRegisteredPlugins();
282
+ // this.log.debug('Response:', debugStringify(response));
283
+ res.json(response);
284
+ });
285
+ // Endpoint to provide devices
286
+ this.expressApp.get('/api/devices', (req, res) => {
287
+ this.log.debug('The frontend sent /api/devices');
288
+ const devices = [];
289
+ this.matterbridge.devices.forEach(async (device) => {
290
+ // Check if the device has the required properties
291
+ if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
292
+ return;
293
+ const cluster = this.getClusterTextFromDevice(device);
294
+ devices.push({
295
+ pluginName: device.plugin,
296
+ type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
297
+ endpoint: device.number,
298
+ name: device.deviceName,
299
+ serial: device.serialNumber,
300
+ productUrl: device.productUrl,
301
+ configUrl: device.configUrl,
302
+ uniqueId: device.uniqueId,
303
+ cluster: cluster,
304
+ });
305
+ });
306
+ // this.log.debug('Response:', debugStringify(data));
307
+ res.json(devices);
308
+ });
309
+ // Endpoint to provide the cluster servers of the devices
310
+ this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
311
+ const selectedPluginName = req.params.selectedPluginName;
312
+ const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
313
+ this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
314
+ if (selectedPluginName === 'none') {
315
+ res.json([]);
316
+ return;
317
+ }
318
+ const data = [];
319
+ this.matterbridge.devices.forEach(async (device) => {
320
+ const pluginName = device.plugin;
321
+ if (pluginName === selectedPluginName && device.number === selectedDeviceEndpoint) {
322
+ const endpointServer = EndpointServer.forEndpoint(device);
323
+ const clusterServers = endpointServer.getAllClusterServers();
324
+ clusterServers.forEach((clusterServer) => {
325
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
326
+ if (clusterServer.name === 'EveHistory')
327
+ return;
328
+ let attributeValue;
329
+ try {
330
+ if (typeof value.getLocal() === 'object')
331
+ attributeValue = stringify(value.getLocal());
332
+ else
333
+ attributeValue = value.getLocal().toString();
334
+ }
335
+ catch (error) {
336
+ attributeValue = 'Fabric-Scoped';
337
+ this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
338
+ }
339
+ data.push({
340
+ endpoint: device.number ? device.number.toString() : '...',
341
+ clusterName: clusterServer.name,
342
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
343
+ attributeName: key,
344
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
345
+ attributeValue,
346
+ });
347
+ });
348
+ });
349
+ endpointServer.getChildEndpoints().forEach((childEndpoint) => {
350
+ const name = childEndpoint.name;
351
+ const clusterServers = childEndpoint.getAllClusterServers();
352
+ clusterServers.forEach((clusterServer) => {
353
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
354
+ if (clusterServer.name === 'EveHistory')
355
+ return;
356
+ let attributeValue;
357
+ try {
358
+ if (typeof value.getLocal() === 'object')
359
+ attributeValue = stringify(value.getLocal());
360
+ else
361
+ attributeValue = value.getLocal().toString();
362
+ }
363
+ catch (error) {
364
+ attributeValue = 'Fabric-Scoped';
365
+ this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
366
+ }
367
+ data.push({
368
+ endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''),
369
+ clusterName: clusterServer.name,
370
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
371
+ attributeName: key,
372
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
373
+ attributeValue,
374
+ });
375
+ });
376
+ });
377
+ });
378
+ }
379
+ });
380
+ res.json(data);
381
+ });
382
+ // Endpoint to view the log
383
+ this.expressApp.get('/api/view-log', async (req, res) => {
384
+ this.log.debug('The frontend sent /api/log');
385
+ try {
386
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
387
+ res.type('text/plain');
388
+ res.send(data);
389
+ }
390
+ catch (error) {
391
+ this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
392
+ res.status(500).send('Error reading log file');
393
+ }
394
+ });
395
+ // Endpoint to download the matterbridge log
396
+ this.expressApp.get('/api/download-mblog', async (req, res) => {
397
+ this.log.debug('The frontend sent /api/download-mblog');
398
+ try {
399
+ await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
400
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
401
+ }
402
+ catch (error) {
403
+ fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
404
+ }
405
+ res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
406
+ if (error) {
407
+ this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
408
+ res.status(500).send('Error downloading the matterbridge log file');
409
+ }
410
+ });
411
+ });
412
+ // Endpoint to download the matter log
413
+ this.expressApp.get('/api/download-mjlog', async (req, res) => {
414
+ this.log.debug('The frontend sent /api/download-mjlog');
415
+ try {
416
+ await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
417
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
418
+ }
419
+ catch (error) {
420
+ fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
421
+ }
422
+ res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
423
+ if (error) {
424
+ this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
425
+ res.status(500).send('Error downloading the matter log file');
426
+ }
427
+ });
428
+ });
429
+ // Endpoint to download the matter storage file
430
+ this.expressApp.get('/api/download-mjstorage', (req, res) => {
431
+ this.log.debug('The frontend sent /api/download-mjstorage');
432
+ res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName), 'matterbridge.json', (error) => {
433
+ if (error) {
434
+ this.log.error(`Error downloading log file ${this.matterbridge.matterStorageName}: ${error instanceof Error ? error.message : error}`);
435
+ res.status(500).send('Error downloading the matter storage file');
436
+ }
437
+ });
438
+ });
439
+ // Endpoint to download the matterbridge storage directory
440
+ this.expressApp.get('/api/download-mbstorage', async (req, res) => {
441
+ this.log.debug('The frontend sent /api/download-mbstorage');
442
+ await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
443
+ res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
444
+ if (error) {
445
+ this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
446
+ res.status(500).send('Error downloading the matterbridge storage file');
447
+ }
448
+ });
449
+ });
450
+ // Endpoint to download the matterbridge plugin directory
451
+ this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
452
+ this.log.debug('The frontend sent /api/download-pluginstorage');
453
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
454
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
455
+ if (error) {
456
+ this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
457
+ res.status(500).send('Error downloading the matterbridge plugin storage file');
458
+ }
459
+ });
460
+ });
461
+ // Endpoint to download the matterbridge plugin config files
462
+ this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
463
+ this.log.debug('The frontend sent /api/download-pluginconfig');
464
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
465
+ // await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
466
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
467
+ if (error) {
468
+ this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
469
+ res.status(500).send('Error downloading the matterbridge plugin storage file');
470
+ }
471
+ });
472
+ });
473
+ // Endpoint to download the matterbridge plugin config files
474
+ this.expressApp.get('/api/download-backup', async (req, res) => {
475
+ this.log.debug('The frontend sent /api/download-backup');
476
+ res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
477
+ if (error) {
478
+ this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
479
+ res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
480
+ }
481
+ });
482
+ });
483
+ // Endpoint to receive commands
484
+ this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
485
+ const command = req.params.command;
486
+ let param = req.params.param;
487
+ this.log.debug(`The frontend sent /api/command/${command}/${param}`);
488
+ if (!command) {
489
+ res.status(400).json({ error: 'No command provided' });
490
+ return;
491
+ }
492
+ this.log.debug(`Received frontend command: ${command}:${param}`);
493
+ // Handle the command setpassword from Settings
494
+ if (command === 'setpassword') {
495
+ const password = param.slice(1, -1); // Remove the first and last characters
496
+ this.log.debug('setpassword', param, password);
497
+ await this.matterbridge.nodeContext?.set('password', password);
498
+ res.json({ message: 'Command received' });
499
+ return;
500
+ }
501
+ // Handle the command setbridgemode from Settings
502
+ if (command === 'setbridgemode') {
503
+ this.log.debug(`setbridgemode: ${param}`);
504
+ this.wssSendRestartRequired();
505
+ await this.matterbridge.nodeContext?.set('bridgeMode', param);
506
+ res.json({ message: 'Command received' });
507
+ return;
508
+ }
509
+ // Handle the command backup from Settings
510
+ if (command === 'backup') {
511
+ this.log.notice(`Prepairing the backup...`);
512
+ await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
513
+ this.log.notice(`Backup ready to be downloaded.`);
514
+ res.json({ message: 'Command received' });
515
+ return;
516
+ }
517
+ // Handle the command setmbloglevel from Settings
518
+ if (command === 'setmbloglevel') {
519
+ this.log.debug('Matterbridge log level:', param);
520
+ if (param === 'Debug') {
521
+ this.log.logLevel = LogLevel.DEBUG;
522
+ }
523
+ else if (param === 'Info') {
524
+ this.log.logLevel = LogLevel.INFO;
525
+ }
526
+ else if (param === 'Notice') {
527
+ this.log.logLevel = LogLevel.NOTICE;
528
+ }
529
+ else if (param === 'Warn') {
530
+ this.log.logLevel = LogLevel.WARN;
531
+ }
532
+ else if (param === 'Error') {
533
+ this.log.logLevel = LogLevel.ERROR;
534
+ }
535
+ else if (param === 'Fatal') {
536
+ this.log.logLevel = LogLevel.FATAL;
537
+ }
538
+ await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
539
+ MatterbridgeEndpoint.logLevel = this.log.logLevel;
540
+ this.matterbridge.plugins.logLevel = this.log.logLevel;
541
+ for (const plugin of this.matterbridge.plugins) {
542
+ if (!plugin.platform || !plugin.platform.config)
543
+ continue;
544
+ plugin.platform.log.logLevel = plugin.platform.config.debug ? LogLevel.DEBUG : this.log.logLevel;
545
+ await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? LogLevel.DEBUG : this.log.logLevel);
546
+ }
547
+ res.json({ message: 'Command received' });
548
+ return;
549
+ }
550
+ // Handle the command setmbloglevel from Settings
551
+ if (command === 'setmjloglevel') {
552
+ this.log.debug('Matter.js log level:', param);
553
+ if (param === 'Debug') {
554
+ Logger.defaultLogLevel = MatterLogLevel.DEBUG;
555
+ }
556
+ else if (param === 'Info') {
557
+ Logger.defaultLogLevel = MatterLogLevel.INFO;
558
+ }
559
+ else if (param === 'Notice') {
560
+ Logger.defaultLogLevel = MatterLogLevel.NOTICE;
561
+ }
562
+ else if (param === 'Warn') {
563
+ Logger.defaultLogLevel = MatterLogLevel.WARN;
564
+ }
565
+ else if (param === 'Error') {
566
+ Logger.defaultLogLevel = MatterLogLevel.ERROR;
567
+ }
568
+ else if (param === 'Fatal') {
569
+ Logger.defaultLogLevel = MatterLogLevel.FATAL;
570
+ }
571
+ await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel);
572
+ res.json({ message: 'Command received' });
573
+ return;
574
+ }
575
+ // Handle the command setmdnsinterface from Settings
576
+ if (command === 'setmdnsinterface') {
577
+ param = param.slice(1, -1); // Remove the first and last characters *mdns*
578
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = param;
579
+ this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
580
+ await this.matterbridge.nodeContext?.set('mattermdnsinterface', param);
581
+ res.json({ message: 'Command received' });
582
+ return;
583
+ }
584
+ // Handle the command setipv4address from Settings
585
+ if (command === 'setipv4address') {
586
+ param = param.slice(1, -1); // Remove the first and last characters *ip*
587
+ this.matterbridge.matterbridgeInformation.matteripv4address = param;
588
+ this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
589
+ await this.matterbridge.nodeContext?.set('matteripv4address', param);
590
+ res.json({ message: 'Command received' });
591
+ return;
592
+ }
593
+ // Handle the command setipv6address from Settings
594
+ if (command === 'setipv6address') {
595
+ param = param.slice(1, -1); // Remove the first and last characters *ip*
596
+ this.matterbridge.matterbridgeInformation.matteripv6address = param;
597
+ this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
598
+ await this.matterbridge.nodeContext?.set('matteripv6address', param);
599
+ res.json({ message: 'Command received' });
600
+ return;
601
+ }
602
+ // Handle the command setmatterport from Settings
603
+ if (command === 'setmatterport') {
604
+ const port = Math.min(Math.max(parseInt(param), 5540), 5560);
605
+ this.matterbridge.matterbridgeInformation.matterPort = port;
606
+ this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
607
+ await this.matterbridge.nodeContext?.set('matterport', port);
608
+ res.json({ message: 'Command received' });
609
+ return;
610
+ }
611
+ // Handle the command setmatterdiscriminator from Settings
612
+ if (command === 'setmatterdiscriminator') {
613
+ const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
614
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
615
+ this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
616
+ await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator);
617
+ res.json({ message: 'Command received' });
618
+ return;
619
+ }
620
+ // Handle the command setmatterpasscode from Settings
621
+ if (command === 'setmatterpasscode') {
622
+ const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
623
+ this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
624
+ this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
625
+ await this.matterbridge.nodeContext?.set('matterpasscode', passcode);
626
+ res.json({ message: 'Command received' });
627
+ return;
628
+ }
629
+ // Handle the command setmbloglevel from Settings
630
+ if (command === 'setmblogfile') {
631
+ this.log.debug('Matterbridge file log:', param);
632
+ this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
633
+ await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
634
+ // Create the file logger for matterbridge
635
+ if (param === 'true')
636
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), LogLevel.DEBUG, true);
637
+ else
638
+ AnsiLogger.setGlobalLogfile(undefined);
639
+ res.json({ message: 'Command received' });
640
+ return;
641
+ }
642
+ // Handle the command setmbloglevel from Settings
643
+ if (command === 'setmjlogfile') {
644
+ this.log.debug('Matter file log:', param);
645
+ this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
646
+ await this.matterbridge.nodeContext?.set('matterFileLog', param === 'true');
647
+ if (param === 'true') {
648
+ try {
649
+ Logger.addLogger('matterfilelogger', await this.matterbridge.createMatterFileLogger(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), true), {
650
+ defaultLogLevel: MatterLogLevel.DEBUG,
651
+ logFormat: MatterLogFormat.PLAIN,
652
+ });
653
+ }
654
+ catch (error) {
655
+ this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
656
+ }
657
+ }
658
+ else {
659
+ try {
660
+ Logger.removeLogger('matterfilelogger');
661
+ }
662
+ catch (error) {
663
+ this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
664
+ }
665
+ }
666
+ res.json({ message: 'Command received' });
667
+ return;
668
+ }
669
+ // Handle the command unregister from Settings
670
+ if (command === 'unregister') {
671
+ await this.matterbridge.unregisterAndShutdownProcess();
672
+ res.json({ message: 'Command received' });
673
+ return;
674
+ }
675
+ // Handle the command reset from Settings
676
+ if (command === 'reset') {
677
+ await this.matterbridge.shutdownProcessAndReset();
678
+ res.json({ message: 'Command received' });
679
+ return;
680
+ }
681
+ // Handle the command factoryreset from Settings
682
+ if (command === 'factoryreset') {
683
+ await this.matterbridge.shutdownProcessAndFactoryReset();
684
+ res.json({ message: 'Command received' });
685
+ return;
686
+ }
687
+ // Handle the command shutdown from Header
688
+ if (command === 'shutdown') {
689
+ await this.matterbridge.shutdownProcess();
690
+ res.json({ message: 'Command received' });
691
+ return;
692
+ }
693
+ // Handle the command restart from Header
694
+ if (command === 'restart') {
695
+ await this.matterbridge.restartProcess();
696
+ res.json({ message: 'Command received' });
697
+ return;
698
+ }
699
+ // Handle the command update from Header
700
+ if (command === 'update') {
701
+ this.log.info('Updating matterbridge...');
702
+ try {
703
+ await this.matterbridge.spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
704
+ this.log.info('Matterbridge has been updated. Full restart required.');
705
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
706
+ }
707
+ catch (error) {
708
+ this.log.error('Error updating matterbridge');
709
+ }
710
+ await this.matterbridge.updateProcess();
711
+ this.wssSendRestartRequired();
712
+ res.json({ message: 'Command received' });
713
+ return;
714
+ }
715
+ // Handle the command saveconfig from Home
716
+ if (command === 'saveconfig') {
717
+ param = param.replace(/\*/g, '\\');
718
+ this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
719
+ // console.log('Req.body:', JSON.stringify(req.body, null, 2));
720
+ if (!this.matterbridge.plugins.has(param)) {
721
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
722
+ }
723
+ else {
724
+ const plugin = this.matterbridge.plugins.get(param);
725
+ if (!plugin)
726
+ return;
727
+ this.matterbridge.plugins.saveConfigFromJson(plugin, req.body);
728
+ }
729
+ this.wssSendRestartRequired();
730
+ res.json({ message: 'Command received' });
731
+ return;
732
+ }
733
+ // Handle the command installplugin from Home
734
+ if (command === 'installplugin') {
735
+ param = param.replace(/\*/g, '\\');
736
+ this.log.info(`Installing plugin ${plg}${param}${nf}...`);
737
+ try {
738
+ await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
739
+ this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
740
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
741
+ }
742
+ catch (error) {
743
+ this.log.error(`Error installing plugin ${plg}${param}${er}`);
744
+ }
745
+ this.wssSendRestartRequired();
746
+ param = param.split('@')[0];
747
+ // Also add the plugin to matterbridge so no return!
748
+ if (param === 'matterbridge') {
749
+ // If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
750
+ res.json({ message: 'Command received' });
751
+ return;
752
+ }
753
+ }
754
+ // Handle the command addplugin from Home
755
+ if (command === 'addplugin' || command === 'installplugin') {
756
+ param = param.replace(/\*/g, '\\');
757
+ const plugin = await this.matterbridge.plugins.add(param);
758
+ if (plugin) {
759
+ if (this.matterbridge.bridgeMode === 'childbridge') {
760
+ // We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node
761
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
762
+ this.matterbridge.createDynamicPlugin(plugin, true);
763
+ }
764
+ this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
765
+ }
766
+ res.json({ message: 'Command received' });
767
+ this.wssSendRefreshRequired();
768
+ return;
769
+ }
770
+ // Handle the command removeplugin from Home
771
+ if (command === 'removeplugin') {
772
+ if (!this.matterbridge.plugins.has(param)) {
773
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
774
+ }
775
+ else {
776
+ const plugin = this.matterbridge.plugins.get(param);
777
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode
778
+ await this.matterbridge.plugins.remove(param);
779
+ }
780
+ res.json({ message: 'Command received' });
781
+ this.wssSendRefreshRequired();
782
+ return;
783
+ }
784
+ // Handle the command enableplugin from Home
785
+ if (command === 'enableplugin') {
786
+ if (!this.matterbridge.plugins.has(param)) {
787
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
788
+ }
789
+ else {
790
+ const plugin = this.matterbridge.plugins.get(param);
791
+ if (plugin && !plugin.enabled) {
792
+ plugin.locked = undefined;
793
+ plugin.error = undefined;
794
+ plugin.loaded = undefined;
795
+ plugin.started = undefined;
796
+ plugin.configured = undefined;
797
+ plugin.connected = undefined;
798
+ plugin.platform = undefined;
799
+ plugin.registeredDevices = undefined;
800
+ plugin.addedDevices = undefined;
801
+ await this.matterbridge.plugins.enable(param);
802
+ if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
803
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
804
+ this.matterbridge.createDynamicPlugin(plugin, true);
805
+ }
806
+ this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background since the server node is already started
807
+ }
808
+ }
809
+ res.json({ message: 'Command received' });
810
+ this.wssSendRefreshRequired();
811
+ return;
812
+ }
813
+ // Handle the command disableplugin from Home
814
+ if (command === 'disableplugin') {
815
+ if (!this.matterbridge.plugins.has(param)) {
816
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
817
+ }
818
+ else {
819
+ const plugin = this.matterbridge.plugins.get(param);
820
+ if (plugin && plugin.enabled) {
821
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true); // This will also close the server node in childbridge mode
822
+ await this.matterbridge.plugins.disable(param);
823
+ }
824
+ }
825
+ res.json({ message: 'Command received' });
826
+ this.wssSendRefreshRequired();
827
+ return;
828
+ }
829
+ });
830
+ // Fallback for routing (must be the last route)
831
+ this.expressApp.get('*', (req, res) => {
832
+ this.log.debug('The frontend sent:', req.url);
833
+ this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
834
+ res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
835
+ });
836
+ this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
837
+ }
838
+ async stop() {
839
+ // Close the http server
840
+ if (this.httpServer) {
841
+ this.httpServer.close();
842
+ this.httpServer.removeAllListeners();
843
+ this.httpServer = undefined;
844
+ this.log.debug('Frontend http server closed successfully');
845
+ }
846
+ // Close the https server
847
+ if (this.httpsServer) {
848
+ this.httpsServer.close();
849
+ this.httpsServer.removeAllListeners();
850
+ this.httpsServer = undefined;
851
+ this.log.debug('Frontend https server closed successfully');
852
+ }
853
+ // Remove listeners from the express app
854
+ if (this.expressApp) {
855
+ this.expressApp.removeAllListeners();
856
+ this.expressApp = undefined;
857
+ this.log.debug('Frontend app closed successfully');
858
+ }
859
+ // Close the WebSocket server
860
+ if (this.webSocketServer) {
861
+ // Close all active connections
862
+ this.webSocketServer.clients.forEach((client) => {
863
+ if (client.readyState === WebSocket.OPEN) {
864
+ client.close();
865
+ }
866
+ });
867
+ this.webSocketServer.close((error) => {
868
+ if (error) {
869
+ this.log.error(`Error closing WebSocket server: ${error}`);
870
+ }
871
+ else {
872
+ this.log.debug('WebSocket server closed successfully');
873
+ }
874
+ });
875
+ this.webSocketServer = undefined;
876
+ }
877
+ }
878
+ /**
879
+ * Retrieves the cluster text description from a given device.
880
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
881
+ * @returns {string} The attributes description of the cluster servers in the device.
882
+ */
883
+ getClusterTextFromDevice(device) {
884
+ const getAttribute = (device, cluster, attribute) => {
885
+ let value = undefined;
886
+ Object.entries(device.state)
887
+ .filter(([clusterName]) => clusterName.toLowerCase() === cluster.toLowerCase())
888
+ .forEach(([, clusterAttributes]) => {
889
+ Object.entries(clusterAttributes)
890
+ .filter(([attributeName]) => attributeName.toLowerCase() === attribute.toLowerCase())
891
+ .forEach(([, attributeValue]) => {
892
+ value = attributeValue;
893
+ });
894
+ });
895
+ if (value === undefined)
896
+ this.log.error(`Cluster ${cluster} or attribute ${attribute} not found in device ${device.deviceName}`);
897
+ return value;
898
+ };
899
+ const getUserLabel = (device) => {
900
+ const labelList = getAttribute(device, 'userLabel', 'labelList');
901
+ if (!labelList)
902
+ return;
903
+ const composed = labelList.find((entry) => entry.label === 'composed');
904
+ if (composed)
905
+ return 'Composed: ' + composed.value;
906
+ else
907
+ return '';
908
+ };
909
+ const getFixedLabel = (device) => {
910
+ const labelList = getAttribute(device, 'fixedLabel', 'labelList');
911
+ if (!labelList)
912
+ return;
913
+ const composed = labelList.find((entry) => entry.label === 'composed');
914
+ if (composed)
915
+ return 'Composed: ' + composed.value;
916
+ else
917
+ return '';
918
+ };
919
+ let attributes = '';
920
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
921
+ Object.entries(device.state).forEach(([clusterName, clusterAttributes]) => {
922
+ Object.entries(clusterAttributes).forEach(([attributeName, attributeValue]) => {
923
+ // console.log(`Cluster: ${clusterName} Attribute: ${attributeName} Value: ${attributeValue}`);
924
+ if (clusterName === 'onOff' && attributeName === 'onOff')
925
+ attributes += `OnOff: ${attributeValue} `;
926
+ if (clusterName === 'switch' && attributeName === 'currentPosition')
927
+ attributes += `Position: ${attributeValue} `;
928
+ if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths')
929
+ attributes += `Cover position: ${attributeValue / 100}% `;
930
+ if (clusterName === 'doorLock' && attributeName === 'lockState')
931
+ attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
932
+ if (clusterName === 'thermostat' && attributeName === 'localTemperature')
933
+ attributes += `Temperature: ${attributeValue / 100}°C `;
934
+ if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint')
935
+ attributes += `Heat to: ${attributeValue / 100}°C `;
936
+ if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint')
937
+ attributes += `Cool to: ${attributeValue / 100}°C `;
938
+ if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
939
+ attributes += `Mode: ${attributeValue} `;
940
+ if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
941
+ attributes += `State: ${attributeValue} `;
942
+ if (clusterName === 'levelControl' && attributeName === 'currentLevel')
943
+ attributes += `Level: ${attributeValue}% `;
944
+ if (clusterName === 'colorControl' && attributeName === 'colorMode')
945
+ attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
946
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue')
947
+ attributes += `Hue: ${Math.round(attributeValue)} `;
948
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation')
949
+ attributes += `Saturation: ${Math.round(attributeValue)} `;
950
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX')
951
+ attributes += `X: ${Math.round(attributeValue)} `;
952
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY')
953
+ attributes += `Y: ${Math.round(attributeValue)} `;
954
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds')
955
+ attributes += `ColorTemp: ${Math.round(attributeValue)} `;
956
+ if (clusterName === 'booleanState' && attributeName === 'stateValue')
957
+ attributes += `Contact: ${attributeValue} `;
958
+ if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive')
959
+ attributes += `Active alarms: ${stringify(attributeValue)} `;
960
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
961
+ attributes += `Smoke: ${attributeValue} `;
962
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
963
+ attributes += `Co: ${attributeValue} `;
964
+ if (clusterName === 'fanControl' && attributeName === 'fanMode')
965
+ attributes += `Mode: ${attributeValue} `;
966
+ if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
967
+ attributes += `Percent: ${attributeValue} `;
968
+ if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
969
+ attributes += `Speed: ${attributeValue} `;
970
+ if (clusterName === 'occupancySensing' && attributeName === 'occupancy')
971
+ attributes += `Occupancy: ${attributeValue.occupied} `;
972
+ if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue')
973
+ attributes += `Illuminance: ${attributeValue} `;
974
+ if (clusterName === 'airQuality' && attributeName === 'airQuality')
975
+ attributes += `Air quality: ${attributeValue} `;
976
+ if (clusterName === 'tvocMeasurement' && attributeName === 'measuredValue')
977
+ attributes += `Voc: ${attributeValue} `;
978
+ if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue')
979
+ attributes += `Temperature: ${attributeValue / 100}°C `;
980
+ if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue')
981
+ attributes += `Humidity: ${attributeValue / 100}% `;
982
+ if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
983
+ attributes += `Pressure: ${attributeValue} `;
984
+ if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
985
+ attributes += `Flow: ${attributeValue} `;
986
+ if (clusterName === 'fixedLabel' && attributeName === 'labelList')
987
+ attributes += `${getFixedLabel(device)} `;
988
+ if (clusterName === 'userLabel' && attributeName === 'labelList')
989
+ attributes += `${getUserLabel(device)} `;
990
+ });
991
+ });
992
+ return attributes.trimStart().trimEnd();
993
+ }
994
+ /**
995
+ * Retrieves the base registered plugins sanitized for res.json().
996
+ * @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
997
+ */
998
+ async getBaseRegisteredPlugins() {
999
+ const baseRegisteredPlugins = [];
1000
+ for (const plugin of this.matterbridge.plugins) {
1001
+ baseRegisteredPlugins.push({
1002
+ path: plugin.path,
1003
+ type: plugin.type,
1004
+ name: plugin.name,
1005
+ version: plugin.version,
1006
+ description: plugin.description,
1007
+ author: plugin.author,
1008
+ latestVersion: plugin.latestVersion,
1009
+ locked: plugin.locked,
1010
+ error: plugin.error,
1011
+ enabled: plugin.enabled,
1012
+ loaded: plugin.loaded,
1013
+ started: plugin.started,
1014
+ configured: plugin.configured,
1015
+ paired: plugin.paired,
1016
+ connected: plugin.connected,
1017
+ fabricInformations: plugin.fabricInformations,
1018
+ sessionInformations: plugin.sessionInformations,
1019
+ registeredDevices: plugin.registeredDevices,
1020
+ addedDevices: plugin.addedDevices,
1021
+ qrPairingCode: plugin.qrPairingCode,
1022
+ manualPairingCode: plugin.manualPairingCode,
1023
+ configJson: plugin.configJson,
1024
+ schemaJson: plugin.schemaJson,
1025
+ });
1026
+ }
1027
+ return baseRegisteredPlugins;
1028
+ }
1029
+ /**
1030
+ * Handles incoming websocket messages for the Matterbridge.
1031
+ *
1032
+ * @param {Matterbridge} this - The Matterbridge instance.
1033
+ * @param {WebSocket} client - The websocket client that sent the message.
1034
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1035
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1036
+ */
1037
+ async wsMessageHandler(client, message) {
1038
+ let data;
1039
+ try {
1040
+ data = JSON.parse(message.toString());
1041
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || !isValidObject(data.params) || data.dst !== 'Matterbridge') {
1042
+ this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1043
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' }));
1044
+ return;
1045
+ }
1046
+ this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
1047
+ if (data.method === 'ping') {
1048
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: 'pong' }));
1049
+ return;
1050
+ }
1051
+ else if (data.method === '/api/login') {
1052
+ if (!this.matterbridge.nodeContext) {
1053
+ this.log.error('Login nodeContext not found');
1054
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: nodeContext not found' }));
1055
+ return;
1056
+ }
1057
+ const storedPassword = await this.matterbridge.nodeContext.get('password', '');
1058
+ if (storedPassword === '' || storedPassword === data.params.password) {
1059
+ this.log.debug('Login password valid');
1060
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: { valid: true } }));
1061
+ return;
1062
+ }
1063
+ else {
1064
+ this.log.debug('Error wrong password');
1065
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' }));
1066
+ return;
1067
+ }
1068
+ }
1069
+ else if (data.method === '/api/install') {
1070
+ if (!isValidString(data.params.packageName, 10)) {
1071
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/install' }));
1072
+ return;
1073
+ }
1074
+ this.matterbridge
1075
+ .spawnCommand('npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
1076
+ .then((response) => {
1077
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
1078
+ })
1079
+ .catch((error) => {
1080
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
1081
+ });
1082
+ return;
1083
+ }
1084
+ else if (data.method === '/api/uninstall') {
1085
+ if (!isValidString(data.params.packageName, 10)) {
1086
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1087
+ return;
1088
+ }
1089
+ this.matterbridge
1090
+ .spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
1091
+ .then((response) => {
1092
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
1093
+ })
1094
+ .catch((error) => {
1095
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
1096
+ });
1097
+ return;
1098
+ }
1099
+ else if (data.method === '/api/restart') {
1100
+ await this.matterbridge.restartProcess();
1101
+ return;
1102
+ }
1103
+ else if (data.method === '/api/shutdown') {
1104
+ await this.matterbridge.shutdownProcess();
1105
+ return;
1106
+ }
1107
+ else if (data.method === '/api/settings') {
1108
+ this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
1109
+ this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
1110
+ this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
1111
+ this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
1112
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
1113
+ this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
1114
+ this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
1115
+ this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
1116
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
1117
+ this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
1118
+ this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
1119
+ this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
1120
+ this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
1121
+ this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
1122
+ this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
1123
+ this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
1124
+ this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
1125
+ const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
1126
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
1127
+ return;
1128
+ }
1129
+ else if (data.method === '/api/plugins') {
1130
+ const response = await this.getBaseRegisteredPlugins();
1131
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
1132
+ return;
1133
+ }
1134
+ else if (data.method === '/api/devices') {
1135
+ const devices = [];
1136
+ this.matterbridge.devices.forEach(async (device) => {
1137
+ // Filter by pluginName if provided
1138
+ if (data.params.pluginName && data.params.pluginName !== device.plugin)
1139
+ return;
1140
+ // Check if the device has the required properties
1141
+ if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
1142
+ return;
1143
+ const cluster = this.getClusterTextFromDevice(device);
1144
+ devices.push({
1145
+ pluginName: device.plugin,
1146
+ type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
1147
+ endpoint: device.number,
1148
+ name: device.deviceName,
1149
+ serial: device.serialNumber,
1150
+ productUrl: device.productUrl,
1151
+ configUrl: device.configUrl,
1152
+ uniqueId: device.uniqueId,
1153
+ cluster: cluster,
1154
+ });
1155
+ });
1156
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: devices }));
1157
+ return;
1158
+ }
1159
+ else if (data.method === '/api/clusters') {
1160
+ if (!isValidString(data.params.plugin, 10)) {
1161
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' }));
1162
+ return;
1163
+ }
1164
+ if (!isValidNumber(data.params.endpoint, 1)) {
1165
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' }));
1166
+ return;
1167
+ }
1168
+ const clusters = [];
1169
+ let deviceName = '';
1170
+ let serialNumber = '';
1171
+ let deviceTypes = [];
1172
+ this.matterbridge.devices.forEach(async (device) => {
1173
+ if (data.params.plugin !== device.plugin)
1174
+ return;
1175
+ if (data.params.endpoint !== device.number)
1176
+ return;
1177
+ deviceName = device.deviceName ?? 'Unknown';
1178
+ serialNumber = device.serialNumber ?? 'Unknown';
1179
+ deviceTypes = [];
1180
+ const endpointServer = EndpointServer.forEndpoint(device);
1181
+ const clusterServers = endpointServer.getAllClusterServers();
1182
+ clusterServers.forEach((clusterServer) => {
1183
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
1184
+ if (clusterServer.name === 'EveHistory')
1185
+ return;
1186
+ if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
1187
+ value.getLocal().forEach((deviceType) => {
1188
+ deviceTypes.push(deviceType.deviceType);
1189
+ });
1190
+ }
1191
+ let attributeValue;
1192
+ let attributeLocalValue;
1193
+ try {
1194
+ if (typeof value.getLocal() === 'object')
1195
+ attributeValue = stringify(value.getLocal());
1196
+ else
1197
+ attributeValue = value.getLocal().toString();
1198
+ attributeLocalValue = value.getLocal();
1199
+ }
1200
+ catch (error) {
1201
+ attributeValue = 'Fabric-Scoped';
1202
+ attributeLocalValue = 'Fabric-Scoped';
1203
+ this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
1204
+ }
1205
+ clusters.push({
1206
+ endpoint: device.number ? device.number.toString() : '...',
1207
+ id: 'main',
1208
+ deviceTypes,
1209
+ clusterName: clusterServer.name,
1210
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
1211
+ attributeName: key,
1212
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
1213
+ attributeValue,
1214
+ attributeLocalValue,
1215
+ });
1216
+ });
1217
+ });
1218
+ endpointServer.getChildEndpoints().forEach((childEndpoint) => {
1219
+ deviceTypes = [];
1220
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1221
+ const name = childEndpoint.endpoint?.id;
1222
+ const clusterServers = childEndpoint.getAllClusterServers();
1223
+ clusterServers.forEach((clusterServer) => {
1224
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
1225
+ if (clusterServer.name === 'EveHistory')
1226
+ return;
1227
+ if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
1228
+ value.getLocal().forEach((deviceType) => {
1229
+ deviceTypes.push(deviceType.deviceType);
1230
+ });
1231
+ }
1232
+ let attributeValue;
1233
+ let attributeLocalValue;
1234
+ try {
1235
+ if (typeof value.getLocal() === 'object')
1236
+ attributeValue = stringify(value.getLocal());
1237
+ else
1238
+ attributeValue = value.getLocal().toString();
1239
+ attributeLocalValue = value.getLocal();
1240
+ }
1241
+ catch (error) {
1242
+ attributeValue = 'Fabric-Scoped';
1243
+ attributeLocalValue = 'Fabric-Scoped';
1244
+ this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
1245
+ }
1246
+ clusters.push({
1247
+ endpoint: childEndpoint.number ? childEndpoint.number.toString() : '...',
1248
+ id: name,
1249
+ deviceTypes,
1250
+ clusterName: clusterServer.name,
1251
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
1252
+ attributeName: key,
1253
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
1254
+ attributeValue,
1255
+ attributeLocalValue,
1256
+ });
1257
+ });
1258
+ });
1259
+ });
1260
+ });
1261
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, deviceName, serialNumber, endpoint: data.params.endpoint, deviceTypes, response: clusters }));
1262
+ return;
1263
+ }
1264
+ else if (data.method === '/api/select') {
1265
+ if (!isValidString(data.params.plugin, 10)) {
1266
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select' }));
1267
+ return;
1268
+ }
1269
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1270
+ if (!plugin) {
1271
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select' }));
1272
+ return;
1273
+ }
1274
+ const selectDeviceValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectDevice.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1275
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectDeviceValues }));
1276
+ return;
1277
+ }
1278
+ else if (data.method === '/api/select/entities') {
1279
+ if (!isValidString(data.params.plugin, 10)) {
1280
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/entities' }));
1281
+ return;
1282
+ }
1283
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1284
+ if (!plugin) {
1285
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' }));
1286
+ return;
1287
+ }
1288
+ const selectEntityValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectEntity.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1289
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectEntityValues }));
1290
+ return;
1291
+ }
1292
+ else {
1293
+ this.log.error(`Invalid method from websocket client: ${debugStringify(data)}`);
1294
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid method' }));
1295
+ return;
1296
+ }
1297
+ }
1298
+ catch (error) {
1299
+ this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1300
+ return;
1301
+ }
1302
+ }
1303
+ /**
1304
+ * Sends a WebSocket message to all connected clients.
1305
+ *
1306
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1307
+ * @param {string} time - The time string of the message
1308
+ * @param {string} name - The logger name of the message
1309
+ * @param {string} message - The content of the message.
1310
+ */
1311
+ wssSendMessage(level, time, name, message) {
1312
+ if (!level || !time || !name || !message)
1313
+ return;
1314
+ // Remove ANSI escape codes from the message
1315
+ // eslint-disable-next-line no-control-regex
1316
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1317
+ // Remove leading asterisks from the message
1318
+ message = message.replace(/^\*+/, '');
1319
+ // Replace all occurrences of \t and \n
1320
+ message = message.replace(/[\t\n]/g, '');
1321
+ // Remove non-printable characters
1322
+ // eslint-disable-next-line no-control-regex
1323
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
1324
+ // Replace all occurrences of \" with "
1325
+ message = message.replace(/\\"/g, '"');
1326
+ // Define the maximum allowed length for continuous characters without a space
1327
+ const maxContinuousLength = 100;
1328
+ const keepStartLength = 20;
1329
+ const keepEndLength = 20;
1330
+ // Split the message into words
1331
+ message = message
1332
+ .split(' ')
1333
+ .map((word) => {
1334
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1335
+ if (word.length > maxContinuousLength) {
1336
+ return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1337
+ }
1338
+ return word;
1339
+ })
1340
+ .join(' ');
1341
+ // Send the message to all connected clients
1342
+ this.webSocketServer?.clients.forEach((client) => {
1343
+ if (client.readyState === WebSocket.OPEN) {
1344
+ client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1345
+ }
1346
+ });
1347
+ }
1348
+ /**
1349
+ * Sends a need to refresh WebSocket message to all connected clients.
1350
+ *
1351
+ */
1352
+ wssSendRefreshRequired() {
1353
+ this.log.debug('Sending a refresh required message to all connected clients');
1354
+ this.matterbridge.matterbridgeInformation.refreshRequired = true;
1355
+ // Send the message to all connected clients
1356
+ this.webSocketServer?.clients.forEach((client) => {
1357
+ if (client.readyState === WebSocket.OPEN) {
1358
+ client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
1359
+ }
1360
+ });
1361
+ }
1362
+ /**
1363
+ * Sends a need to restart WebSocket message to all connected clients.
1364
+ *
1365
+ */
1366
+ wssSendRestartRequired() {
1367
+ this.log.debug('Sending a restart required message to all connected clients');
1368
+ this.matterbridge.matterbridgeInformation.restartRequired = true;
1369
+ // Send the message to all connected clients
1370
+ this.webSocketServer?.clients.forEach((client) => {
1371
+ if (client.readyState === WebSocket.OPEN) {
1372
+ client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1373
+ }
1374
+ });
1375
+ }
1376
+ }
1377
+ //# sourceMappingURL=frontend.js.map