matterbridge 3.0.4-dev-20250525-b1cbfb7 → 3.0.4

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 (163) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/cli.d.ts +29 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +37 -2
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cluster/export.d.ts +2 -0
  7. package/dist/cluster/export.d.ts.map +1 -0
  8. package/dist/cluster/export.js +2 -0
  9. package/dist/cluster/export.js.map +1 -0
  10. package/dist/defaultConfigSchema.d.ts +27 -0
  11. package/dist/defaultConfigSchema.d.ts.map +1 -0
  12. package/dist/defaultConfigSchema.js +23 -0
  13. package/dist/defaultConfigSchema.js.map +1 -0
  14. package/dist/deviceManager.d.ts +114 -0
  15. package/dist/deviceManager.d.ts.map +1 -0
  16. package/dist/deviceManager.js +94 -1
  17. package/dist/deviceManager.js.map +1 -0
  18. package/dist/frontend.d.ts +256 -0
  19. package/dist/frontend.d.ts.map +1 -0
  20. package/dist/frontend.js +363 -16
  21. package/dist/frontend.js.map +1 -0
  22. package/dist/helpers.d.ts +47 -0
  23. package/dist/helpers.d.ts.map +1 -0
  24. package/dist/helpers.js +51 -0
  25. package/dist/helpers.js.map +1 -0
  26. package/dist/index.d.ts +35 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +27 -1
  29. package/dist/index.js.map +1 -0
  30. package/dist/logger/export.d.ts +2 -0
  31. package/dist/logger/export.d.ts.map +1 -0
  32. package/dist/logger/export.js +1 -0
  33. package/dist/logger/export.js.map +1 -0
  34. package/dist/matter/behaviors.d.ts +2 -0
  35. package/dist/matter/behaviors.d.ts.map +1 -0
  36. package/dist/matter/behaviors.js +2 -0
  37. package/dist/matter/behaviors.js.map +1 -0
  38. package/dist/matter/clusters.d.ts +2 -0
  39. package/dist/matter/clusters.d.ts.map +1 -0
  40. package/dist/matter/clusters.js +2 -0
  41. package/dist/matter/clusters.js.map +1 -0
  42. package/dist/matter/devices.d.ts +2 -0
  43. package/dist/matter/devices.d.ts.map +1 -0
  44. package/dist/matter/devices.js +2 -0
  45. package/dist/matter/devices.js.map +1 -0
  46. package/dist/matter/endpoints.d.ts +2 -0
  47. package/dist/matter/endpoints.d.ts.map +1 -0
  48. package/dist/matter/endpoints.js +2 -0
  49. package/dist/matter/endpoints.js.map +1 -0
  50. package/dist/matter/export.d.ts +5 -0
  51. package/dist/matter/export.d.ts.map +1 -0
  52. package/dist/matter/export.js +2 -0
  53. package/dist/matter/export.js.map +1 -0
  54. package/dist/matter/types.d.ts +3 -0
  55. package/dist/matter/types.d.ts.map +1 -0
  56. package/dist/matter/types.js +2 -0
  57. package/dist/matter/types.js.map +1 -0
  58. package/dist/matterbridge.d.ts +445 -0
  59. package/dist/matterbridge.d.ts.map +1 -0
  60. package/dist/matterbridge.js +747 -47
  61. package/dist/matterbridge.js.map +1 -0
  62. package/dist/matterbridgeAccessoryPlatform.d.ts +40 -0
  63. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  64. package/dist/matterbridgeAccessoryPlatform.js +34 -0
  65. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  66. package/dist/matterbridgeBehaviors.d.ts +1398 -0
  67. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  68. package/dist/matterbridgeBehaviors.js +61 -4
  69. package/dist/matterbridgeBehaviors.js.map +1 -0
  70. package/dist/matterbridgeDeviceTypes.d.ts +629 -0
  71. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  72. package/dist/matterbridgeDeviceTypes.js +563 -15
  73. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  74. package/dist/matterbridgeDynamicPlatform.d.ts +40 -0
  75. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  76. package/dist/matterbridgeDynamicPlatform.js +34 -0
  77. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  78. package/dist/matterbridgeEndpoint.d.ts +1053 -0
  79. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  80. package/dist/matterbridgeEndpoint.js +903 -23
  81. package/dist/matterbridgeEndpoint.js.map +1 -0
  82. package/dist/matterbridgeEndpointHelpers.d.ts +2749 -0
  83. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  84. package/dist/matterbridgeEndpointHelpers.js +172 -10
  85. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  86. package/dist/matterbridgePlatform.d.ts +294 -0
  87. package/dist/matterbridgePlatform.d.ts.map +1 -0
  88. package/dist/matterbridgePlatform.js +225 -7
  89. package/dist/matterbridgePlatform.js.map +1 -0
  90. package/dist/matterbridgeTypes.d.ts +196 -0
  91. package/dist/matterbridgeTypes.d.ts.map +1 -0
  92. package/dist/matterbridgeTypes.js +24 -0
  93. package/dist/matterbridgeTypes.js.map +1 -0
  94. package/dist/pluginManager.d.ts +273 -0
  95. package/dist/pluginManager.d.ts.map +1 -0
  96. package/dist/pluginManager.js +264 -3
  97. package/dist/pluginManager.js.map +1 -0
  98. package/dist/roboticVacuumCleaner.d.ts +82 -0
  99. package/dist/roboticVacuumCleaner.d.ts.map +1 -0
  100. package/dist/roboticVacuumCleaner.js +78 -3
  101. package/dist/roboticVacuumCleaner.js.map +1 -0
  102. package/dist/shelly.d.ts +153 -0
  103. package/dist/shelly.d.ts.map +1 -0
  104. package/dist/shelly.js +155 -7
  105. package/dist/shelly.js.map +1 -0
  106. package/dist/storage/export.d.ts +2 -0
  107. package/dist/storage/export.d.ts.map +1 -0
  108. package/dist/storage/export.js +1 -0
  109. package/dist/storage/export.js.map +1 -0
  110. package/dist/update.d.ts +58 -0
  111. package/dist/update.d.ts.map +1 -0
  112. package/dist/update.js +53 -0
  113. package/dist/update.js.map +1 -0
  114. package/dist/utils/colorUtils.d.ts +61 -0
  115. package/dist/utils/colorUtils.d.ts.map +1 -0
  116. package/dist/utils/colorUtils.js +205 -2
  117. package/dist/utils/colorUtils.js.map +1 -0
  118. package/dist/utils/commandLine.d.ts +58 -0
  119. package/dist/utils/commandLine.d.ts.map +1 -0
  120. package/dist/utils/commandLine.js +53 -0
  121. package/dist/utils/commandLine.js.map +1 -0
  122. package/dist/utils/copyDirectory.d.ts +32 -0
  123. package/dist/utils/copyDirectory.d.ts.map +1 -0
  124. package/dist/utils/copyDirectory.js +37 -1
  125. package/dist/utils/copyDirectory.js.map +1 -0
  126. package/dist/utils/createZip.d.ts +38 -0
  127. package/dist/utils/createZip.d.ts.map +1 -0
  128. package/dist/utils/createZip.js +42 -2
  129. package/dist/utils/createZip.js.map +1 -0
  130. package/dist/utils/deepCopy.d.ts +31 -0
  131. package/dist/utils/deepCopy.d.ts.map +1 -0
  132. package/dist/utils/deepCopy.js +38 -0
  133. package/dist/utils/deepCopy.js.map +1 -0
  134. package/dist/utils/deepEqual.d.ts +53 -0
  135. package/dist/utils/deepEqual.d.ts.map +1 -0
  136. package/dist/utils/deepEqual.js +71 -1
  137. package/dist/utils/deepEqual.js.map +1 -0
  138. package/dist/utils/export.d.ts +11 -0
  139. package/dist/utils/export.d.ts.map +1 -0
  140. package/dist/utils/export.js +1 -0
  141. package/dist/utils/export.js.map +1 -0
  142. package/dist/utils/hex.d.ts +48 -0
  143. package/dist/utils/hex.d.ts.map +1 -0
  144. package/dist/utils/hex.js +57 -0
  145. package/dist/utils/hex.js.map +1 -0
  146. package/dist/utils/isvalid.d.ts +102 -0
  147. package/dist/utils/isvalid.d.ts.map +1 -0
  148. package/dist/utils/isvalid.js +100 -0
  149. package/dist/utils/isvalid.js.map +1 -0
  150. package/dist/utils/network.d.ts +69 -0
  151. package/dist/utils/network.d.ts.map +1 -0
  152. package/dist/utils/network.js +76 -5
  153. package/dist/utils/network.js.map +1 -0
  154. package/dist/utils/wait.d.ts +52 -0
  155. package/dist/utils/wait.d.ts.map +1 -0
  156. package/dist/utils/wait.js +58 -9
  157. package/dist/utils/wait.js.map +1 -0
  158. package/dist/waterHeater.d.ts +75 -0
  159. package/dist/waterHeater.d.ts.map +1 -0
  160. package/dist/waterHeater.js +52 -0
  161. package/dist/waterHeater.js.map +1 -0
  162. package/npm-shrinkwrap.json +2 -2
  163. package/package.json +2 -1
package/dist/frontend.js CHANGED
@@ -1,29 +1,112 @@
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.2
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
1
24
  import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
2
25
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
26
+ // Node modules
3
27
  import { createServer } from 'node:http';
4
28
  import https from 'node:https';
5
29
  import os from 'node:os';
6
30
  import path from 'node:path';
7
31
  import { promises as fs } from 'node:fs';
32
+ // Third-party modules
8
33
  import express from 'express';
9
34
  import WebSocket, { WebSocketServer } from 'ws';
10
35
  import multer from 'multer';
36
+ // AnsiLogger module
11
37
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from './logger/export.js';
38
+ // Matterbridge
12
39
  import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout } from './utils/export.js';
13
40
  import { plg } from './matterbridgeTypes.js';
14
41
  import { hasParameter } from './utils/export.js';
15
42
  import { capitalizeFirstLetter } from './matterbridgeEndpointHelpers.js';
43
+ /**
44
+ * Websocket message ID for logging.
45
+ * @constant {number}
46
+ */
16
47
  export const WS_ID_LOG = 0;
48
+ /**
49
+ * Websocket message ID indicating a refresh is needed.
50
+ * @constant {number}
51
+ */
17
52
  export const WS_ID_REFRESH_NEEDED = 1;
53
+ /**
54
+ * Websocket message ID indicating a restart is needed.
55
+ * @constant {number}
56
+ */
18
57
  export const WS_ID_RESTART_NEEDED = 2;
58
+ /**
59
+ * Websocket message ID indicating a cpu update.
60
+ * @constant {number}
61
+ */
19
62
  export const WS_ID_CPU_UPDATE = 3;
63
+ /**
64
+ * Websocket message ID indicating a memory update.
65
+ * @constant {number}
66
+ */
20
67
  export const WS_ID_MEMORY_UPDATE = 4;
68
+ /**
69
+ * Websocket message ID indicating an uptime update.
70
+ * @constant {number}
71
+ */
21
72
  export const WS_ID_UPTIME_UPDATE = 5;
73
+ /**
74
+ * Websocket message ID indicating a snackbar message.
75
+ * @constant {number}
76
+ */
22
77
  export const WS_ID_SNACKBAR = 6;
78
+ /**
79
+ * Websocket message ID indicating matterbridge has un update available.
80
+ * @constant {number}
81
+ */
23
82
  export const WS_ID_UPDATE_NEEDED = 7;
83
+ /**
84
+ * Websocket message ID indicating a state update.
85
+ * @constant {number}
86
+ */
24
87
  export const WS_ID_STATEUPDATE = 8;
88
+ /**
89
+ * Websocket message ID indicating to close a permanent snackbar message.
90
+ * @constant {number}
91
+ */
25
92
  export const WS_ID_CLOSE_SNACKBAR = 9;
93
+ /**
94
+ * Websocket message ID indicating a shelly system update.
95
+ * check:
96
+ * curl -k http://127.0.0.1:8101/api/updates/sys/check
97
+ * perform:
98
+ * curl -k http://127.0.0.1:8101/api/updates/sys/perform
99
+ * @constant {number}
100
+ */
26
101
  export const WS_ID_SHELLY_SYS_UPDATE = 100;
102
+ /**
103
+ * Websocket message ID indicating a shelly main update.
104
+ * check:
105
+ * curl -k http://127.0.0.1:8101/api/updates/main/check
106
+ * perform:
107
+ * curl -k http://127.0.0.1:8101/api/updates/main/perform
108
+ * @constant {number}
109
+ */
27
110
  export const WS_ID_SHELLY_MAIN_UPDATE = 101;
28
111
  export class Frontend {
29
112
  matterbridge;
@@ -36,7 +119,7 @@ export class Frontend {
36
119
  webSocketServer;
37
120
  constructor(matterbridge) {
38
121
  this.matterbridge = matterbridge;
39
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
122
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
40
123
  }
41
124
  set logLevel(logLevel) {
42
125
  this.log.logLevel = logLevel;
@@ -44,13 +127,43 @@ export class Frontend {
44
127
  async start(port = 8283) {
45
128
  this.port = port;
46
129
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
130
+ // Initialize multer with the upload directory
47
131
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
48
132
  await fs.mkdir(uploadDir, { recursive: true });
49
133
  const upload = multer({ dest: uploadDir });
134
+ // Create the express app that serves the frontend
50
135
  this.expressApp = express();
136
+ // Inject logging/debug wrapper for route/middleware registration
137
+ /*
138
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
139
+ for (const method of methods) {
140
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
144
+ try {
145
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
146
+ return original(path, ...rest);
147
+ } catch (err) {
148
+ console.error(`[ERROR] Failed to register route: ${path}`);
149
+ throw err;
150
+ }
151
+ };
152
+ }
153
+ */
154
+ // Log all requests to the server for debugging
155
+ /*
156
+ this.expressApp.use((req, res, next) => {
157
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
158
+ next();
159
+ });
160
+ */
161
+ // Serve static files from '/static' endpoint
51
162
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
52
163
  if (!hasParameter('ssl')) {
164
+ // Create an HTTP server and attach the express app
53
165
  this.httpServer = createServer(this.expressApp);
166
+ // Listen on the specified port
54
167
  if (hasParameter('ingress')) {
55
168
  this.httpServer.listen(this.port, '0.0.0.0', () => {
56
169
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -64,6 +177,7 @@ export class Frontend {
64
177
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
65
178
  });
66
179
  }
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
181
  this.httpServer.on('error', (error) => {
68
182
  this.log.error(`Frontend http server error listening on ${this.port}`);
69
183
  switch (error.code) {
@@ -79,6 +193,7 @@ export class Frontend {
79
193
  });
80
194
  }
81
195
  else {
196
+ // Load the SSL certificate, the private key and optionally the CA certificate
82
197
  let cert;
83
198
  try {
84
199
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
@@ -106,7 +221,9 @@ export class Frontend {
106
221
  this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
107
222
  }
108
223
  const serverOptions = { cert, key, ca };
224
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
109
225
  this.httpsServer = https.createServer(serverOptions, this.expressApp);
226
+ // Listen on the specified port
110
227
  if (hasParameter('ingress')) {
111
228
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
112
229
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -120,6 +237,7 @@ export class Frontend {
120
237
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
121
238
  });
122
239
  }
240
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
241
  this.httpsServer.on('error', (error) => {
124
242
  this.log.error(`Frontend https server error listening on ${this.port}`);
125
243
  switch (error.code) {
@@ -136,16 +254,18 @@ export class Frontend {
136
254
  }
137
255
  if (this.initializeError)
138
256
  return;
257
+ // Create a WebSocket server and attach it to the http or https server
139
258
  const wssPort = this.port;
140
259
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
141
260
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
142
261
  this.webSocketServer.on('connection', (ws, request) => {
143
262
  const clientIp = request.socket.remoteAddress;
144
- let callbackLogLevel = "notice";
145
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
146
- callbackLogLevel = "info";
147
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
148
- callbackLogLevel = "debug";
263
+ // Set the global logger callback for the WebSocketServer
264
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
265
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
266
+ callbackLogLevel = "info" /* LogLevel.INFO */;
267
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
268
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
149
269
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
150
270
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
151
271
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -179,6 +299,7 @@ export class Frontend {
179
299
  this.webSocketServer.on('error', (ws, error) => {
180
300
  this.log.error(`WebSocketServer error: ${error}`);
181
301
  });
302
+ // Subscribe to cli events
182
303
  const { cliEmitter } = await import('./cli.js');
183
304
  cliEmitter.removeAllListeners();
184
305
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
@@ -190,6 +311,8 @@ export class Frontend {
190
311
  cliEmitter.on('cpu', (cpuUsage) => {
191
312
  this.wssSendCpuUpdate(cpuUsage);
192
313
  });
314
+ // Endpoint to validate login code
315
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
193
316
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
194
317
  const { password } = req.body;
195
318
  this.log.debug('The frontend sent /api/login', password);
@@ -208,23 +331,27 @@ export class Frontend {
208
331
  this.log.warn('/api/login error wrong password');
209
332
  res.json({ valid: false });
210
333
  }
334
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
211
335
  }
212
336
  catch (error) {
213
337
  this.log.error('/api/login error getting password');
214
338
  res.json({ valid: false });
215
339
  }
216
340
  });
341
+ // Endpoint to provide health check for docker
217
342
  this.expressApp.get('/health', (req, res) => {
218
343
  this.log.debug('Express received /health');
219
344
  const healthStatus = {
220
- status: 'ok',
221
- uptime: process.uptime(),
222
- timestamp: new Date().toISOString(),
345
+ status: 'ok', // Indicate service is healthy
346
+ uptime: process.uptime(), // Server uptime in seconds
347
+ timestamp: new Date().toISOString(), // Current timestamp
223
348
  };
224
349
  res.status(200).json(healthStatus);
225
350
  });
351
+ // Endpoint to provide memory usage details
226
352
  this.expressApp.get('/memory', async (req, res) => {
227
353
  this.log.debug('Express received /memory');
354
+ // Memory usage from process
228
355
  const memoryUsageRaw = process.memoryUsage();
229
356
  const memoryUsage = {
230
357
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -233,10 +360,13 @@ export class Frontend {
233
360
  external: this.formatMemoryUsage(memoryUsageRaw.external),
234
361
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
235
362
  };
363
+ // V8 heap statistics
236
364
  const { default: v8 } = await import('node:v8');
237
365
  const heapStatsRaw = v8.getHeapStatistics();
238
366
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
367
+ // Format heapStats
239
368
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
369
+ // Format heapSpaces
240
370
  const heapSpaces = heapSpacesRaw.map((space) => ({
241
371
  ...space,
242
372
  space_size: this.formatMemoryUsage(space.space_size),
@@ -254,19 +384,23 @@ export class Frontend {
254
384
  };
255
385
  res.status(200).json(memoryReport);
256
386
  });
387
+ // Endpoint to provide settings
257
388
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
258
389
  this.log.debug('The frontend sent /api/settings');
259
390
  res.json(await this.getApiSettings());
260
391
  });
392
+ // Endpoint to provide plugins
261
393
  this.expressApp.get('/api/plugins', async (req, res) => {
262
394
  this.log.debug('The frontend sent /api/plugins');
263
395
  res.json(this.getBaseRegisteredPlugins());
264
396
  });
397
+ // Endpoint to provide devices
265
398
  this.expressApp.get('/api/devices', async (req, res) => {
266
399
  this.log.debug('The frontend sent /api/devices');
267
400
  const devices = await this.getDevices();
268
401
  res.json(devices);
269
402
  });
403
+ // Endpoint to view the matterbridge log
270
404
  this.expressApp.get('/api/view-mblog', async (req, res) => {
271
405
  this.log.debug('The frontend sent /api/view-mblog');
272
406
  try {
@@ -279,6 +413,7 @@ export class Frontend {
279
413
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
280
414
  }
281
415
  });
416
+ // Endpoint to view the matter.js log
282
417
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
283
418
  this.log.debug('The frontend sent /api/view-mjlog');
284
419
  try {
@@ -291,6 +426,7 @@ export class Frontend {
291
426
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
292
427
  }
293
428
  });
429
+ // Endpoint to view the shelly log
294
430
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
295
431
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
296
432
  try {
@@ -303,6 +439,7 @@ export class Frontend {
303
439
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
304
440
  }
305
441
  });
442
+ // Endpoint to download the matterbridge log
306
443
  this.expressApp.get('/api/download-mblog', async (req, res) => {
307
444
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
308
445
  try {
@@ -315,6 +452,7 @@ export class Frontend {
315
452
  this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
316
453
  }
317
454
  res.type('text/plain');
455
+ // res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
318
456
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
319
457
  if (error) {
320
458
  this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
@@ -322,6 +460,7 @@ export class Frontend {
322
460
  }
323
461
  });
324
462
  });
463
+ // Endpoint to download the matter log
325
464
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
326
465
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
327
466
  try {
@@ -341,6 +480,7 @@ export class Frontend {
341
480
  }
342
481
  });
343
482
  });
483
+ // Endpoint to download the shelly log
344
484
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
345
485
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
346
486
  try {
@@ -360,6 +500,7 @@ export class Frontend {
360
500
  }
361
501
  });
362
502
  });
503
+ // Endpoint to download the matterbridge storage directory
363
504
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
364
505
  this.log.debug('The frontend sent /api/download-mbstorage');
365
506
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
@@ -370,6 +511,7 @@ export class Frontend {
370
511
  }
371
512
  });
372
513
  });
514
+ // Endpoint to download the matter storage file
373
515
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
374
516
  this.log.debug('The frontend sent /api/download-mjstorage');
375
517
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
@@ -380,6 +522,7 @@ export class Frontend {
380
522
  }
381
523
  });
382
524
  });
525
+ // Endpoint to download the matterbridge plugin directory
383
526
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
384
527
  this.log.debug('The frontend sent /api/download-pluginstorage');
385
528
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
@@ -390,6 +533,7 @@ export class Frontend {
390
533
  }
391
534
  });
392
535
  });
536
+ // Endpoint to download the matterbridge plugin config files
393
537
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
394
538
  this.log.debug('The frontend sent /api/download-pluginconfig');
395
539
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
@@ -400,6 +544,7 @@ export class Frontend {
400
544
  }
401
545
  });
402
546
  });
547
+ // Endpoint to download the matterbridge backup (created with the backup command)
403
548
  this.expressApp.get('/api/download-backup', async (req, res) => {
404
549
  this.log.debug('The frontend sent /api/download-backup');
405
550
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
@@ -409,6 +554,7 @@ export class Frontend {
409
554
  }
410
555
  });
411
556
  });
557
+ // Endpoint to upload a package
412
558
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
413
559
  const { filename } = req.body;
414
560
  const file = req.file;
@@ -418,10 +564,13 @@ export class Frontend {
418
564
  return;
419
565
  }
420
566
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
567
+ // Define the path where the plugin file will be saved
421
568
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
422
569
  try {
570
+ // Move the uploaded file to the specified path
423
571
  await fs.rename(file.path, filePath);
424
572
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
573
+ // Install the plugin package
425
574
  if (filename.endsWith('.tgz')) {
426
575
  await this.matterbridge.spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
427
576
  this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
@@ -440,6 +589,7 @@ export class Frontend {
440
589
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
441
590
  }
442
591
  });
592
+ // Fallback for routing (must be the last route)
443
593
  this.expressApp.use((req, res) => {
444
594
  this.log.debug('The frontend sent:', req.url);
445
595
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -448,12 +598,15 @@ export class Frontend {
448
598
  }
449
599
  async stop() {
450
600
  this.log.debug('Stopping the frontend...');
601
+ // Remove listeners from the express app
451
602
  if (this.expressApp) {
452
603
  this.expressApp.removeAllListeners();
453
604
  this.expressApp = undefined;
454
605
  this.log.debug('Frontend app closed successfully');
455
606
  }
607
+ // Close the WebSocket server
456
608
  if (this.webSocketServer) {
609
+ // Close all active connections
457
610
  this.webSocketServer.clients.forEach((client) => {
458
611
  if (client.readyState === WebSocket.OPEN) {
459
612
  client.close();
@@ -473,6 +626,7 @@ export class Frontend {
473
626
  this.webSocketServer.removeAllListeners();
474
627
  this.webSocketServer = undefined;
475
628
  }
629
+ // Close the http server
476
630
  if (this.httpServer) {
477
631
  await withTimeout(new Promise((resolve) => {
478
632
  this.httpServer?.close((error) => {
@@ -489,6 +643,7 @@ export class Frontend {
489
643
  this.httpServer = undefined;
490
644
  this.log.debug('Frontend http server closed successfully');
491
645
  }
646
+ // Close the https server
492
647
  if (this.httpsServer) {
493
648
  await withTimeout(new Promise((resolve) => {
494
649
  this.httpsServer?.close((error) => {
@@ -507,6 +662,7 @@ export class Frontend {
507
662
  }
508
663
  this.log.debug('Frontend stopped successfully');
509
664
  }
665
+ // Function to format bytes to KB, MB, or GB
510
666
  formatMemoryUsage = (bytes) => {
511
667
  if (bytes >= 1024 ** 3) {
512
668
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -518,6 +674,7 @@ export class Frontend {
518
674
  return `${(bytes / 1024).toFixed(2)} KB`;
519
675
  }
520
676
  };
677
+ // Function to format system uptime with only the most significant unit
521
678
  formatOsUpTime = (seconds) => {
522
679
  if (seconds >= 86400) {
523
680
  const days = Math.floor(seconds / 86400);
@@ -533,8 +690,14 @@ export class Frontend {
533
690
  }
534
691
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
535
692
  };
693
+ /**
694
+ * Retrieves the api settings data.
695
+ *
696
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
697
+ */
536
698
  async getApiSettings() {
537
699
  const { lastCpuUsage } = await import('./cli.js');
700
+ // Update the system information
538
701
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
539
702
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
540
703
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -543,6 +706,7 @@ export class Frontend {
543
706
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
544
707
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
545
708
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
709
+ // Update the matterbridge information
546
710
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
547
711
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
548
712
  this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
@@ -561,6 +725,11 @@ export class Frontend {
561
725
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
562
726
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
563
727
  }
728
+ /**
729
+ * Retrieves the reachable attribute.
730
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
731
+ * @returns {boolean} The reachable attribute.
732
+ */
564
733
  getReachability(device) {
565
734
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
566
735
  return false;
@@ -570,6 +739,11 @@ export class Frontend {
570
739
  return true;
571
740
  return false;
572
741
  }
742
+ /**
743
+ * Retrieves the power source attribute.
744
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice object.
745
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
746
+ */
573
747
  getPowerSource(endpoint) {
574
748
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
575
749
  return undefined;
@@ -585,13 +759,20 @@ export class Frontend {
585
759
  }
586
760
  return;
587
761
  };
762
+ // Root endpoint
588
763
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
589
764
  return powerSource(endpoint);
765
+ // Child endpoints
590
766
  for (const child of endpoint.getChildEndpoints()) {
591
767
  if (child.hasClusterServer(PowerSource.Cluster.id))
592
768
  return powerSource(child);
593
769
  }
594
770
  }
771
+ /**
772
+ * Retrieves the cluster text description from a given device.
773
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
774
+ * @returns {string} The attributes description of the cluster servers in the device.
775
+ */
595
776
  getClusterTextFromDevice(device) {
596
777
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
597
778
  return '';
@@ -633,6 +814,7 @@ export class Frontend {
633
814
  let attributes = '';
634
815
  let supportedModes = [];
635
816
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
817
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
636
818
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
637
819
  return;
638
820
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -724,8 +906,13 @@ export class Frontend {
724
906
  if (clusterName === 'userLabel' && attributeName === 'labelList')
725
907
  attributes += `${getUserLabel(device)} `;
726
908
  });
909
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
727
910
  return attributes.trimStart().trimEnd();
728
911
  }
912
+ /**
913
+ * Retrieves the base registered plugins sanitized for res.json().
914
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
915
+ */
729
916
  getBaseRegisteredPlugins() {
730
917
  const baseRegisteredPlugins = [];
731
918
  for (const plugin of this.matterbridge.plugins) {
@@ -764,11 +951,18 @@ export class Frontend {
764
951
  }
765
952
  return baseRegisteredPlugins;
766
953
  }
954
+ /**
955
+ * Retrieves the devices from Matterbridge.
956
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
957
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices.
958
+ */
767
959
  async getDevices(pluginName) {
768
960
  const devices = [];
769
961
  this.matterbridge.devices.forEach(async (device) => {
962
+ // Filter by pluginName if provided
770
963
  if (pluginName && pluginName !== device.plugin)
771
964
  return;
965
+ // Check if the device has the required properties
772
966
  if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
773
967
  return;
774
968
  const cluster = this.getClusterTextFromDevice(device);
@@ -788,22 +982,37 @@ export class Frontend {
788
982
  });
789
983
  return devices;
790
984
  }
985
+ /**
986
+ * Retrieves the clusters from a given plugin and endpoint number.
987
+ *
988
+ * Response for /api/clusters
989
+ *
990
+ * @param {string} pluginName - The name of the plugin.
991
+ * @param {number} endpointNumber - The endpoint number.
992
+ * @returns {Promise<ApiClustersResponse | undefined>} A promise that resolves to the clusters or undefined if not found.
993
+ */
791
994
  getClusters(pluginName, endpointNumber) {
792
995
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
793
996
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
794
997
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
795
998
  return;
796
999
  }
1000
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1001
+ // Get the device types from the main endpoint
797
1002
  const deviceTypes = [];
798
1003
  const clusters = [];
799
1004
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
800
1005
  deviceTypes.push(d.deviceType);
801
1006
  });
1007
+ // Get the clusters from the main endpoint
802
1008
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
803
1009
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
804
1010
  return;
805
1011
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
806
1012
  return;
1013
+ // console.log(
1014
+ // `${idn}${endpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
1015
+ // );
807
1016
  clusters.push({
808
1017
  endpoint: endpoint.number.toString(),
809
1018
  id: 'main',
@@ -816,12 +1025,18 @@ export class Frontend {
816
1025
  attributeLocalValue: attributeValue,
817
1026
  });
818
1027
  });
1028
+ // Get the child endpoints
819
1029
  const childEndpoints = endpoint.getChildEndpoints();
1030
+ // if (childEndpoints.length === 0) {
1031
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1032
+ // }
820
1033
  childEndpoints.forEach((childEndpoint) => {
821
1034
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
822
1035
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
823
1036
  return;
824
1037
  }
1038
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1039
+ // Get the device types of the child endpoint
825
1040
  const deviceTypes = [];
826
1041
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
827
1042
  deviceTypes.push(d.deviceType);
@@ -831,9 +1046,12 @@ export class Frontend {
831
1046
  return;
832
1047
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
833
1048
  return;
1049
+ // console.log(
1050
+ // `${idn}${childEndpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
1051
+ // );
834
1052
  clusters.push({
835
1053
  endpoint: childEndpoint.number.toString(),
836
- id: childEndpoint.maybeId ?? 'null',
1054
+ id: childEndpoint.maybeId ?? 'null', // Never happens
837
1055
  deviceTypes,
838
1056
  clusterName: capitalizeFirstLetter(clusterName),
839
1057
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -846,6 +1064,13 @@ export class Frontend {
846
1064
  });
847
1065
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
848
1066
  }
1067
+ /**
1068
+ * Handles incoming websocket messages for the Matterbridge frontend.
1069
+ *
1070
+ * @param {WebSocket} client - The websocket client that sent the message.
1071
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1072
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1073
+ */
849
1074
  async wsMessageHandler(client, message) {
850
1075
  let data;
851
1076
  try {
@@ -892,8 +1117,10 @@ export class Frontend {
892
1117
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
893
1118
  const packageName = data.params.packageName.replace(/@.*$/, '');
894
1119
  if (data.params.restart === false && packageName !== 'matterbridge') {
1120
+ // The install comes from InstallPlugins
895
1121
  this.matterbridge.plugins.add(packageName).then((plugin) => {
896
1122
  if (plugin) {
1123
+ // The plugin is not registered
897
1124
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
898
1125
  this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
899
1126
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
@@ -901,6 +1128,7 @@ export class Frontend {
901
1128
  });
902
1129
  }
903
1130
  else {
1131
+ // The plugin is already registered
904
1132
  this.wssSendSnackbarMessage(`Restart required`, 0);
905
1133
  this.wssSendRefreshRequired('plugins');
906
1134
  this.wssSendRestartRequired();
@@ -908,6 +1136,7 @@ export class Frontend {
908
1136
  });
909
1137
  }
910
1138
  else {
1139
+ // The package is matterbridge
911
1140
  if (this.matterbridge.restartMode !== '') {
912
1141
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
913
1142
  this.matterbridge.shutdownProcess();
@@ -929,6 +1158,7 @@ export class Frontend {
929
1158
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
930
1159
  return;
931
1160
  }
1161
+ // The package is a plugin
932
1162
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
933
1163
  if (plugin) {
934
1164
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -937,6 +1167,7 @@ export class Frontend {
937
1167
  this.wssSendRefreshRequired('plugins');
938
1168
  this.wssSendRefreshRequired('devices');
939
1169
  }
1170
+ // Uninstall the package
940
1171
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
941
1172
  this.matterbridge
942
1173
  .spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1248,22 +1479,22 @@ export class Frontend {
1248
1479
  if (isValidString(data.params.value, 4)) {
1249
1480
  this.log.debug('Matterbridge logger level:', data.params.value);
1250
1481
  if (data.params.value === 'Debug') {
1251
- await this.matterbridge.setLogLevel("debug");
1482
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1252
1483
  }
1253
1484
  else if (data.params.value === 'Info') {
1254
- await this.matterbridge.setLogLevel("info");
1485
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1255
1486
  }
1256
1487
  else if (data.params.value === 'Notice') {
1257
- await this.matterbridge.setLogLevel("notice");
1488
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1258
1489
  }
1259
1490
  else if (data.params.value === 'Warn') {
1260
- await this.matterbridge.setLogLevel("warn");
1491
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1261
1492
  }
1262
1493
  else if (data.params.value === 'Error') {
1263
- await this.matterbridge.setLogLevel("error");
1494
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1264
1495
  }
1265
1496
  else if (data.params.value === 'Fatal') {
1266
- await this.matterbridge.setLogLevel("fatal");
1497
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1267
1498
  }
1268
1499
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1269
1500
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1274,6 +1505,7 @@ export class Frontend {
1274
1505
  this.log.debug('Matterbridge file log:', data.params.value);
1275
1506
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1276
1507
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1508
+ // Create the file logger for matterbridge
1277
1509
  if (data.params.value)
1278
1510
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1279
1511
  else
@@ -1438,15 +1670,19 @@ export class Frontend {
1438
1670
  return;
1439
1671
  }
1440
1672
  const config = plugin.configJson;
1673
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1441
1674
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1675
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1442
1676
  if (select === 'serial')
1443
1677
  this.log.info(`Selected device serial ${data.params.serial}`);
1444
1678
  if (select === 'name')
1445
1679
  this.log.info(`Selected device name ${data.params.name}`);
1446
1680
  if (config && select && (select === 'serial' || select === 'name')) {
1681
+ // Remove postfix from the serial if it exists
1447
1682
  if (config.postfix) {
1448
1683
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1449
1684
  }
1685
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1450
1686
  if (isValidArray(config.whiteList, 1)) {
1451
1687
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1452
1688
  config.whiteList.push(data.params.serial);
@@ -1455,6 +1691,7 @@ export class Frontend {
1455
1691
  config.whiteList.push(data.params.name);
1456
1692
  }
1457
1693
  }
1694
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1458
1695
  if (isValidArray(config.blackList, 1)) {
1459
1696
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1460
1697
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1484,7 +1721,9 @@ export class Frontend {
1484
1721
  return;
1485
1722
  }
1486
1723
  const config = plugin.configJson;
1724
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1487
1725
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1726
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1488
1727
  if (select === 'serial')
1489
1728
  this.log.info(`Unselected device serial ${data.params.serial}`);
1490
1729
  if (select === 'name')
@@ -1493,6 +1732,7 @@ export class Frontend {
1493
1732
  if (config.postfix) {
1494
1733
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1495
1734
  }
1735
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1496
1736
  if (isValidArray(config.whiteList, 1)) {
1497
1737
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1498
1738
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1501,6 +1741,7 @@ export class Frontend {
1501
1741
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1502
1742
  }
1503
1743
  }
1744
+ // Add the serial to the blackList
1504
1745
  if (isValidArray(config.blackList)) {
1505
1746
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1506
1747
  config.blackList.push(data.params.serial);
@@ -1533,114 +1774,219 @@ export class Frontend {
1533
1774
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1534
1775
  }
1535
1776
  }
1777
+ /**
1778
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1779
+ *
1780
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1781
+ * @param {string} time - The time string of the message
1782
+ * @param {string} name - The logger name of the message
1783
+ * @param {string} message - The content of the message.
1784
+ *
1785
+ * @remark
1786
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1787
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1788
+ * The function sends the message to all connected clients.
1789
+ */
1536
1790
  wssSendMessage(level, time, name, message) {
1537
1791
  if (!level || !time || !name || !message)
1538
1792
  return;
1793
+ // Remove ANSI escape codes from the message
1794
+ // eslint-disable-next-line no-control-regex
1539
1795
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1796
+ // Remove leading asterisks from the message
1540
1797
  message = message.replace(/^\*+/, '');
1798
+ // Replace all occurrences of \t and \n
1541
1799
  message = message.replace(/[\t\n]/g, '');
1800
+ // Remove non-printable characters
1801
+ // eslint-disable-next-line no-control-regex
1542
1802
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1803
+ // Replace all occurrences of \" with "
1543
1804
  message = message.replace(/\\"/g, '"');
1805
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1544
1806
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1807
+ // Define the maximum allowed length for continuous characters without a space
1545
1808
  const maxContinuousLength = 100;
1546
1809
  const keepStartLength = 20;
1547
1810
  const keepEndLength = 20;
1811
+ // Split the message into words
1548
1812
  message = message
1549
1813
  .split(' ')
1550
1814
  .map((word) => {
1815
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1551
1816
  if (word.length > maxContinuousLength) {
1552
1817
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1553
1818
  }
1554
1819
  return word;
1555
1820
  })
1556
1821
  .join(' ');
1822
+ // Send the message to all connected clients
1557
1823
  this.webSocketServer?.clients.forEach((client) => {
1558
1824
  if (client.readyState === WebSocket.OPEN) {
1559
1825
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1560
1826
  }
1561
1827
  });
1562
1828
  }
1829
+ /**
1830
+ * Sends a need to refresh WebSocket message to all connected clients.
1831
+ *
1832
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1833
+ * possible values:
1834
+ * - 'matterbridgeLatestVersion'
1835
+ * - 'matterbridgeAdvertise'
1836
+ * - 'online'
1837
+ * - 'offline'
1838
+ * - 'reachability'
1839
+ * - 'settings'
1840
+ * - 'plugins'
1841
+ * - 'pluginsRestart'
1842
+ * - 'devices'
1843
+ * - 'fabrics'
1844
+ * - 'sessions'
1845
+ */
1563
1846
  wssSendRefreshRequired(changed = null) {
1564
1847
  this.log.debug('Sending a refresh required message to all connected clients');
1848
+ // Send the message to all connected clients
1565
1849
  this.webSocketServer?.clients.forEach((client) => {
1566
1850
  if (client.readyState === WebSocket.OPEN) {
1567
1851
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1568
1852
  }
1569
1853
  });
1570
1854
  }
1855
+ /**
1856
+ * Sends a need to restart WebSocket message to all connected clients.
1857
+ *
1858
+ */
1571
1859
  wssSendRestartRequired(snackbar = true) {
1572
1860
  this.log.debug('Sending a restart required message to all connected clients');
1573
1861
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1574
1862
  if (snackbar === true)
1575
1863
  this.wssSendSnackbarMessage(`Restart required`, 0);
1864
+ // Send the message to all connected clients
1576
1865
  this.webSocketServer?.clients.forEach((client) => {
1577
1866
  if (client.readyState === WebSocket.OPEN) {
1578
1867
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1579
1868
  }
1580
1869
  });
1581
1870
  }
1871
+ /**
1872
+ * Sends a need to update WebSocket message to all connected clients.
1873
+ *
1874
+ */
1582
1875
  wssSendUpdateRequired() {
1583
1876
  this.log.debug('Sending an update required message to all connected clients');
1584
1877
  this.matterbridge.matterbridgeInformation.updateRequired = true;
1878
+ // Send the message to all connected clients
1585
1879
  this.webSocketServer?.clients.forEach((client) => {
1586
1880
  if (client.readyState === WebSocket.OPEN) {
1587
1881
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1588
1882
  }
1589
1883
  });
1590
1884
  }
1885
+ /**
1886
+ * Sends a cpu update message to all connected clients.
1887
+ *
1888
+ */
1591
1889
  wssSendCpuUpdate(cpuUsage) {
1592
1890
  if (hasParameter('debug'))
1593
1891
  this.log.debug('Sending a cpu update message to all connected clients');
1892
+ // Send the message to all connected clients
1594
1893
  this.webSocketServer?.clients.forEach((client) => {
1595
1894
  if (client.readyState === WebSocket.OPEN) {
1596
1895
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1597
1896
  }
1598
1897
  });
1599
1898
  }
1899
+ /**
1900
+ * Sends a memory update message to all connected clients.
1901
+ *
1902
+ */
1600
1903
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1601
1904
  if (hasParameter('debug'))
1602
1905
  this.log.debug('Sending a memory update message to all connected clients');
1906
+ // Send the message to all connected clients
1603
1907
  this.webSocketServer?.clients.forEach((client) => {
1604
1908
  if (client.readyState === WebSocket.OPEN) {
1605
1909
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1606
1910
  }
1607
1911
  });
1608
1912
  }
1913
+ /**
1914
+ * Sends an uptime update message to all connected clients.
1915
+ *
1916
+ */
1609
1917
  wssSendUptimeUpdate(systemUptime, processUptime) {
1610
1918
  if (hasParameter('debug'))
1611
1919
  this.log.debug('Sending a uptime update message to all connected clients');
1920
+ // Send the message to all connected clients
1612
1921
  this.webSocketServer?.clients.forEach((client) => {
1613
1922
  if (client.readyState === WebSocket.OPEN) {
1614
1923
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1615
1924
  }
1616
1925
  });
1617
1926
  }
1927
+ /**
1928
+ * Sends an open snackbar message to all connected clients.
1929
+ * @param {string} message - The message to send.
1930
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
1931
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
1932
+ *
1933
+ */
1618
1934
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1619
1935
  this.log.debug('Sending a snackbar message to all connected clients');
1936
+ // Send the message to all connected clients
1620
1937
  this.webSocketServer?.clients.forEach((client) => {
1621
1938
  if (client.readyState === WebSocket.OPEN) {
1622
1939
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1623
1940
  }
1624
1941
  });
1625
1942
  }
1943
+ /**
1944
+ * Sends a close snackbar message to all connected clients.
1945
+ * @param {string} message - The message to send.
1946
+ *
1947
+ */
1626
1948
  wssSendCloseSnackbarMessage(message) {
1627
1949
  this.log.debug('Sending a close snackbar message to all connected clients');
1950
+ // Send the message to all connected clients
1628
1951
  this.webSocketServer?.clients.forEach((client) => {
1629
1952
  if (client.readyState === WebSocket.OPEN) {
1630
1953
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1631
1954
  }
1632
1955
  });
1633
1956
  }
1957
+ /**
1958
+ * Sends an attribute update message to all connected WebSocket clients.
1959
+ *
1960
+ * @param {string | undefined} plugin - The name of the plugin.
1961
+ * @param {string | undefined} serialNumber - The serial number of the device.
1962
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
1963
+ * @param {string} cluster - The cluster name where the attribute belongs.
1964
+ * @param {string} attribute - The name of the attribute that changed.
1965
+ * @param {number | string | boolean} value - The new value of the attribute.
1966
+ *
1967
+ * @remarks
1968
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
1969
+ * with the updated attribute information.
1970
+ */
1634
1971
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1635
1972
  this.log.debug('Sending an attribute update message to all connected clients');
1973
+ // Send the message to all connected clients
1636
1974
  this.webSocketServer?.clients.forEach((client) => {
1637
1975
  if (client.readyState === WebSocket.OPEN) {
1638
1976
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1639
1977
  }
1640
1978
  });
1641
1979
  }
1980
+ /**
1981
+ * Sends a message to all connected clients.
1982
+ * @param {number} id - The message id.
1983
+ * @param {string} method - The message method.
1984
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
1985
+ *
1986
+ */
1642
1987
  wssBroadcastMessage(id, method, params) {
1643
1988
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
1989
+ // Send the message to all connected clients
1644
1990
  this.webSocketServer?.clients.forEach((client) => {
1645
1991
  if (client.readyState === WebSocket.OPEN) {
1646
1992
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1648,3 +1994,4 @@ export class Frontend {
1648
1994
  });
1649
1995
  }
1650
1996
  }
1997
+ //# sourceMappingURL=frontend.js.map