matterbridge 3.0.7-dev-20250618-fb768ee → 3.0.7

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