matterbridge 3.0.5-dev-20250607-33ce8f8 → 3.0.5

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 (171) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/dist/cli.d.ts +29 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +79 -5
  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/evse.d.ts +63 -0
  19. package/dist/evse.d.ts.map +1 -0
  20. package/dist/evse.js +66 -29
  21. package/dist/evse.js.map +1 -0
  22. package/dist/frontend.d.ts +256 -0
  23. package/dist/frontend.d.ts.map +1 -0
  24. package/dist/frontend.js +374 -16
  25. package/dist/frontend.js.map +1 -0
  26. package/dist/helpers.d.ts +47 -0
  27. package/dist/helpers.d.ts.map +1 -0
  28. package/dist/helpers.js +51 -0
  29. package/dist/helpers.js.map +1 -0
  30. package/dist/index.d.ts +37 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +28 -1
  33. package/dist/index.js.map +1 -0
  34. package/dist/laundryWasher.d.ts +243 -0
  35. package/dist/laundryWasher.d.ts.map +1 -0
  36. package/dist/laundryWasher.js +92 -7
  37. package/dist/laundryWasher.js.map +1 -0
  38. package/dist/logger/export.d.ts +2 -0
  39. package/dist/logger/export.d.ts.map +1 -0
  40. package/dist/logger/export.js +1 -0
  41. package/dist/logger/export.js.map +1 -0
  42. package/dist/matter/behaviors.d.ts +2 -0
  43. package/dist/matter/behaviors.d.ts.map +1 -0
  44. package/dist/matter/behaviors.js +2 -0
  45. package/dist/matter/behaviors.js.map +1 -0
  46. package/dist/matter/clusters.d.ts +2 -0
  47. package/dist/matter/clusters.d.ts.map +1 -0
  48. package/dist/matter/clusters.js +2 -0
  49. package/dist/matter/clusters.js.map +1 -0
  50. package/dist/matter/devices.d.ts +2 -0
  51. package/dist/matter/devices.d.ts.map +1 -0
  52. package/dist/matter/devices.js +2 -0
  53. package/dist/matter/devices.js.map +1 -0
  54. package/dist/matter/endpoints.d.ts +2 -0
  55. package/dist/matter/endpoints.d.ts.map +1 -0
  56. package/dist/matter/endpoints.js +2 -0
  57. package/dist/matter/endpoints.js.map +1 -0
  58. package/dist/matter/export.d.ts +5 -0
  59. package/dist/matter/export.d.ts.map +1 -0
  60. package/dist/matter/export.js +2 -0
  61. package/dist/matter/export.js.map +1 -0
  62. package/dist/matter/types.d.ts +3 -0
  63. package/dist/matter/types.d.ts.map +1 -0
  64. package/dist/matter/types.js +2 -0
  65. package/dist/matter/types.js.map +1 -0
  66. package/dist/matterbridge.d.ts +449 -0
  67. package/dist/matterbridge.d.ts.map +1 -0
  68. package/dist/matterbridge.js +747 -47
  69. package/dist/matterbridge.js.map +1 -0
  70. package/dist/matterbridgeAccessoryPlatform.d.ts +40 -0
  71. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  72. package/dist/matterbridgeAccessoryPlatform.js +34 -0
  73. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  74. package/dist/matterbridgeBehaviors.d.ts +1379 -0
  75. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  76. package/dist/matterbridgeBehaviors.js +49 -1
  77. package/dist/matterbridgeBehaviors.js.map +1 -0
  78. package/dist/matterbridgeDeviceTypes.d.ts +644 -0
  79. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  80. package/dist/matterbridgeDeviceTypes.js +578 -15
  81. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  82. package/dist/matterbridgeDynamicPlatform.d.ts +40 -0
  83. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  84. package/dist/matterbridgeDynamicPlatform.js +34 -0
  85. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  86. package/dist/matterbridgeEndpoint.d.ts +1079 -0
  87. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  88. package/dist/matterbridgeEndpoint.js +924 -24
  89. package/dist/matterbridgeEndpoint.js.map +1 -0
  90. package/dist/matterbridgeEndpointHelpers.d.ts +2749 -0
  91. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  92. package/dist/matterbridgeEndpointHelpers.js +172 -10
  93. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  94. package/dist/matterbridgePlatform.d.ts +294 -0
  95. package/dist/matterbridgePlatform.d.ts.map +1 -0
  96. package/dist/matterbridgePlatform.js +225 -7
  97. package/dist/matterbridgePlatform.js.map +1 -0
  98. package/dist/matterbridgeTypes.d.ts +196 -0
  99. package/dist/matterbridgeTypes.d.ts.map +1 -0
  100. package/dist/matterbridgeTypes.js +24 -0
  101. package/dist/matterbridgeTypes.js.map +1 -0
  102. package/dist/pluginManager.d.ts +273 -0
  103. package/dist/pluginManager.d.ts.map +1 -0
  104. package/dist/pluginManager.js +264 -3
  105. package/dist/pluginManager.js.map +1 -0
  106. package/dist/roboticVacuumCleaner.d.ts +102 -0
  107. package/dist/roboticVacuumCleaner.d.ts.map +1 -0
  108. package/dist/roboticVacuumCleaner.js +81 -6
  109. package/dist/roboticVacuumCleaner.js.map +1 -0
  110. package/dist/shelly.d.ts +153 -0
  111. package/dist/shelly.d.ts.map +1 -0
  112. package/dist/shelly.js +155 -7
  113. package/dist/shelly.js.map +1 -0
  114. package/dist/storage/export.d.ts +2 -0
  115. package/dist/storage/export.d.ts.map +1 -0
  116. package/dist/storage/export.js +1 -0
  117. package/dist/storage/export.js.map +1 -0
  118. package/dist/update.d.ts +58 -0
  119. package/dist/update.d.ts.map +1 -0
  120. package/dist/update.js +53 -0
  121. package/dist/update.js.map +1 -0
  122. package/dist/utils/colorUtils.d.ts +61 -0
  123. package/dist/utils/colorUtils.d.ts.map +1 -0
  124. package/dist/utils/colorUtils.js +205 -2
  125. package/dist/utils/colorUtils.js.map +1 -0
  126. package/dist/utils/commandLine.d.ts +58 -0
  127. package/dist/utils/commandLine.d.ts.map +1 -0
  128. package/dist/utils/commandLine.js +53 -0
  129. package/dist/utils/commandLine.js.map +1 -0
  130. package/dist/utils/copyDirectory.d.ts +32 -0
  131. package/dist/utils/copyDirectory.d.ts.map +1 -0
  132. package/dist/utils/copyDirectory.js +37 -1
  133. package/dist/utils/copyDirectory.js.map +1 -0
  134. package/dist/utils/createZip.d.ts +38 -0
  135. package/dist/utils/createZip.d.ts.map +1 -0
  136. package/dist/utils/createZip.js +42 -2
  137. package/dist/utils/createZip.js.map +1 -0
  138. package/dist/utils/deepCopy.d.ts +31 -0
  139. package/dist/utils/deepCopy.d.ts.map +1 -0
  140. package/dist/utils/deepCopy.js +38 -0
  141. package/dist/utils/deepCopy.js.map +1 -0
  142. package/dist/utils/deepEqual.d.ts +53 -0
  143. package/dist/utils/deepEqual.d.ts.map +1 -0
  144. package/dist/utils/deepEqual.js +71 -1
  145. package/dist/utils/deepEqual.js.map +1 -0
  146. package/dist/utils/export.d.ts +11 -0
  147. package/dist/utils/export.d.ts.map +1 -0
  148. package/dist/utils/export.js +1 -0
  149. package/dist/utils/export.js.map +1 -0
  150. package/dist/utils/hex.d.ts +48 -0
  151. package/dist/utils/hex.d.ts.map +1 -0
  152. package/dist/utils/hex.js +57 -0
  153. package/dist/utils/hex.js.map +1 -0
  154. package/dist/utils/isvalid.d.ts +102 -0
  155. package/dist/utils/isvalid.d.ts.map +1 -0
  156. package/dist/utils/isvalid.js +100 -0
  157. package/dist/utils/isvalid.js.map +1 -0
  158. package/dist/utils/network.d.ts +69 -0
  159. package/dist/utils/network.d.ts.map +1 -0
  160. package/dist/utils/network.js +76 -5
  161. package/dist/utils/network.js.map +1 -0
  162. package/dist/utils/wait.d.ts +52 -0
  163. package/dist/utils/wait.d.ts.map +1 -0
  164. package/dist/utils/wait.js +58 -9
  165. package/dist/utils/wait.js.map +1 -0
  166. package/dist/waterHeater.d.ts +90 -0
  167. package/dist/waterHeater.d.ts.map +1 -0
  168. package/dist/waterHeater.js +62 -2
  169. package/dist/waterHeater.js.map +1 -0
  170. package/npm-shrinkwrap.json +2 -2
  171. 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} method ${req.method}: sending index.html as fallback`);
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 '';
@@ -632,7 +813,19 @@ export class Frontend {
632
813
  };
633
814
  let attributes = '';
634
815
  let supportedModes = [];
816
+ /*
817
+ Object.keys(device.behaviors.supported).forEach((clusterName) => {
818
+ const clusterBehavior = device.behaviors.supported[lowercaseFirstLetter(clusterName)] as ClusterBehavior.Type | undefined;
819
+ // console.log(`Device: ${device.deviceName} => Cluster: ${clusterName} Behavior: ${clusterBehavior?.id}`, clusterBehavior);
820
+ if (clusterBehavior && clusterBehavior.cluster && clusterBehavior.cluster.attributes) {
821
+ Object.entries(clusterBehavior.cluster.attributes).forEach(([attributeName, attribute]) => {
822
+ // console.log(`${device.deviceName} => Cluster: ${clusterName} Attribute: ${attributeName}`, attribute);
823
+ });
824
+ }
825
+ });
826
+ */
635
827
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
828
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
636
829
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
637
830
  return;
638
831
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -724,8 +917,13 @@ export class Frontend {
724
917
  if (clusterName === 'userLabel' && attributeName === 'labelList')
725
918
  attributes += `${getUserLabel(device)} `;
726
919
  });
920
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
727
921
  return attributes.trimStart().trimEnd();
728
922
  }
923
+ /**
924
+ * Retrieves the base registered plugins sanitized for res.json().
925
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
926
+ */
729
927
  getBaseRegisteredPlugins() {
730
928
  const baseRegisteredPlugins = [];
731
929
  for (const plugin of this.matterbridge.plugins) {
@@ -764,11 +962,18 @@ export class Frontend {
764
962
  }
765
963
  return baseRegisteredPlugins;
766
964
  }
965
+ /**
966
+ * Retrieves the devices from Matterbridge.
967
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
968
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices.
969
+ */
767
970
  async getDevices(pluginName) {
768
971
  const devices = [];
769
972
  this.matterbridge.devices.forEach(async (device) => {
973
+ // Filter by pluginName if provided
770
974
  if (pluginName && pluginName !== device.plugin)
771
975
  return;
976
+ // Check if the device has the required properties
772
977
  if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
773
978
  return;
774
979
  const cluster = this.getClusterTextFromDevice(device);
@@ -788,22 +993,37 @@ export class Frontend {
788
993
  });
789
994
  return devices;
790
995
  }
996
+ /**
997
+ * Retrieves the clusters from a given plugin and endpoint number.
998
+ *
999
+ * Response for /api/clusters
1000
+ *
1001
+ * @param {string} pluginName - The name of the plugin.
1002
+ * @param {number} endpointNumber - The endpoint number.
1003
+ * @returns {Promise<ApiClustersResponse | undefined>} A promise that resolves to the clusters or undefined if not found.
1004
+ */
791
1005
  getClusters(pluginName, endpointNumber) {
792
1006
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
793
1007
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
794
1008
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
795
1009
  return;
796
1010
  }
1011
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1012
+ // Get the device types from the main endpoint
797
1013
  const deviceTypes = [];
798
1014
  const clusters = [];
799
1015
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
800
1016
  deviceTypes.push(d.deviceType);
801
1017
  });
1018
+ // Get the clusters from the main endpoint
802
1019
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
803
1020
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
804
1021
  return;
805
1022
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
806
1023
  return;
1024
+ // console.log(
1025
+ // `${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}`,
1026
+ // );
807
1027
  clusters.push({
808
1028
  endpoint: endpoint.number.toString(),
809
1029
  id: 'main',
@@ -816,12 +1036,18 @@ export class Frontend {
816
1036
  attributeLocalValue: attributeValue,
817
1037
  });
818
1038
  });
1039
+ // Get the child endpoints
819
1040
  const childEndpoints = endpoint.getChildEndpoints();
1041
+ // if (childEndpoints.length === 0) {
1042
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1043
+ // }
820
1044
  childEndpoints.forEach((childEndpoint) => {
821
1045
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
822
1046
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
823
1047
  return;
824
1048
  }
1049
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1050
+ // Get the device types of the child endpoint
825
1051
  const deviceTypes = [];
826
1052
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
827
1053
  deviceTypes.push(d.deviceType);
@@ -831,9 +1057,12 @@ export class Frontend {
831
1057
  return;
832
1058
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
833
1059
  return;
1060
+ // console.log(
1061
+ // `${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}`,
1062
+ // );
834
1063
  clusters.push({
835
1064
  endpoint: childEndpoint.number.toString(),
836
- id: childEndpoint.maybeId ?? 'null',
1065
+ id: childEndpoint.maybeId ?? 'null', // Never happens
837
1066
  deviceTypes,
838
1067
  clusterName: capitalizeFirstLetter(clusterName),
839
1068
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -846,6 +1075,13 @@ export class Frontend {
846
1075
  });
847
1076
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
848
1077
  }
1078
+ /**
1079
+ * Handles incoming websocket messages for the Matterbridge frontend.
1080
+ *
1081
+ * @param {WebSocket} client - The websocket client that sent the message.
1082
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1083
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1084
+ */
849
1085
  async wsMessageHandler(client, message) {
850
1086
  let data;
851
1087
  try {
@@ -892,8 +1128,10 @@ export class Frontend {
892
1128
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
893
1129
  const packageName = data.params.packageName.replace(/@.*$/, '');
894
1130
  if (data.params.restart === false && packageName !== 'matterbridge') {
1131
+ // The install comes from InstallPlugins
895
1132
  this.matterbridge.plugins.add(packageName).then((plugin) => {
896
1133
  if (plugin) {
1134
+ // The plugin is not registered
897
1135
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
898
1136
  this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
899
1137
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
@@ -901,6 +1139,7 @@ export class Frontend {
901
1139
  });
902
1140
  }
903
1141
  else {
1142
+ // The plugin is already registered
904
1143
  this.wssSendSnackbarMessage(`Restart required`, 0);
905
1144
  this.wssSendRefreshRequired('plugins');
906
1145
  this.wssSendRestartRequired();
@@ -908,6 +1147,7 @@ export class Frontend {
908
1147
  });
909
1148
  }
910
1149
  else {
1150
+ // The package is matterbridge
911
1151
  if (this.matterbridge.restartMode !== '') {
912
1152
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
913
1153
  this.matterbridge.shutdownProcess();
@@ -929,6 +1169,7 @@ export class Frontend {
929
1169
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
930
1170
  return;
931
1171
  }
1172
+ // The package is a plugin
932
1173
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
933
1174
  if (plugin) {
934
1175
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -937,6 +1178,7 @@ export class Frontend {
937
1178
  this.wssSendRefreshRequired('plugins');
938
1179
  this.wssSendRefreshRequired('devices');
939
1180
  }
1181
+ // Uninstall the package
940
1182
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
941
1183
  this.matterbridge
942
1184
  .spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1248,22 +1490,22 @@ export class Frontend {
1248
1490
  if (isValidString(data.params.value, 4)) {
1249
1491
  this.log.debug('Matterbridge logger level:', data.params.value);
1250
1492
  if (data.params.value === 'Debug') {
1251
- await this.matterbridge.setLogLevel("debug");
1493
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1252
1494
  }
1253
1495
  else if (data.params.value === 'Info') {
1254
- await this.matterbridge.setLogLevel("info");
1496
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1255
1497
  }
1256
1498
  else if (data.params.value === 'Notice') {
1257
- await this.matterbridge.setLogLevel("notice");
1499
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1258
1500
  }
1259
1501
  else if (data.params.value === 'Warn') {
1260
- await this.matterbridge.setLogLevel("warn");
1502
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1261
1503
  }
1262
1504
  else if (data.params.value === 'Error') {
1263
- await this.matterbridge.setLogLevel("error");
1505
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1264
1506
  }
1265
1507
  else if (data.params.value === 'Fatal') {
1266
- await this.matterbridge.setLogLevel("fatal");
1508
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1267
1509
  }
1268
1510
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1269
1511
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1274,6 +1516,7 @@ export class Frontend {
1274
1516
  this.log.debug('Matterbridge file log:', data.params.value);
1275
1517
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1276
1518
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1519
+ // Create the file logger for matterbridge
1277
1520
  if (data.params.value)
1278
1521
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1279
1522
  else
@@ -1438,15 +1681,19 @@ export class Frontend {
1438
1681
  return;
1439
1682
  }
1440
1683
  const config = plugin.configJson;
1684
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1441
1685
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1686
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1442
1687
  if (select === 'serial')
1443
1688
  this.log.info(`Selected device serial ${data.params.serial}`);
1444
1689
  if (select === 'name')
1445
1690
  this.log.info(`Selected device name ${data.params.name}`);
1446
1691
  if (config && select && (select === 'serial' || select === 'name')) {
1692
+ // Remove postfix from the serial if it exists
1447
1693
  if (config.postfix) {
1448
1694
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1449
1695
  }
1696
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1450
1697
  if (isValidArray(config.whiteList, 1)) {
1451
1698
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1452
1699
  config.whiteList.push(data.params.serial);
@@ -1455,6 +1702,7 @@ export class Frontend {
1455
1702
  config.whiteList.push(data.params.name);
1456
1703
  }
1457
1704
  }
1705
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1458
1706
  if (isValidArray(config.blackList, 1)) {
1459
1707
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1460
1708
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1484,7 +1732,9 @@ export class Frontend {
1484
1732
  return;
1485
1733
  }
1486
1734
  const config = plugin.configJson;
1735
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1487
1736
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1737
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1488
1738
  if (select === 'serial')
1489
1739
  this.log.info(`Unselected device serial ${data.params.serial}`);
1490
1740
  if (select === 'name')
@@ -1493,6 +1743,7 @@ export class Frontend {
1493
1743
  if (config.postfix) {
1494
1744
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1495
1745
  }
1746
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1496
1747
  if (isValidArray(config.whiteList, 1)) {
1497
1748
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1498
1749
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1501,6 +1752,7 @@ export class Frontend {
1501
1752
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1502
1753
  }
1503
1754
  }
1755
+ // Add the serial to the blackList
1504
1756
  if (isValidArray(config.blackList)) {
1505
1757
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1506
1758
  config.blackList.push(data.params.serial);
@@ -1533,114 +1785,219 @@ export class Frontend {
1533
1785
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1534
1786
  }
1535
1787
  }
1788
+ /**
1789
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1790
+ *
1791
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1792
+ * @param {string} time - The time string of the message
1793
+ * @param {string} name - The logger name of the message
1794
+ * @param {string} message - The content of the message.
1795
+ *
1796
+ * @remark
1797
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1798
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1799
+ * The function sends the message to all connected clients.
1800
+ */
1536
1801
  wssSendMessage(level, time, name, message) {
1537
1802
  if (!level || !time || !name || !message)
1538
1803
  return;
1804
+ // Remove ANSI escape codes from the message
1805
+ // eslint-disable-next-line no-control-regex
1539
1806
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1807
+ // Remove leading asterisks from the message
1540
1808
  message = message.replace(/^\*+/, '');
1809
+ // Replace all occurrences of \t and \n
1541
1810
  message = message.replace(/[\t\n]/g, '');
1811
+ // Remove non-printable characters
1812
+ // eslint-disable-next-line no-control-regex
1542
1813
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1814
+ // Replace all occurrences of \" with "
1543
1815
  message = message.replace(/\\"/g, '"');
1816
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1544
1817
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1818
+ // Define the maximum allowed length for continuous characters without a space
1545
1819
  const maxContinuousLength = 100;
1546
1820
  const keepStartLength = 20;
1547
1821
  const keepEndLength = 20;
1822
+ // Split the message into words
1548
1823
  message = message
1549
1824
  .split(' ')
1550
1825
  .map((word) => {
1826
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1551
1827
  if (word.length > maxContinuousLength) {
1552
1828
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1553
1829
  }
1554
1830
  return word;
1555
1831
  })
1556
1832
  .join(' ');
1833
+ // Send the message to all connected clients
1557
1834
  this.webSocketServer?.clients.forEach((client) => {
1558
1835
  if (client.readyState === WebSocket.OPEN) {
1559
1836
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1560
1837
  }
1561
1838
  });
1562
1839
  }
1840
+ /**
1841
+ * Sends a need to refresh WebSocket message to all connected clients.
1842
+ *
1843
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1844
+ * possible values:
1845
+ * - 'matterbridgeLatestVersion'
1846
+ * - 'matterbridgeAdvertise'
1847
+ * - 'online'
1848
+ * - 'offline'
1849
+ * - 'reachability'
1850
+ * - 'settings'
1851
+ * - 'plugins'
1852
+ * - 'pluginsRestart'
1853
+ * - 'devices'
1854
+ * - 'fabrics'
1855
+ * - 'sessions'
1856
+ */
1563
1857
  wssSendRefreshRequired(changed = null) {
1564
1858
  this.log.debug('Sending a refresh required message to all connected clients');
1859
+ // Send the message to all connected clients
1565
1860
  this.webSocketServer?.clients.forEach((client) => {
1566
1861
  if (client.readyState === WebSocket.OPEN) {
1567
1862
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1568
1863
  }
1569
1864
  });
1570
1865
  }
1866
+ /**
1867
+ * Sends a need to restart WebSocket message to all connected clients.
1868
+ *
1869
+ */
1571
1870
  wssSendRestartRequired(snackbar = true) {
1572
1871
  this.log.debug('Sending a restart required message to all connected clients');
1573
1872
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1574
1873
  if (snackbar === true)
1575
1874
  this.wssSendSnackbarMessage(`Restart required`, 0);
1875
+ // Send the message to all connected clients
1576
1876
  this.webSocketServer?.clients.forEach((client) => {
1577
1877
  if (client.readyState === WebSocket.OPEN) {
1578
1878
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1579
1879
  }
1580
1880
  });
1581
1881
  }
1882
+ /**
1883
+ * Sends a need to update WebSocket message to all connected clients.
1884
+ *
1885
+ */
1582
1886
  wssSendUpdateRequired() {
1583
1887
  this.log.debug('Sending an update required message to all connected clients');
1584
1888
  this.matterbridge.matterbridgeInformation.updateRequired = true;
1889
+ // Send the message to all connected clients
1585
1890
  this.webSocketServer?.clients.forEach((client) => {
1586
1891
  if (client.readyState === WebSocket.OPEN) {
1587
1892
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1588
1893
  }
1589
1894
  });
1590
1895
  }
1896
+ /**
1897
+ * Sends a cpu update message to all connected clients.
1898
+ *
1899
+ */
1591
1900
  wssSendCpuUpdate(cpuUsage) {
1592
1901
  if (hasParameter('debug'))
1593
1902
  this.log.debug('Sending a cpu update message to all connected clients');
1903
+ // Send the message to all connected clients
1594
1904
  this.webSocketServer?.clients.forEach((client) => {
1595
1905
  if (client.readyState === WebSocket.OPEN) {
1596
1906
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1597
1907
  }
1598
1908
  });
1599
1909
  }
1910
+ /**
1911
+ * Sends a memory update message to all connected clients.
1912
+ *
1913
+ */
1600
1914
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1601
1915
  if (hasParameter('debug'))
1602
1916
  this.log.debug('Sending a memory update message to all connected clients');
1917
+ // Send the message to all connected clients
1603
1918
  this.webSocketServer?.clients.forEach((client) => {
1604
1919
  if (client.readyState === WebSocket.OPEN) {
1605
1920
  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
1921
  }
1607
1922
  });
1608
1923
  }
1924
+ /**
1925
+ * Sends an uptime update message to all connected clients.
1926
+ *
1927
+ */
1609
1928
  wssSendUptimeUpdate(systemUptime, processUptime) {
1610
1929
  if (hasParameter('debug'))
1611
1930
  this.log.debug('Sending a uptime update message to all connected clients');
1931
+ // Send the message to all connected clients
1612
1932
  this.webSocketServer?.clients.forEach((client) => {
1613
1933
  if (client.readyState === WebSocket.OPEN) {
1614
1934
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1615
1935
  }
1616
1936
  });
1617
1937
  }
1938
+ /**
1939
+ * Sends an open snackbar message to all connected clients.
1940
+ * @param {string} message - The message to send.
1941
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
1942
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
1943
+ *
1944
+ */
1618
1945
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1619
1946
  this.log.debug('Sending a snackbar message to all connected clients');
1947
+ // Send the message to all connected clients
1620
1948
  this.webSocketServer?.clients.forEach((client) => {
1621
1949
  if (client.readyState === WebSocket.OPEN) {
1622
1950
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1623
1951
  }
1624
1952
  });
1625
1953
  }
1954
+ /**
1955
+ * Sends a close snackbar message to all connected clients.
1956
+ * @param {string} message - The message to send.
1957
+ *
1958
+ */
1626
1959
  wssSendCloseSnackbarMessage(message) {
1627
1960
  this.log.debug('Sending a close snackbar message to all connected clients');
1961
+ // Send the message to all connected clients
1628
1962
  this.webSocketServer?.clients.forEach((client) => {
1629
1963
  if (client.readyState === WebSocket.OPEN) {
1630
1964
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1631
1965
  }
1632
1966
  });
1633
1967
  }
1968
+ /**
1969
+ * Sends an attribute update message to all connected WebSocket clients.
1970
+ *
1971
+ * @param {string | undefined} plugin - The name of the plugin.
1972
+ * @param {string | undefined} serialNumber - The serial number of the device.
1973
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
1974
+ * @param {string} cluster - The cluster name where the attribute belongs.
1975
+ * @param {string} attribute - The name of the attribute that changed.
1976
+ * @param {number | string | boolean} value - The new value of the attribute.
1977
+ *
1978
+ * @remarks
1979
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
1980
+ * with the updated attribute information.
1981
+ */
1634
1982
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1635
1983
  this.log.debug('Sending an attribute update message to all connected clients');
1984
+ // Send the message to all connected clients
1636
1985
  this.webSocketServer?.clients.forEach((client) => {
1637
1986
  if (client.readyState === WebSocket.OPEN) {
1638
1987
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1639
1988
  }
1640
1989
  });
1641
1990
  }
1991
+ /**
1992
+ * Sends a message to all connected clients.
1993
+ * @param {number} id - The message id.
1994
+ * @param {string} method - The message method.
1995
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
1996
+ *
1997
+ */
1642
1998
  wssBroadcastMessage(id, method, params) {
1643
1999
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2000
+ // Send the message to all connected clients
1644
2001
  this.webSocketServer?.clients.forEach((client) => {
1645
2002
  if (client.readyState === WebSocket.OPEN) {
1646
2003
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1648,3 +2005,4 @@ export class Frontend {
1648
2005
  });
1649
2006
  }
1650
2007
  }
2008
+ //# sourceMappingURL=frontend.js.map