matterbridge 3.1.1-dev-20250703-80c685d → 3.1.1

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 (208) hide show
  1. package/CHANGELOG.md +7 -5
  2. package/README.md +5 -25
  3. package/dist/cli.d.ts +26 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +93 -6
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cliEmitter.d.ts +34 -0
  8. package/dist/cliEmitter.d.ts.map +1 -0
  9. package/dist/cliEmitter.js +36 -0
  10. package/dist/cliEmitter.js.map +1 -0
  11. package/dist/clusters/export.d.ts +2 -0
  12. package/dist/clusters/export.d.ts.map +1 -0
  13. package/dist/clusters/export.js +2 -0
  14. package/dist/clusters/export.js.map +1 -0
  15. package/dist/defaultConfigSchema.d.ts +28 -0
  16. package/dist/defaultConfigSchema.d.ts.map +1 -0
  17. package/dist/defaultConfigSchema.js +24 -0
  18. package/dist/defaultConfigSchema.js.map +1 -0
  19. package/dist/deviceManager.d.ts +112 -0
  20. package/dist/deviceManager.d.ts.map +1 -0
  21. package/dist/deviceManager.js +94 -1
  22. package/dist/deviceManager.js.map +1 -0
  23. package/dist/devices/batteryStorage.d.ts +48 -0
  24. package/dist/devices/batteryStorage.d.ts.map +1 -0
  25. package/dist/devices/batteryStorage.js +48 -1
  26. package/dist/devices/batteryStorage.js.map +1 -0
  27. package/dist/devices/evse.d.ts +75 -0
  28. package/dist/devices/evse.d.ts.map +1 -0
  29. package/dist/devices/evse.js +74 -10
  30. package/dist/devices/evse.js.map +1 -0
  31. package/dist/devices/export.d.ts +9 -0
  32. package/dist/devices/export.d.ts.map +1 -0
  33. package/dist/devices/export.js +2 -0
  34. package/dist/devices/export.js.map +1 -0
  35. package/dist/devices/heatPump.d.ts +47 -0
  36. package/dist/devices/heatPump.d.ts.map +1 -0
  37. package/dist/devices/heatPump.js +50 -2
  38. package/dist/devices/heatPump.js.map +1 -0
  39. package/dist/devices/laundryDryer.d.ts +87 -0
  40. package/dist/devices/laundryDryer.d.ts.map +1 -0
  41. package/dist/devices/laundryDryer.js +83 -6
  42. package/dist/devices/laundryDryer.js.map +1 -0
  43. package/dist/devices/laundryWasher.d.ts +242 -0
  44. package/dist/devices/laundryWasher.d.ts.map +1 -0
  45. package/dist/devices/laundryWasher.js +91 -7
  46. package/dist/devices/laundryWasher.js.map +1 -0
  47. package/dist/devices/roboticVacuumCleaner.d.ts +103 -0
  48. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  49. package/dist/devices/roboticVacuumCleaner.js +82 -6
  50. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  51. package/dist/devices/solarPower.d.ts +40 -0
  52. package/dist/devices/solarPower.d.ts.map +1 -0
  53. package/dist/devices/solarPower.js +38 -0
  54. package/dist/devices/solarPower.js.map +1 -0
  55. package/dist/devices/waterHeater.d.ts +111 -0
  56. package/dist/devices/waterHeater.d.ts.map +1 -0
  57. package/dist/devices/waterHeater.js +82 -2
  58. package/dist/devices/waterHeater.js.map +1 -0
  59. package/dist/frontend.d.ts +302 -0
  60. package/dist/frontend.d.ts.map +1 -0
  61. package/dist/frontend.js +455 -27
  62. package/dist/frontend.js.map +1 -0
  63. package/dist/globalMatterbridge.d.ts +59 -0
  64. package/dist/globalMatterbridge.d.ts.map +1 -0
  65. package/dist/globalMatterbridge.js +47 -0
  66. package/dist/globalMatterbridge.js.map +1 -0
  67. package/dist/helpers.d.ts +48 -0
  68. package/dist/helpers.d.ts.map +1 -0
  69. package/dist/helpers.js +53 -0
  70. package/dist/helpers.js.map +1 -0
  71. package/dist/index.d.ts +41 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +31 -1
  74. package/dist/index.js.map +1 -0
  75. package/dist/logger/export.d.ts +2 -0
  76. package/dist/logger/export.d.ts.map +1 -0
  77. package/dist/logger/export.js +1 -0
  78. package/dist/logger/export.js.map +1 -0
  79. package/dist/matter/behaviors.d.ts +2 -0
  80. package/dist/matter/behaviors.d.ts.map +1 -0
  81. package/dist/matter/behaviors.js +2 -0
  82. package/dist/matter/behaviors.js.map +1 -0
  83. package/dist/matter/clusters.d.ts +2 -0
  84. package/dist/matter/clusters.d.ts.map +1 -0
  85. package/dist/matter/clusters.js +2 -0
  86. package/dist/matter/clusters.js.map +1 -0
  87. package/dist/matter/devices.d.ts +2 -0
  88. package/dist/matter/devices.d.ts.map +1 -0
  89. package/dist/matter/devices.js +2 -0
  90. package/dist/matter/devices.js.map +1 -0
  91. package/dist/matter/endpoints.d.ts +2 -0
  92. package/dist/matter/endpoints.d.ts.map +1 -0
  93. package/dist/matter/endpoints.js +2 -0
  94. package/dist/matter/endpoints.js.map +1 -0
  95. package/dist/matter/export.d.ts +5 -0
  96. package/dist/matter/export.d.ts.map +1 -0
  97. package/dist/matter/export.js +3 -0
  98. package/dist/matter/export.js.map +1 -0
  99. package/dist/matter/types.d.ts +3 -0
  100. package/dist/matter/types.d.ts.map +1 -0
  101. package/dist/matter/types.js +3 -0
  102. package/dist/matter/types.js.map +1 -0
  103. package/dist/matterbridge.d.ts +450 -0
  104. package/dist/matterbridge.d.ts.map +1 -0
  105. package/dist/matterbridge.js +819 -68
  106. package/dist/matterbridge.js.map +1 -0
  107. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  108. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  109. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  110. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  111. package/dist/matterbridgeBehaviors.d.ts +1340 -0
  112. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  113. package/dist/matterbridgeBehaviors.js +61 -1
  114. package/dist/matterbridgeBehaviors.js.map +1 -0
  115. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  116. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  117. package/dist/matterbridgeDeviceTypes.js +579 -15
  118. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  119. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  120. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  121. package/dist/matterbridgeDynamicPlatform.js +36 -0
  122. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  123. package/dist/matterbridgeEndpoint.d.ts +1179 -0
  124. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  125. package/dist/matterbridgeEndpoint.js +1027 -42
  126. package/dist/matterbridgeEndpoint.js.map +1 -0
  127. package/dist/matterbridgeEndpointHelpers.d.ts +3198 -0
  128. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  129. package/dist/matterbridgeEndpointHelpers.js +322 -12
  130. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  131. package/dist/matterbridgePlatform.d.ts +310 -0
  132. package/dist/matterbridgePlatform.d.ts.map +1 -0
  133. package/dist/matterbridgePlatform.js +233 -0
  134. package/dist/matterbridgePlatform.js.map +1 -0
  135. package/dist/matterbridgeTypes.d.ts +192 -0
  136. package/dist/matterbridgeTypes.d.ts.map +1 -0
  137. package/dist/matterbridgeTypes.js +25 -0
  138. package/dist/matterbridgeTypes.js.map +1 -0
  139. package/dist/pluginManager.d.ts +291 -0
  140. package/dist/pluginManager.d.ts.map +1 -0
  141. package/dist/pluginManager.js +269 -5
  142. package/dist/pluginManager.js.map +1 -0
  143. package/dist/shelly.d.ts +174 -0
  144. package/dist/shelly.d.ts.map +1 -0
  145. package/dist/shelly.js +168 -7
  146. package/dist/shelly.js.map +1 -0
  147. package/dist/storage/export.d.ts +2 -0
  148. package/dist/storage/export.d.ts.map +1 -0
  149. package/dist/storage/export.js +1 -0
  150. package/dist/storage/export.js.map +1 -0
  151. package/dist/update.d.ts +59 -0
  152. package/dist/update.d.ts.map +1 -0
  153. package/dist/update.js +54 -0
  154. package/dist/update.js.map +1 -0
  155. package/dist/utils/colorUtils.d.ts +117 -0
  156. package/dist/utils/colorUtils.d.ts.map +1 -0
  157. package/dist/utils/colorUtils.js +263 -2
  158. package/dist/utils/colorUtils.js.map +1 -0
  159. package/dist/utils/commandLine.d.ts +59 -0
  160. package/dist/utils/commandLine.d.ts.map +1 -0
  161. package/dist/utils/commandLine.js +54 -0
  162. package/dist/utils/commandLine.js.map +1 -0
  163. package/dist/utils/copyDirectory.d.ts +33 -0
  164. package/dist/utils/copyDirectory.d.ts.map +1 -0
  165. package/dist/utils/copyDirectory.js +38 -1
  166. package/dist/utils/copyDirectory.js.map +1 -0
  167. package/dist/utils/createDirectory.d.ts +34 -0
  168. package/dist/utils/createDirectory.d.ts.map +1 -0
  169. package/dist/utils/createDirectory.js +33 -0
  170. package/dist/utils/createDirectory.js.map +1 -0
  171. package/dist/utils/createZip.d.ts +39 -0
  172. package/dist/utils/createZip.d.ts.map +1 -0
  173. package/dist/utils/createZip.js +47 -2
  174. package/dist/utils/createZip.js.map +1 -0
  175. package/dist/utils/deepCopy.d.ts +32 -0
  176. package/dist/utils/deepCopy.d.ts.map +1 -0
  177. package/dist/utils/deepCopy.js +39 -0
  178. package/dist/utils/deepCopy.js.map +1 -0
  179. package/dist/utils/deepEqual.d.ts +54 -0
  180. package/dist/utils/deepEqual.d.ts.map +1 -0
  181. package/dist/utils/deepEqual.js +72 -1
  182. package/dist/utils/deepEqual.js.map +1 -0
  183. package/dist/utils/export.d.ts +12 -0
  184. package/dist/utils/export.d.ts.map +1 -0
  185. package/dist/utils/export.js +1 -0
  186. package/dist/utils/export.js.map +1 -0
  187. package/dist/utils/hex.d.ts +49 -0
  188. package/dist/utils/hex.d.ts.map +1 -0
  189. package/dist/utils/hex.js +58 -0
  190. package/dist/utils/hex.js.map +1 -0
  191. package/dist/utils/isvalid.d.ts +103 -0
  192. package/dist/utils/isvalid.d.ts.map +1 -0
  193. package/dist/utils/isvalid.js +101 -0
  194. package/dist/utils/isvalid.js.map +1 -0
  195. package/dist/utils/network.d.ts +76 -0
  196. package/dist/utils/network.d.ts.map +1 -0
  197. package/dist/utils/network.js +83 -5
  198. package/dist/utils/network.js.map +1 -0
  199. package/dist/utils/spawn.d.ts +11 -0
  200. package/dist/utils/spawn.d.ts.map +1 -0
  201. package/dist/utils/spawn.js +79 -63
  202. package/dist/utils/spawn.js.map +1 -0
  203. package/dist/utils/wait.d.ts +56 -0
  204. package/dist/utils/wait.d.ts.map +1 -0
  205. package/dist/utils/wait.js +62 -9
  206. package/dist/utils/wait.js.map +1 -0
  207. package/npm-shrinkwrap.json +6 -6
  208. package/package.json +3 -2
package/dist/frontend.js CHANGED
@@ -1,31 +1,128 @@
1
+ /**
2
+ * This file contains the class Frontend.
3
+ *
4
+ * @file frontend.ts
5
+ * @author Luca Liguori
6
+ * @created 2025-01-13
7
+ * @version 1.1.0
8
+ * @license Apache-2.0
9
+ *
10
+ * Copyright 2025, 2026, 2027 Luca Liguori.
11
+ *
12
+ * Licensed under the Apache License, Version 2.0 (the "License");
13
+ * you may not use this file except in compliance with the License.
14
+ * You may obtain a copy of the License at
15
+ *
16
+ * http://www.apache.org/licenses/LICENSE-2.0
17
+ *
18
+ * Unless required by applicable law or agreed to in writing, software
19
+ * distributed under the License is distributed on an "AS IS" BASIS,
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ * See the License for the specific language governing permissions and
22
+ * limitations under the License.
23
+ */
24
+ // Node modules
1
25
  import { createServer } from 'node:http';
2
26
  import https from 'node:https';
3
27
  import os from 'node:os';
4
28
  import path from 'node:path';
5
29
  import { promises as fs } from 'node:fs';
30
+ import EventEmitter from 'node:events';
31
+ // Third-party modules
6
32
  import express from 'express';
7
33
  import WebSocket, { WebSocketServer } from 'ws';
8
34
  import multer from 'multer';
35
+ // AnsiLogger module
9
36
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from 'node-ansi-logger';
37
+ // @matter
10
38
  import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
11
39
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
40
+ // Matterbridge
12
41
  import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
13
42
  import { plg } from './matterbridgeTypes.js';
14
43
  import { capitalizeFirstLetter } from './matterbridgeEndpointHelpers.js';
15
- import spawn from './utils/spawn.js';
44
+ import { cliEmitter, lastCpuUsage } from './cliEmitter.js';
45
+ /**
46
+ * Websocket message ID for logging.
47
+ *
48
+ * @constant {number}
49
+ */
16
50
  export const WS_ID_LOG = 0;
51
+ /**
52
+ * Websocket message ID indicating a refresh is needed.
53
+ *
54
+ * @constant {number}
55
+ */
17
56
  export const WS_ID_REFRESH_NEEDED = 1;
57
+ /**
58
+ * Websocket message ID indicating a restart is needed.
59
+ *
60
+ * @constant {number}
61
+ */
18
62
  export const WS_ID_RESTART_NEEDED = 2;
63
+ /**
64
+ * Websocket message ID indicating a cpu update.
65
+ *
66
+ * @constant {number}
67
+ */
19
68
  export const WS_ID_CPU_UPDATE = 3;
69
+ /**
70
+ * Websocket message ID indicating a memory update.
71
+ *
72
+ * @constant {number}
73
+ */
20
74
  export const WS_ID_MEMORY_UPDATE = 4;
75
+ /**
76
+ * Websocket message ID indicating an uptime update.
77
+ *
78
+ * @constant {number}
79
+ */
21
80
  export const WS_ID_UPTIME_UPDATE = 5;
81
+ /**
82
+ * Websocket message ID indicating a snackbar message.
83
+ *
84
+ * @constant {number}
85
+ */
22
86
  export const WS_ID_SNACKBAR = 6;
87
+ /**
88
+ * Websocket message ID indicating matterbridge has un update available.
89
+ *
90
+ * @constant {number}
91
+ */
23
92
  export const WS_ID_UPDATE_NEEDED = 7;
93
+ /**
94
+ * Websocket message ID indicating a state update.
95
+ *
96
+ * @constant {number}
97
+ */
24
98
  export const WS_ID_STATEUPDATE = 8;
99
+ /**
100
+ * Websocket message ID indicating to close a permanent snackbar message.
101
+ *
102
+ * @constant {number}
103
+ */
25
104
  export const WS_ID_CLOSE_SNACKBAR = 9;
105
+ /**
106
+ * Websocket message ID indicating a shelly system update.
107
+ * check:
108
+ * curl -k http://127.0.0.1:8101/api/updates/sys/check
109
+ * perform:
110
+ * curl -k http://127.0.0.1:8101/api/updates/sys/perform
111
+ *
112
+ * @constant {number}
113
+ */
26
114
  export const WS_ID_SHELLY_SYS_UPDATE = 100;
115
+ /**
116
+ * Websocket message ID indicating a shelly main update.
117
+ * check:
118
+ * curl -k http://127.0.0.1:8101/api/updates/main/check
119
+ * perform:
120
+ * curl -k http://127.0.0.1:8101/api/updates/main/perform
121
+ *
122
+ * @constant {number}
123
+ */
27
124
  export const WS_ID_SHELLY_MAIN_UPDATE = 101;
28
- export class Frontend {
125
+ export class Frontend extends EventEmitter {
29
126
  matterbridge;
30
127
  log;
31
128
  port = 8283;
@@ -35,8 +132,9 @@ export class Frontend {
35
132
  httpsServer;
36
133
  webSocketServer;
37
134
  constructor(matterbridge) {
135
+ super();
38
136
  this.matterbridge = matterbridge;
39
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
137
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
40
138
  }
41
139
  set logLevel(logLevel) {
42
140
  this.log.logLevel = logLevel;
@@ -44,16 +142,54 @@ export class Frontend {
44
142
  async start(port = 8283) {
45
143
  this.port = port;
46
144
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
145
+ // Initialize multer with the upload directory
47
146
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
48
147
  await fs.mkdir(uploadDir, { recursive: true });
49
148
  const upload = multer({ dest: uploadDir });
149
+ // Create the express app that serves the frontend
50
150
  this.expressApp = express();
151
+ // Inject logging/debug wrapper for route/middleware registration
152
+ /*
153
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
154
+ for (const method of methods) {
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
159
+ try {
160
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
161
+ return original(path, ...rest);
162
+ } catch (err) {
163
+ console.error(`[ERROR] Failed to register route: ${path}`);
164
+ throw err;
165
+ }
166
+ };
167
+ }
168
+ */
169
+ // Log all requests to the server for debugging
170
+ /*
171
+ this.expressApp.use((req, res, next) => {
172
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
173
+ next();
174
+ });
175
+ */
176
+ // Serve static files from '/static' endpoint
51
177
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
52
178
  if (!hasParameter('ssl')) {
53
- this.httpServer = createServer(this.expressApp);
179
+ // Create an HTTP server and attach the express app
180
+ try {
181
+ this.httpServer = createServer(this.expressApp);
182
+ }
183
+ catch (error) {
184
+ this.log.error(`Failed to create HTTP server: ${error}`);
185
+ this.emit('server_error', error);
186
+ return;
187
+ }
188
+ // Listen on the specified port
54
189
  if (hasParameter('ingress')) {
55
190
  this.httpServer.listen(this.port, '0.0.0.0', () => {
56
191
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
192
+ this.emit('server_listening', 'http', this.port, '0.0.0.0');
57
193
  });
58
194
  }
59
195
  else {
@@ -62,6 +198,7 @@ export class Frontend {
62
198
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
63
199
  if (this.matterbridge.systemInformation.ipv6Address !== '')
64
200
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
201
+ this.emit('server_listening', 'http', this.port);
65
202
  });
66
203
  }
67
204
  this.httpServer.on('error', (error) => {
@@ -75,10 +212,12 @@ export class Frontend {
75
212
  break;
76
213
  }
77
214
  this.initializeError = true;
215
+ this.emit('server_error', error);
78
216
  return;
79
217
  });
80
218
  }
81
219
  else {
220
+ // Load the SSL certificate, the private key and optionally the CA certificate
82
221
  let cert;
83
222
  try {
84
223
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
@@ -86,6 +225,7 @@ export class Frontend {
86
225
  }
87
226
  catch (error) {
88
227
  this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
228
+ this.emit('server_error', error);
89
229
  return;
90
230
  }
91
231
  let key;
@@ -95,6 +235,7 @@ export class Frontend {
95
235
  }
96
236
  catch (error) {
97
237
  this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
238
+ this.emit('server_error', error);
98
239
  return;
99
240
  }
100
241
  let ca;
@@ -106,10 +247,20 @@ export class Frontend {
106
247
  this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
107
248
  }
108
249
  const serverOptions = { cert, key, ca };
109
- this.httpsServer = https.createServer(serverOptions, this.expressApp);
250
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
251
+ try {
252
+ this.httpsServer = https.createServer(serverOptions, this.expressApp);
253
+ }
254
+ catch (error) {
255
+ this.log.error(`Failed to create HTTPS server: ${error}`);
256
+ this.emit('server_error', error);
257
+ return;
258
+ }
259
+ // Listen on the specified port
110
260
  if (hasParameter('ingress')) {
111
261
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
112
262
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
263
+ this.emit('server_listening', 'https', this.port, '0.0.0.0');
113
264
  });
114
265
  }
115
266
  else {
@@ -118,6 +269,7 @@ export class Frontend {
118
269
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
119
270
  if (this.matterbridge.systemInformation.ipv6Address !== '')
120
271
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
272
+ this.emit('server_listening', 'https', this.port);
121
273
  });
122
274
  }
123
275
  this.httpsServer.on('error', (error) => {
@@ -131,21 +283,24 @@ export class Frontend {
131
283
  break;
132
284
  }
133
285
  this.initializeError = true;
286
+ this.emit('server_error', error);
134
287
  return;
135
288
  });
136
289
  }
137
290
  if (this.initializeError)
138
291
  return;
292
+ // Create a WebSocket server and attach it to the http or https server
139
293
  const wssPort = this.port;
140
294
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
141
295
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
142
296
  this.webSocketServer.on('connection', (ws, request) => {
143
297
  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";
298
+ // Set the global logger callback for the WebSocketServer
299
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
300
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
301
+ callbackLogLevel = "info" /* LogLevel.INFO */;
302
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
303
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
149
304
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
150
305
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
151
306
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -175,11 +330,12 @@ export class Frontend {
175
330
  });
176
331
  this.webSocketServer.on('listening', () => {
177
332
  this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
333
+ this.emit('websocket_server_listening', wssHost);
178
334
  });
179
335
  this.webSocketServer.on('error', (ws, error) => {
180
336
  this.log.error(`WebSocketServer error: ${error}`);
181
337
  });
182
- const { cliEmitter } = await import('./cli.js');
338
+ // Subscribe to cli events
183
339
  cliEmitter.removeAllListeners();
184
340
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
185
341
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -190,6 +346,8 @@ export class Frontend {
190
346
  cliEmitter.on('cpu', (cpuUsage) => {
191
347
  this.wssSendCpuUpdate(cpuUsage);
192
348
  });
349
+ // Endpoint to validate login code
350
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
193
351
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
194
352
  const { password } = req.body;
195
353
  this.log.debug('The frontend sent /api/login', password);
@@ -208,23 +366,27 @@ export class Frontend {
208
366
  this.log.warn('/api/login error wrong password');
209
367
  res.json({ valid: false });
210
368
  }
369
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
211
370
  }
212
371
  catch (error) {
213
372
  this.log.error('/api/login error getting password');
214
373
  res.json({ valid: false });
215
374
  }
216
375
  });
376
+ // Endpoint to provide health check for docker
217
377
  this.expressApp.get('/health', (req, res) => {
218
378
  this.log.debug('Express received /health');
219
379
  const healthStatus = {
220
- status: 'ok',
221
- uptime: process.uptime(),
222
- timestamp: new Date().toISOString(),
380
+ status: 'ok', // Indicate service is healthy
381
+ uptime: process.uptime(), // Server uptime in seconds
382
+ timestamp: new Date().toISOString(), // Current timestamp
223
383
  };
224
384
  res.status(200).json(healthStatus);
225
385
  });
386
+ // Endpoint to provide memory usage details
226
387
  this.expressApp.get('/memory', async (req, res) => {
227
388
  this.log.debug('Express received /memory');
389
+ // Memory usage from process
228
390
  const memoryUsageRaw = process.memoryUsage();
229
391
  const memoryUsage = {
230
392
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -233,10 +395,13 @@ export class Frontend {
233
395
  external: this.formatMemoryUsage(memoryUsageRaw.external),
234
396
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
235
397
  };
398
+ // V8 heap statistics
236
399
  const { default: v8 } = await import('node:v8');
237
400
  const heapStatsRaw = v8.getHeapStatistics();
238
401
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
402
+ // Format heapStats
239
403
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
404
+ // Format heapSpaces
240
405
  const heapSpaces = heapSpacesRaw.map((space) => ({
241
406
  ...space,
242
407
  space_size: this.formatMemoryUsage(space.space_size),
@@ -254,19 +419,23 @@ export class Frontend {
254
419
  };
255
420
  res.status(200).json(memoryReport);
256
421
  });
422
+ // Endpoint to provide settings
257
423
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
258
424
  this.log.debug('The frontend sent /api/settings');
259
425
  res.json(await this.getApiSettings());
260
426
  });
427
+ // Endpoint to provide plugins
261
428
  this.expressApp.get('/api/plugins', async (req, res) => {
262
429
  this.log.debug('The frontend sent /api/plugins');
263
430
  res.json(this.getBaseRegisteredPlugins());
264
431
  });
432
+ // Endpoint to provide devices
265
433
  this.expressApp.get('/api/devices', async (req, res) => {
266
434
  this.log.debug('The frontend sent /api/devices');
267
435
  const devices = await this.getDevices();
268
436
  res.json(devices);
269
437
  });
438
+ // Endpoint to view the matterbridge log
270
439
  this.expressApp.get('/api/view-mblog', async (req, res) => {
271
440
  this.log.debug('The frontend sent /api/view-mblog');
272
441
  try {
@@ -279,6 +448,7 @@ export class Frontend {
279
448
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
280
449
  }
281
450
  });
451
+ // Endpoint to view the matter.js log
282
452
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
283
453
  this.log.debug('The frontend sent /api/view-mjlog');
284
454
  try {
@@ -291,6 +461,7 @@ export class Frontend {
291
461
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
292
462
  }
293
463
  });
464
+ // Endpoint to view the shelly log
294
465
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
295
466
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
296
467
  try {
@@ -303,9 +474,11 @@ export class Frontend {
303
474
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
304
475
  }
305
476
  });
477
+ // Endpoint to download the matterbridge log
306
478
  this.expressApp.get('/api/download-mblog', async (req, res) => {
307
479
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
308
480
  try {
481
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
309
482
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
310
483
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
311
484
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), data, 'utf-8');
@@ -315,16 +488,20 @@ export class Frontend {
315
488
  this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
316
489
  }
317
490
  res.type('text/plain');
491
+ // res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
318
492
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
493
+ /* istanbul ignore if */
319
494
  if (error) {
320
495
  this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
321
496
  res.status(500).send('Error downloading the matterbridge log file');
322
497
  }
323
498
  });
324
499
  });
500
+ // Endpoint to download the matter log
325
501
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
326
502
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
327
503
  try {
504
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
328
505
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
329
506
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
330
507
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
@@ -335,15 +512,18 @@ export class Frontend {
335
512
  }
336
513
  res.type('text/plain');
337
514
  res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
515
+ /* istanbul ignore if */
338
516
  if (error) {
339
517
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
340
518
  res.status(500).send('Error downloading the matter log file');
341
519
  }
342
520
  });
343
521
  });
522
+ // Endpoint to download the shelly log
344
523
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
345
524
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
346
525
  try {
526
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
347
527
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
348
528
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
349
529
  await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
@@ -354,12 +534,14 @@ export class Frontend {
354
534
  }
355
535
  res.type('text/plain');
356
536
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
537
+ /* istanbul ignore if */
357
538
  if (error) {
358
539
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
359
540
  res.status(500).send('Error downloading Shelly system log file');
360
541
  }
361
542
  });
362
543
  });
544
+ // Endpoint to download the matterbridge storage directory
363
545
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
364
546
  this.log.debug('The frontend sent /api/download-mbstorage');
365
547
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
@@ -370,6 +552,7 @@ export class Frontend {
370
552
  }
371
553
  });
372
554
  });
555
+ // Endpoint to download the matter storage file
373
556
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
374
557
  this.log.debug('The frontend sent /api/download-mjstorage');
375
558
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
@@ -380,6 +563,7 @@ export class Frontend {
380
563
  }
381
564
  });
382
565
  });
566
+ // Endpoint to download the matterbridge plugin directory
383
567
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
384
568
  this.log.debug('The frontend sent /api/download-pluginstorage');
385
569
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
@@ -390,6 +574,7 @@ export class Frontend {
390
574
  }
391
575
  });
392
576
  });
577
+ // Endpoint to download the matterbridge plugin config files
393
578
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
394
579
  this.log.debug('The frontend sent /api/download-pluginconfig');
395
580
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
@@ -400,6 +585,7 @@ export class Frontend {
400
585
  }
401
586
  });
402
587
  });
588
+ // Endpoint to download the matterbridge backup (created with the backup command)
403
589
  this.expressApp.get('/api/download-backup', async (req, res) => {
404
590
  this.log.debug('The frontend sent /api/download-backup');
405
591
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
@@ -409,6 +595,7 @@ export class Frontend {
409
595
  }
410
596
  });
411
597
  });
598
+ // Endpoint to upload a package
412
599
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
413
600
  const { filename } = req.body;
414
601
  const file = req.file;
@@ -418,12 +605,16 @@ export class Frontend {
418
605
  return;
419
606
  }
420
607
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
608
+ // Define the path where the plugin file will be saved
421
609
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
422
610
  try {
611
+ // Move the uploaded file to the specified path
423
612
  await fs.rename(file.path, filePath);
424
613
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
614
+ // Install the plugin package
425
615
  if (filename.endsWith('.tgz')) {
426
- await spawn.spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
616
+ const { spawnCommand } = await import('./utils/spawn.js');
617
+ await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
427
618
  this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
428
619
  this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
429
620
  this.wssSendSnackbarMessage(`Installed package ${filename}`, 10, 'success');
@@ -440,6 +631,7 @@ export class Frontend {
440
631
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
441
632
  }
442
633
  });
634
+ // Fallback for routing (must be the last route)
443
635
  this.expressApp.use((req, res) => {
444
636
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
445
637
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -448,12 +640,15 @@ export class Frontend {
448
640
  }
449
641
  async stop() {
450
642
  this.log.debug('Stopping the frontend...');
643
+ // Remove listeners from the express app
451
644
  if (this.expressApp) {
452
645
  this.expressApp.removeAllListeners();
453
646
  this.expressApp = undefined;
454
647
  this.log.debug('Frontend app closed successfully');
455
648
  }
649
+ // Close the WebSocket server
456
650
  if (this.webSocketServer) {
651
+ // Close all active connections
457
652
  this.webSocketServer.clients.forEach((client) => {
458
653
  if (client.readyState === WebSocket.OPEN) {
459
654
  client.close();
@@ -473,6 +668,7 @@ export class Frontend {
473
668
  this.webSocketServer.removeAllListeners();
474
669
  this.webSocketServer = undefined;
475
670
  }
671
+ // Close the http server
476
672
  if (this.httpServer) {
477
673
  await withTimeout(new Promise((resolve) => {
478
674
  this.httpServer?.close((error) => {
@@ -489,6 +685,7 @@ export class Frontend {
489
685
  this.httpServer = undefined;
490
686
  this.log.debug('Frontend http server closed successfully');
491
687
  }
688
+ // Close the https server
492
689
  if (this.httpsServer) {
493
690
  await withTimeout(new Promise((resolve) => {
494
691
  this.httpsServer?.close((error) => {
@@ -507,6 +704,7 @@ export class Frontend {
507
704
  }
508
705
  this.log.debug('Frontend stopped successfully');
509
706
  }
707
+ // Function to format bytes to KB, MB, or GB
510
708
  formatMemoryUsage = (bytes) => {
511
709
  if (bytes >= 1024 ** 3) {
512
710
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -518,6 +716,7 @@ export class Frontend {
518
716
  return `${(bytes / 1024).toFixed(2)} KB`;
519
717
  }
520
718
  };
719
+ // Function to format system uptime with only the most significant unit
521
720
  formatOsUpTime = (seconds) => {
522
721
  if (seconds >= 86400) {
523
722
  const days = Math.floor(seconds / 86400);
@@ -533,8 +732,13 @@ export class Frontend {
533
732
  }
534
733
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
535
734
  };
735
+ /**
736
+ * Retrieves the api settings data.
737
+ *
738
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
739
+ */
536
740
  async getApiSettings() {
537
- const { lastCpuUsage } = await import('./cli.js');
741
+ // Update the system information
538
742
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
539
743
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
540
744
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -543,6 +747,7 @@ export class Frontend {
543
747
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
544
748
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
545
749
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
750
+ // Update the matterbridge information
546
751
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
547
752
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
548
753
  this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
@@ -561,6 +766,12 @@ export class Frontend {
561
766
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
562
767
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
563
768
  }
769
+ /**
770
+ * Retrieves the reachable attribute.
771
+ *
772
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
773
+ * @returns {boolean} The reachable attribute.
774
+ */
564
775
  getReachability(device) {
565
776
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
566
777
  return false;
@@ -572,6 +783,12 @@ export class Frontend {
572
783
  return true;
573
784
  return false;
574
785
  }
786
+ /**
787
+ * Retrieves the power source attribute.
788
+ *
789
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice object.
790
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
791
+ */
575
792
  getPowerSource(endpoint) {
576
793
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
577
794
  return undefined;
@@ -587,13 +804,21 @@ export class Frontend {
587
804
  }
588
805
  return;
589
806
  };
807
+ // Root endpoint
590
808
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
591
809
  return powerSource(endpoint);
810
+ // Child endpoints
592
811
  for (const child of endpoint.getChildEndpoints()) {
593
812
  if (child.hasClusterServer(PowerSource.Cluster.id))
594
813
  return powerSource(child);
595
814
  }
596
815
  }
816
+ /**
817
+ * Retrieves the matter pairing code from a given device.
818
+ *
819
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object to retrieve the QR pairing code from.
820
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
821
+ */
597
822
  getMatterDataFromDevice(device) {
598
823
  if (device.mode === 'server' && device.serverNode && device.serverContext) {
599
824
  return {
@@ -605,6 +830,12 @@ export class Frontend {
605
830
  };
606
831
  }
607
832
  }
833
+ /**
834
+ * Retrieves the cluster text description from a given device.
835
+ *
836
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
837
+ * @returns {string} The attributes description of the cluster servers in the device.
838
+ */
608
839
  getClusterTextFromDevice(device) {
609
840
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
610
841
  return '';
@@ -645,7 +876,19 @@ export class Frontend {
645
876
  };
646
877
  let attributes = '';
647
878
  let supportedModes = [];
879
+ /*
880
+ Object.keys(device.behaviors.supported).forEach((clusterName) => {
881
+ const clusterBehavior = device.behaviors.supported[lowercaseFirstLetter(clusterName)] as ClusterBehavior.Type | undefined;
882
+ // console.log(`Device: ${device.deviceName} => Cluster: ${clusterName} Behavior: ${clusterBehavior?.id}`, clusterBehavior);
883
+ if (clusterBehavior && clusterBehavior.cluster && clusterBehavior.cluster.attributes) {
884
+ Object.entries(clusterBehavior.cluster.attributes).forEach(([attributeName, attribute]) => {
885
+ // console.log(`${device.deviceName} => Cluster: ${clusterName} Attribute: ${attributeName}`, attribute);
886
+ });
887
+ }
888
+ });
889
+ */
648
890
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
891
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
649
892
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
650
893
  return;
651
894
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -737,8 +980,14 @@ export class Frontend {
737
980
  if (clusterName === 'userLabel' && attributeName === 'labelList')
738
981
  attributes += `${getUserLabel(device)} `;
739
982
  });
983
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
740
984
  return attributes.trimStart().trimEnd();
741
985
  }
986
+ /**
987
+ * Retrieves the base registered plugins sanitized for res.json().
988
+ *
989
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
990
+ */
742
991
  getBaseRegisteredPlugins() {
743
992
  const baseRegisteredPlugins = [];
744
993
  for (const plugin of this.matterbridge.plugins) {
@@ -777,11 +1026,19 @@ export class Frontend {
777
1026
  }
778
1027
  return baseRegisteredPlugins;
779
1028
  }
1029
+ /**
1030
+ * Retrieves the devices from Matterbridge.
1031
+ *
1032
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1033
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1034
+ */
780
1035
  async getDevices(pluginName) {
781
1036
  const devices = [];
782
1037
  for (const device of this.matterbridge.devices.array()) {
1038
+ // Filter by pluginName if provided
783
1039
  if (pluginName && pluginName !== device.plugin)
784
1040
  continue;
1041
+ // Check if the device has the required properties
785
1042
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
786
1043
  continue;
787
1044
  devices.push({
@@ -801,22 +1058,37 @@ export class Frontend {
801
1058
  }
802
1059
  return devices;
803
1060
  }
1061
+ /**
1062
+ * Retrieves the clusters from a given plugin and endpoint number.
1063
+ *
1064
+ * Response for /api/clusters
1065
+ *
1066
+ * @param {string} pluginName - The name of the plugin.
1067
+ * @param {number} endpointNumber - The endpoint number.
1068
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1069
+ */
804
1070
  getClusters(pluginName, endpointNumber) {
805
1071
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
806
1072
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
807
1073
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
808
1074
  return;
809
1075
  }
1076
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1077
+ // Get the device types from the main endpoint
810
1078
  const deviceTypes = [];
811
1079
  const clusters = [];
812
1080
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
813
1081
  deviceTypes.push(d.deviceType);
814
1082
  });
1083
+ // Get the clusters from the main endpoint
815
1084
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
816
1085
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
817
1086
  return;
818
1087
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
819
1088
  return;
1089
+ // console.log(
1090
+ // `${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}`,
1091
+ // );
820
1092
  clusters.push({
821
1093
  endpoint: endpoint.number.toString(),
822
1094
  id: 'main',
@@ -829,12 +1101,18 @@ export class Frontend {
829
1101
  attributeLocalValue: attributeValue,
830
1102
  });
831
1103
  });
1104
+ // Get the child endpoints
832
1105
  const childEndpoints = endpoint.getChildEndpoints();
1106
+ // if (childEndpoints.length === 0) {
1107
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1108
+ // }
833
1109
  childEndpoints.forEach((childEndpoint) => {
834
1110
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
835
1111
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
836
1112
  return;
837
1113
  }
1114
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1115
+ // Get the device types of the child endpoint
838
1116
  const deviceTypes = [];
839
1117
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
840
1118
  deviceTypes.push(d.deviceType);
@@ -844,9 +1122,12 @@ export class Frontend {
844
1122
  return;
845
1123
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
846
1124
  return;
1125
+ // console.log(
1126
+ // `${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}`,
1127
+ // );
847
1128
  clusters.push({
848
1129
  endpoint: childEndpoint.number.toString(),
849
- id: childEndpoint.maybeId ?? 'null',
1130
+ id: childEndpoint.maybeId ?? 'null', // Never happens
850
1131
  deviceTypes,
851
1132
  clusterName: capitalizeFirstLetter(clusterName),
852
1133
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -859,6 +1140,13 @@ export class Frontend {
859
1140
  });
860
1141
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
861
1142
  }
1143
+ /**
1144
+ * Handles incoming websocket messages for the Matterbridge frontend.
1145
+ *
1146
+ * @param {WebSocket} client - The websocket client that sent the message.
1147
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1148
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1149
+ */
862
1150
  async wsMessageHandler(client, message) {
863
1151
  let data;
864
1152
  try {
@@ -897,40 +1185,49 @@ export class Frontend {
897
1185
  return;
898
1186
  }
899
1187
  this.wssSendSnackbarMessage(`Installing package ${data.params.packageName}...`, 0);
900
- spawn
901
- .spawnCommand(this.matterbridge, 'npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
1188
+ const { spawnCommand } = await import('./utils/spawn.js');
1189
+ spawnCommand(this.matterbridge, 'npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
902
1190
  .then((response) => {
903
1191
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
904
1192
  this.wssSendCloseSnackbarMessage(`Installing package ${data.params.packageName}...`);
905
1193
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
906
1194
  const packageName = data.params.packageName.replace(/@.*$/, '');
907
1195
  if (data.params.restart === false && packageName !== 'matterbridge') {
1196
+ // The install comes from InstallPlugins
908
1197
  this.matterbridge.plugins
909
1198
  .add(packageName)
910
1199
  .then((plugin) => {
911
1200
  if (plugin) {
1201
+ // The plugin is not registered
912
1202
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
913
1203
  this.matterbridge.plugins
914
1204
  .load(plugin, true, 'The plugin has been added', true)
1205
+ // eslint-disable-next-line promise/no-nesting
915
1206
  .then(() => {
916
1207
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
917
1208
  this.wssSendRefreshRequired('plugins');
918
1209
  return;
919
1210
  })
1211
+ // eslint-disable-next-line promise/no-nesting
920
1212
  .catch((_error) => {
1213
+ //
921
1214
  });
922
1215
  }
923
1216
  else {
1217
+ // The plugin is already registered
924
1218
  this.wssSendSnackbarMessage(`Restart required`, 0);
925
1219
  this.wssSendRefreshRequired('plugins');
926
1220
  this.wssSendRestartRequired();
927
1221
  }
928
1222
  return;
929
1223
  })
1224
+ // eslint-disable-next-line promise/no-nesting
930
1225
  .catch((_error) => {
1226
+ //
931
1227
  });
932
1228
  }
933
1229
  else {
1230
+ // The package is matterbridge
934
1231
  if (this.matterbridge.restartMode !== '') {
935
1232
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
936
1233
  this.matterbridge.shutdownProcess();
@@ -953,6 +1250,7 @@ export class Frontend {
953
1250
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
954
1251
  return;
955
1252
  }
1253
+ // The package is a plugin
956
1254
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
957
1255
  if (plugin) {
958
1256
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -961,9 +1259,10 @@ export class Frontend {
961
1259
  this.wssSendRefreshRequired('plugins');
962
1260
  this.wssSendRefreshRequired('devices');
963
1261
  }
1262
+ // Uninstall the package
964
1263
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
965
- spawn
966
- .spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
1264
+ const { spawnCommand } = await import('./utils/spawn.js');
1265
+ spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
967
1266
  .then((response) => {
968
1267
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
969
1268
  this.wssSendCloseSnackbarMessage(`Uninstalling package ${data.params.packageName}...`);
@@ -1001,6 +1300,7 @@ export class Frontend {
1001
1300
  return;
1002
1301
  })
1003
1302
  .catch((_error) => {
1303
+ //
1004
1304
  });
1005
1305
  }
1006
1306
  else {
@@ -1047,6 +1347,7 @@ export class Frontend {
1047
1347
  return;
1048
1348
  })
1049
1349
  .catch((_error) => {
1350
+ //
1050
1351
  });
1051
1352
  }
1052
1353
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1284,22 +1585,22 @@ export class Frontend {
1284
1585
  if (isValidString(data.params.value, 4)) {
1285
1586
  this.log.debug('Matterbridge logger level:', data.params.value);
1286
1587
  if (data.params.value === 'Debug') {
1287
- await this.matterbridge.setLogLevel("debug");
1588
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1288
1589
  }
1289
1590
  else if (data.params.value === 'Info') {
1290
- await this.matterbridge.setLogLevel("info");
1591
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1291
1592
  }
1292
1593
  else if (data.params.value === 'Notice') {
1293
- await this.matterbridge.setLogLevel("notice");
1594
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1294
1595
  }
1295
1596
  else if (data.params.value === 'Warn') {
1296
- await this.matterbridge.setLogLevel("warn");
1597
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1297
1598
  }
1298
1599
  else if (data.params.value === 'Error') {
1299
- await this.matterbridge.setLogLevel("error");
1600
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1300
1601
  }
1301
1602
  else if (data.params.value === 'Fatal') {
1302
- await this.matterbridge.setLogLevel("fatal");
1603
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1303
1604
  }
1304
1605
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1305
1606
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1310,6 +1611,7 @@ export class Frontend {
1310
1611
  this.log.debug('Matterbridge file log:', data.params.value);
1311
1612
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1312
1613
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1614
+ // Create the file logger for matterbridge
1313
1615
  if (data.params.value)
1314
1616
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1315
1617
  else
@@ -1474,15 +1776,19 @@ export class Frontend {
1474
1776
  return;
1475
1777
  }
1476
1778
  const config = plugin.configJson;
1779
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1477
1780
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1781
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1478
1782
  if (select === 'serial')
1479
1783
  this.log.info(`Selected device serial ${data.params.serial}`);
1480
1784
  if (select === 'name')
1481
1785
  this.log.info(`Selected device name ${data.params.name}`);
1482
1786
  if (config && select && (select === 'serial' || select === 'name')) {
1787
+ // Remove postfix from the serial if it exists
1483
1788
  if (config.postfix) {
1484
1789
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1485
1790
  }
1791
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1486
1792
  if (isValidArray(config.whiteList, 1)) {
1487
1793
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1488
1794
  config.whiteList.push(data.params.serial);
@@ -1491,6 +1797,7 @@ export class Frontend {
1491
1797
  config.whiteList.push(data.params.name);
1492
1798
  }
1493
1799
  }
1800
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1494
1801
  if (isValidArray(config.blackList, 1)) {
1495
1802
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1496
1803
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1520,7 +1827,9 @@ export class Frontend {
1520
1827
  return;
1521
1828
  }
1522
1829
  const config = plugin.configJson;
1830
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1523
1831
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1832
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1524
1833
  if (select === 'serial')
1525
1834
  this.log.info(`Unselected device serial ${data.params.serial}`);
1526
1835
  if (select === 'name')
@@ -1529,6 +1838,7 @@ export class Frontend {
1529
1838
  if (config.postfix) {
1530
1839
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1531
1840
  }
1841
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1532
1842
  if (isValidArray(config.whiteList, 1)) {
1533
1843
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1534
1844
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1537,6 +1847,7 @@ export class Frontend {
1537
1847
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1538
1848
  }
1539
1849
  }
1850
+ // Add the serial to the blackList
1540
1851
  if (isValidArray(config.blackList)) {
1541
1852
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1542
1853
  config.blackList.push(data.params.serial);
@@ -1569,114 +1880,230 @@ export class Frontend {
1569
1880
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1570
1881
  }
1571
1882
  }
1883
+ /**
1884
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1885
+ *
1886
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1887
+ * @param {string} time - The time string of the message
1888
+ * @param {string} name - The logger name of the message
1889
+ * @param {string} message - The content of the message.
1890
+ *
1891
+ * @remarks
1892
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1893
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1894
+ * The function sends the message to all connected clients.
1895
+ */
1572
1896
  wssSendMessage(level, time, name, message) {
1573
1897
  if (!level || !time || !name || !message)
1574
1898
  return;
1899
+ // Remove ANSI escape codes from the message
1900
+ // eslint-disable-next-line no-control-regex
1575
1901
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1902
+ // Remove leading asterisks from the message
1576
1903
  message = message.replace(/^\*+/, '');
1904
+ // Replace all occurrences of \t and \n
1577
1905
  message = message.replace(/[\t\n]/g, '');
1906
+ // Remove non-printable characters
1907
+ // eslint-disable-next-line no-control-regex
1578
1908
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1909
+ // Replace all occurrences of \" with "
1579
1910
  message = message.replace(/\\"/g, '"');
1911
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1580
1912
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1913
+ // Define the maximum allowed length for continuous characters without a space
1581
1914
  const maxContinuousLength = 100;
1582
1915
  const keepStartLength = 20;
1583
1916
  const keepEndLength = 20;
1917
+ // Split the message into words
1584
1918
  message = message
1585
1919
  .split(' ')
1586
1920
  .map((word) => {
1921
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1587
1922
  if (word.length > maxContinuousLength) {
1588
1923
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1589
1924
  }
1590
1925
  return word;
1591
1926
  })
1592
1927
  .join(' ');
1928
+ // Send the message to all connected clients
1593
1929
  this.webSocketServer?.clients.forEach((client) => {
1594
1930
  if (client.readyState === WebSocket.OPEN) {
1595
1931
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1596
1932
  }
1597
1933
  });
1598
1934
  }
1935
+ /**
1936
+ * Sends a need to refresh WebSocket message to all connected clients.
1937
+ *
1938
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1939
+ * possible values:
1940
+ * - 'matterbridgeLatestVersion'
1941
+ * - 'matterbridgeAdvertise'
1942
+ * - 'online'
1943
+ * - 'offline'
1944
+ * - 'reachability'
1945
+ * - 'settings'
1946
+ * - 'plugins'
1947
+ * - 'pluginsRestart'
1948
+ * - 'devices'
1949
+ * - 'fabrics'
1950
+ * - 'sessions'
1951
+ */
1599
1952
  wssSendRefreshRequired(changed = null) {
1600
1953
  this.log.debug('Sending a refresh required message to all connected clients');
1954
+ // Send the message to all connected clients
1601
1955
  this.webSocketServer?.clients.forEach((client) => {
1602
1956
  if (client.readyState === WebSocket.OPEN) {
1603
1957
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1604
1958
  }
1605
1959
  });
1606
1960
  }
1961
+ /**
1962
+ * Sends a need to restart WebSocket message to all connected clients.
1963
+ *
1964
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients.
1965
+ */
1607
1966
  wssSendRestartRequired(snackbar = true) {
1608
1967
  this.log.debug('Sending a restart required message to all connected clients');
1609
1968
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1610
1969
  if (snackbar === true)
1611
1970
  this.wssSendSnackbarMessage(`Restart required`, 0);
1971
+ // Send the message to all connected clients
1612
1972
  this.webSocketServer?.clients.forEach((client) => {
1613
1973
  if (client.readyState === WebSocket.OPEN) {
1614
1974
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1615
1975
  }
1616
1976
  });
1617
1977
  }
1978
+ /**
1979
+ * Sends a need to update WebSocket message to all connected clients.
1980
+ *
1981
+ */
1618
1982
  wssSendUpdateRequired() {
1619
1983
  this.log.debug('Sending an update required message to all connected clients');
1620
1984
  this.matterbridge.matterbridgeInformation.updateRequired = true;
1985
+ // Send the message to all connected clients
1621
1986
  this.webSocketServer?.clients.forEach((client) => {
1622
1987
  if (client.readyState === WebSocket.OPEN) {
1623
1988
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1624
1989
  }
1625
1990
  });
1626
1991
  }
1992
+ /**
1993
+ * Sends a cpu update message to all connected clients.
1994
+ *
1995
+ * @param {number} cpuUsage - The CPU usage percentage to send.
1996
+ */
1627
1997
  wssSendCpuUpdate(cpuUsage) {
1628
1998
  if (hasParameter('debug'))
1629
1999
  this.log.debug('Sending a cpu update message to all connected clients');
2000
+ // Send the message to all connected clients
1630
2001
  this.webSocketServer?.clients.forEach((client) => {
1631
2002
  if (client.readyState === WebSocket.OPEN) {
1632
2003
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1633
2004
  }
1634
2005
  });
1635
2006
  }
2007
+ /**
2008
+ * Sends a memory update message to all connected clients.
2009
+ *
2010
+ * @param {string} totalMemory - The total memory in bytes.
2011
+ * @param {string} freeMemory - The free memory in bytes.
2012
+ * @param {string} rss - The resident set size in bytes.
2013
+ * @param {string} heapTotal - The total heap memory in bytes.
2014
+ * @param {string} heapUsed - The used heap memory in bytes.
2015
+ * @param {string} external - The external memory in bytes.
2016
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2017
+ */
1636
2018
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1637
2019
  if (hasParameter('debug'))
1638
2020
  this.log.debug('Sending a memory update message to all connected clients');
2021
+ // Send the message to all connected clients
1639
2022
  this.webSocketServer?.clients.forEach((client) => {
1640
2023
  if (client.readyState === WebSocket.OPEN) {
1641
2024
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1642
2025
  }
1643
2026
  });
1644
2027
  }
2028
+ /**
2029
+ * Sends an uptime update message to all connected clients.
2030
+ *
2031
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2032
+ * @param {string} processUptime - The process uptime in a human-readable format.
2033
+ */
1645
2034
  wssSendUptimeUpdate(systemUptime, processUptime) {
1646
2035
  if (hasParameter('debug'))
1647
2036
  this.log.debug('Sending a uptime update message to all connected clients');
2037
+ // Send the message to all connected clients
1648
2038
  this.webSocketServer?.clients.forEach((client) => {
1649
2039
  if (client.readyState === WebSocket.OPEN) {
1650
2040
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1651
2041
  }
1652
2042
  });
1653
2043
  }
2044
+ /**
2045
+ * Sends an open snackbar message to all connected clients.
2046
+ *
2047
+ * @param {string} message - The message to send.
2048
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2049
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2050
+ */
1654
2051
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1655
2052
  this.log.debug('Sending a snackbar message to all connected clients');
2053
+ // Send the message to all connected clients
1656
2054
  this.webSocketServer?.clients.forEach((client) => {
1657
2055
  if (client.readyState === WebSocket.OPEN) {
1658
2056
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1659
2057
  }
1660
2058
  });
1661
2059
  }
2060
+ /**
2061
+ * Sends a close snackbar message to all connected clients.
2062
+ *
2063
+ * @param {string} message - The message to send.
2064
+ */
1662
2065
  wssSendCloseSnackbarMessage(message) {
1663
2066
  this.log.debug('Sending a close snackbar message to all connected clients');
2067
+ // Send the message to all connected clients
1664
2068
  this.webSocketServer?.clients.forEach((client) => {
1665
2069
  if (client.readyState === WebSocket.OPEN) {
1666
2070
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1667
2071
  }
1668
2072
  });
1669
2073
  }
2074
+ /**
2075
+ * Sends an attribute update message to all connected WebSocket clients.
2076
+ *
2077
+ * @param {string | undefined} plugin - The name of the plugin.
2078
+ * @param {string | undefined} serialNumber - The serial number of the device.
2079
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2080
+ * @param {string} cluster - The cluster name where the attribute belongs.
2081
+ * @param {string} attribute - The name of the attribute that changed.
2082
+ * @param {number | string | boolean} value - The new value of the attribute.
2083
+ *
2084
+ * @remarks
2085
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2086
+ * with the updated attribute information.
2087
+ */
1670
2088
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1671
2089
  this.log.debug('Sending an attribute update message to all connected clients');
2090
+ // Send the message to all connected clients
1672
2091
  this.webSocketServer?.clients.forEach((client) => {
1673
2092
  if (client.readyState === WebSocket.OPEN) {
1674
2093
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1675
2094
  }
1676
2095
  });
1677
2096
  }
2097
+ /**
2098
+ * Sends a message to all connected clients.
2099
+ *
2100
+ * @param {number} id - The message id.
2101
+ * @param {string} method - The message method.
2102
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2103
+ */
1678
2104
  wssBroadcastMessage(id, method, params) {
1679
2105
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2106
+ // Send the message to all connected clients
1680
2107
  this.webSocketServer?.clients.forEach((client) => {
1681
2108
  if (client.readyState === WebSocket.OPEN) {
1682
2109
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1684,3 +2111,4 @@ export class Frontend {
1684
2111
  });
1685
2112
  }
1686
2113
  }
2114
+ //# sourceMappingURL=frontend.js.map