matterbridge 3.1.5-dev-20250718-054cd80 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/CHANGELOG.md +5 -6
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +91 -2
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cliEmitter.d.ts +34 -0
  7. package/dist/cliEmitter.d.ts.map +1 -0
  8. package/dist/cliEmitter.js +30 -0
  9. package/dist/cliEmitter.js.map +1 -0
  10. package/dist/clusters/export.d.ts +2 -0
  11. package/dist/clusters/export.d.ts.map +1 -0
  12. package/dist/clusters/export.js +2 -0
  13. package/dist/clusters/export.js.map +1 -0
  14. package/dist/defaultConfigSchema.d.ts +28 -0
  15. package/dist/defaultConfigSchema.d.ts.map +1 -0
  16. package/dist/defaultConfigSchema.js +24 -0
  17. package/dist/defaultConfigSchema.js.map +1 -0
  18. package/dist/deviceManager.d.ts +112 -0
  19. package/dist/deviceManager.d.ts.map +1 -0
  20. package/dist/deviceManager.js +94 -1
  21. package/dist/deviceManager.js.map +1 -0
  22. package/dist/devices/batteryStorage.d.ts +48 -0
  23. package/dist/devices/batteryStorage.d.ts.map +1 -0
  24. package/dist/devices/batteryStorage.js +48 -1
  25. package/dist/devices/batteryStorage.js.map +1 -0
  26. package/dist/devices/evse.d.ts +75 -0
  27. package/dist/devices/evse.d.ts.map +1 -0
  28. package/dist/devices/evse.js +74 -10
  29. package/dist/devices/evse.js.map +1 -0
  30. package/dist/devices/export.d.ts +9 -0
  31. package/dist/devices/export.d.ts.map +1 -0
  32. package/dist/devices/export.js +2 -0
  33. package/dist/devices/export.js.map +1 -0
  34. package/dist/devices/heatPump.d.ts +47 -0
  35. package/dist/devices/heatPump.d.ts.map +1 -0
  36. package/dist/devices/heatPump.js +50 -2
  37. package/dist/devices/heatPump.js.map +1 -0
  38. package/dist/devices/laundryDryer.d.ts +87 -0
  39. package/dist/devices/laundryDryer.d.ts.map +1 -0
  40. package/dist/devices/laundryDryer.js +83 -6
  41. package/dist/devices/laundryDryer.js.map +1 -0
  42. package/dist/devices/laundryWasher.d.ts +242 -0
  43. package/dist/devices/laundryWasher.d.ts.map +1 -0
  44. package/dist/devices/laundryWasher.js +91 -7
  45. package/dist/devices/laundryWasher.js.map +1 -0
  46. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  47. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  48. package/dist/devices/roboticVacuumCleaner.js +103 -14
  49. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  50. package/dist/devices/solarPower.d.ts +40 -0
  51. package/dist/devices/solarPower.d.ts.map +1 -0
  52. package/dist/devices/solarPower.js +38 -0
  53. package/dist/devices/solarPower.js.map +1 -0
  54. package/dist/devices/waterHeater.d.ts +111 -0
  55. package/dist/devices/waterHeater.d.ts.map +1 -0
  56. package/dist/devices/waterHeater.js +82 -2
  57. package/dist/devices/waterHeater.js.map +1 -0
  58. package/dist/frontend.d.ts +304 -0
  59. package/dist/frontend.d.ts.map +1 -0
  60. package/dist/frontend.js +430 -22
  61. package/dist/frontend.js.map +1 -0
  62. package/dist/globalMatterbridge.d.ts +59 -0
  63. package/dist/globalMatterbridge.d.ts.map +1 -0
  64. package/dist/globalMatterbridge.js +47 -0
  65. package/dist/globalMatterbridge.js.map +1 -0
  66. package/dist/helpers.d.ts +48 -0
  67. package/dist/helpers.d.ts.map +1 -0
  68. package/dist/helpers.js +53 -0
  69. package/dist/helpers.js.map +1 -0
  70. package/dist/index.d.ts +33 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +30 -1
  73. package/dist/index.js.map +1 -0
  74. package/dist/logger/export.d.ts +2 -0
  75. package/dist/logger/export.d.ts.map +1 -0
  76. package/dist/logger/export.js +1 -0
  77. package/dist/logger/export.js.map +1 -0
  78. package/dist/matter/behaviors.d.ts +2 -0
  79. package/dist/matter/behaviors.d.ts.map +1 -0
  80. package/dist/matter/behaviors.js +2 -0
  81. package/dist/matter/behaviors.js.map +1 -0
  82. package/dist/matter/clusters.d.ts +2 -0
  83. package/dist/matter/clusters.d.ts.map +1 -0
  84. package/dist/matter/clusters.js +2 -0
  85. package/dist/matter/clusters.js.map +1 -0
  86. package/dist/matter/devices.d.ts +2 -0
  87. package/dist/matter/devices.d.ts.map +1 -0
  88. package/dist/matter/devices.js +2 -0
  89. package/dist/matter/devices.js.map +1 -0
  90. package/dist/matter/endpoints.d.ts +2 -0
  91. package/dist/matter/endpoints.d.ts.map +1 -0
  92. package/dist/matter/endpoints.js +2 -0
  93. package/dist/matter/endpoints.js.map +1 -0
  94. package/dist/matter/export.d.ts +5 -0
  95. package/dist/matter/export.d.ts.map +1 -0
  96. package/dist/matter/export.js +3 -0
  97. package/dist/matter/export.js.map +1 -0
  98. package/dist/matter/types.d.ts +3 -0
  99. package/dist/matter/types.d.ts.map +1 -0
  100. package/dist/matter/types.js +3 -0
  101. package/dist/matter/types.js.map +1 -0
  102. package/dist/matterbridge.d.ts +447 -0
  103. package/dist/matterbridge.d.ts.map +1 -0
  104. package/dist/matterbridge.js +791 -51
  105. package/dist/matterbridge.js.map +1 -0
  106. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  107. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  108. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  109. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  110. package/dist/matterbridgeBehaviors.d.ts +1340 -0
  111. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  112. package/dist/matterbridgeBehaviors.js +61 -1
  113. package/dist/matterbridgeBehaviors.js.map +1 -0
  114. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  115. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  116. package/dist/matterbridgeDeviceTypes.js +579 -15
  117. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  118. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  119. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  120. package/dist/matterbridgeDynamicPlatform.js +36 -0
  121. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  122. package/dist/matterbridgeEndpoint.d.ts +1250 -0
  123. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  124. package/dist/matterbridgeEndpoint.js +1106 -42
  125. package/dist/matterbridgeEndpoint.js.map +1 -0
  126. package/dist/matterbridgeEndpointHelpers.d.ts +3198 -0
  127. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  128. package/dist/matterbridgeEndpointHelpers.js +322 -12
  129. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  130. package/dist/matterbridgePlatform.d.ts +310 -0
  131. package/dist/matterbridgePlatform.d.ts.map +1 -0
  132. package/dist/matterbridgePlatform.js +233 -0
  133. package/dist/matterbridgePlatform.js.map +1 -0
  134. package/dist/matterbridgeTypes.d.ts +195 -0
  135. package/dist/matterbridgeTypes.d.ts.map +1 -0
  136. package/dist/matterbridgeTypes.js +25 -0
  137. package/dist/matterbridgeTypes.js.map +1 -0
  138. package/dist/pluginManager.d.ts +270 -0
  139. package/dist/pluginManager.d.ts.map +1 -0
  140. package/dist/pluginManager.js +249 -3
  141. package/dist/pluginManager.js.map +1 -0
  142. package/dist/shelly.d.ts +174 -0
  143. package/dist/shelly.d.ts.map +1 -0
  144. package/dist/shelly.js +168 -7
  145. package/dist/shelly.js.map +1 -0
  146. package/dist/storage/export.d.ts +2 -0
  147. package/dist/storage/export.d.ts.map +1 -0
  148. package/dist/storage/export.js +1 -0
  149. package/dist/storage/export.js.map +1 -0
  150. package/dist/update.d.ts +59 -0
  151. package/dist/update.d.ts.map +1 -0
  152. package/dist/update.js +54 -0
  153. package/dist/update.js.map +1 -0
  154. package/dist/utils/colorUtils.d.ts +117 -0
  155. package/dist/utils/colorUtils.d.ts.map +1 -0
  156. package/dist/utils/colorUtils.js +263 -2
  157. package/dist/utils/colorUtils.js.map +1 -0
  158. package/dist/utils/commandLine.d.ts +59 -0
  159. package/dist/utils/commandLine.d.ts.map +1 -0
  160. package/dist/utils/commandLine.js +54 -0
  161. package/dist/utils/commandLine.js.map +1 -0
  162. package/dist/utils/copyDirectory.d.ts +33 -0
  163. package/dist/utils/copyDirectory.d.ts.map +1 -0
  164. package/dist/utils/copyDirectory.js +38 -1
  165. package/dist/utils/copyDirectory.js.map +1 -0
  166. package/dist/utils/createDirectory.d.ts +34 -0
  167. package/dist/utils/createDirectory.d.ts.map +1 -0
  168. package/dist/utils/createDirectory.js +33 -0
  169. package/dist/utils/createDirectory.js.map +1 -0
  170. package/dist/utils/createZip.d.ts +39 -0
  171. package/dist/utils/createZip.d.ts.map +1 -0
  172. package/dist/utils/createZip.js +47 -2
  173. package/dist/utils/createZip.js.map +1 -0
  174. package/dist/utils/deepCopy.d.ts +32 -0
  175. package/dist/utils/deepCopy.d.ts.map +1 -0
  176. package/dist/utils/deepCopy.js +39 -0
  177. package/dist/utils/deepCopy.js.map +1 -0
  178. package/dist/utils/deepEqual.d.ts +54 -0
  179. package/dist/utils/deepEqual.d.ts.map +1 -0
  180. package/dist/utils/deepEqual.js +72 -1
  181. package/dist/utils/deepEqual.js.map +1 -0
  182. package/dist/utils/error.d.ts +44 -0
  183. package/dist/utils/error.d.ts.map +1 -0
  184. package/dist/utils/error.js +41 -0
  185. package/dist/utils/error.js.map +1 -0
  186. package/dist/utils/export.d.ts +12 -0
  187. package/dist/utils/export.d.ts.map +1 -0
  188. package/dist/utils/export.js +1 -0
  189. package/dist/utils/export.js.map +1 -0
  190. package/dist/utils/hex.d.ts +49 -0
  191. package/dist/utils/hex.d.ts.map +1 -0
  192. package/dist/utils/hex.js +58 -0
  193. package/dist/utils/hex.js.map +1 -0
  194. package/dist/utils/isvalid.d.ts +103 -0
  195. package/dist/utils/isvalid.d.ts.map +1 -0
  196. package/dist/utils/isvalid.js +101 -0
  197. package/dist/utils/isvalid.js.map +1 -0
  198. package/dist/utils/network.d.ts +74 -0
  199. package/dist/utils/network.d.ts.map +1 -0
  200. package/dist/utils/network.js +81 -5
  201. package/dist/utils/network.js.map +1 -0
  202. package/dist/utils/spawn.d.ts +33 -0
  203. package/dist/utils/spawn.d.ts.map +1 -0
  204. package/dist/utils/spawn.js +40 -0
  205. package/dist/utils/spawn.js.map +1 -0
  206. package/dist/utils/wait.d.ts +56 -0
  207. package/dist/utils/wait.d.ts.map +1 -0
  208. package/dist/utils/wait.js +62 -9
  209. package/dist/utils/wait.js.map +1 -0
  210. package/npm-shrinkwrap.json +2 -2
  211. package/package.json +2 -1
package/dist/frontend.js CHANGED
@@ -1,30 +1,126 @@
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.2.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 { existsSync, promises as fs } from 'node:fs';
6
30
  import EventEmitter from 'node:events';
31
+ // Third-party modules
7
32
  import express from 'express';
8
33
  import WebSocket, { WebSocketServer } from 'ws';
9
34
  import multer from 'multer';
35
+ // AnsiLogger module
10
36
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
37
+ // @matter
11
38
  import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
12
39
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
40
+ // Matterbridge
13
41
  import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
14
42
  import { plg } from './matterbridgeTypes.js';
15
43
  import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
16
44
  import { cliEmitter, lastCpuUsage } from './cliEmitter.js';
45
+ /**
46
+ * Websocket message ID for logging.
47
+ *
48
+ * @constant {number}
49
+ */
17
50
  export const WS_ID_LOG = 0;
51
+ /**
52
+ * Websocket message ID indicating a refresh is needed.
53
+ *
54
+ * @constant {number}
55
+ */
18
56
  export const WS_ID_REFRESH_NEEDED = 1;
57
+ /**
58
+ * Websocket message ID indicating a restart is needed.
59
+ *
60
+ * @constant {number}
61
+ */
19
62
  export const WS_ID_RESTART_NEEDED = 2;
63
+ /**
64
+ * Websocket message ID indicating a cpu update.
65
+ *
66
+ * @constant {number}
67
+ */
20
68
  export const WS_ID_CPU_UPDATE = 3;
69
+ /**
70
+ * Websocket message ID indicating a memory update.
71
+ *
72
+ * @constant {number}
73
+ */
21
74
  export const WS_ID_MEMORY_UPDATE = 4;
75
+ /**
76
+ * Websocket message ID indicating an uptime update.
77
+ *
78
+ * @constant {number}
79
+ */
22
80
  export const WS_ID_UPTIME_UPDATE = 5;
81
+ /**
82
+ * Websocket message ID indicating a snackbar message.
83
+ *
84
+ * @constant {number}
85
+ */
23
86
  export const WS_ID_SNACKBAR = 6;
87
+ /**
88
+ * Websocket message ID indicating matterbridge has un update available.
89
+ *
90
+ * @constant {number}
91
+ */
24
92
  export const WS_ID_UPDATE_NEEDED = 7;
93
+ /**
94
+ * Websocket message ID indicating a state update.
95
+ *
96
+ * @constant {number}
97
+ */
25
98
  export const WS_ID_STATEUPDATE = 8;
99
+ /**
100
+ * Websocket message ID indicating to close a permanent snackbar message.
101
+ *
102
+ * @constant {number}
103
+ */
26
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
+ */
27
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
+ */
28
124
  export const WS_ID_SHELLY_MAIN_UPDATE = 101;
29
125
  export class Frontend extends EventEmitter {
30
126
  matterbridge;
@@ -37,7 +133,7 @@ export class Frontend extends EventEmitter {
37
133
  constructor(matterbridge) {
38
134
  super();
39
135
  this.matterbridge = matterbridge;
40
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
136
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
41
137
  }
42
138
  set logLevel(logLevel) {
43
139
  this.log.logLevel = logLevel;
@@ -45,12 +141,41 @@ export class Frontend extends EventEmitter {
45
141
  async start(port = 8283) {
46
142
  this.port = port;
47
143
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
144
+ // Initialize multer with the upload directory
48
145
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
49
146
  await fs.mkdir(uploadDir, { recursive: true });
50
147
  const upload = multer({ dest: uploadDir });
148
+ // Create the express app that serves the frontend
51
149
  this.expressApp = express();
150
+ // Inject logging/debug wrapper for route/middleware registration
151
+ /*
152
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
153
+ for (const method of methods) {
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
156
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
158
+ try {
159
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
160
+ return original(path, ...rest);
161
+ } catch (err) {
162
+ console.error(`[ERROR] Failed to register route: ${path}`);
163
+ throw err;
164
+ }
165
+ };
166
+ }
167
+ */
168
+ // Log all requests to the server for debugging
169
+ /*
170
+ this.expressApp.use((req, res, next) => {
171
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
172
+ next();
173
+ });
174
+ */
175
+ // Serve static files from '/static' endpoint
52
176
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
53
177
  if (!hasParameter('ssl')) {
178
+ // Create an HTTP server and attach the express app
54
179
  try {
55
180
  this.log.debug(`Creating HTTP server...`);
56
181
  this.httpServer = createServer(this.expressApp);
@@ -60,6 +185,7 @@ export class Frontend extends EventEmitter {
60
185
  this.emit('server_error', error);
61
186
  return;
62
187
  }
188
+ // Listen on the specified port
63
189
  if (hasParameter('ingress')) {
64
190
  this.httpServer.listen(this.port, '0.0.0.0', () => {
65
191
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -98,6 +224,7 @@ export class Frontend extends EventEmitter {
98
224
  let passphrase;
99
225
  let httpsServerOptions = {};
100
226
  if (existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
227
+ // Load the p12 certificate and the passphrase
101
228
  try {
102
229
  pfx = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
103
230
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
@@ -109,7 +236,7 @@ export class Frontend extends EventEmitter {
109
236
  }
110
237
  try {
111
238
  passphrase = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
112
- passphrase = passphrase.trim();
239
+ passphrase = passphrase.trim(); // Ensure no extra characters
113
240
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
114
241
  }
115
242
  catch (error) {
@@ -120,6 +247,7 @@ export class Frontend extends EventEmitter {
120
247
  httpsServerOptions = { pfx, passphrase };
121
248
  }
122
249
  else {
250
+ // Load the SSL certificate, the private key and optionally the CA certificate. If the CA certificate is present, it will be used to create a full chain certificate.
123
251
  try {
124
252
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
125
253
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
@@ -149,9 +277,10 @@ export class Frontend extends EventEmitter {
149
277
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
150
278
  }
151
279
  if (hasParameter('mtls')) {
152
- httpsServerOptions.requestCert = true;
153
- httpsServerOptions.rejectUnauthorized = true;
280
+ httpsServerOptions.requestCert = true; // Request client certificate
281
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
154
282
  }
283
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
155
284
  try {
156
285
  this.log.debug(`Creating HTTPS server...`);
157
286
  this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
@@ -161,6 +290,7 @@ export class Frontend extends EventEmitter {
161
290
  this.emit('server_error', error);
162
291
  return;
163
292
  }
293
+ // Listen on the specified port
164
294
  if (hasParameter('ingress')) {
165
295
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
166
296
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -190,17 +320,19 @@ export class Frontend extends EventEmitter {
190
320
  return;
191
321
  });
192
322
  }
323
+ // Create a WebSocket server and attach it to the http or https server
193
324
  const wssPort = this.port;
194
325
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
195
326
  this.log.debug(`Creating WebSocketServer on host ${CYAN}${wssHost}${db}...`);
196
327
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
197
328
  this.webSocketServer.on('connection', (ws, request) => {
198
329
  const clientIp = request.socket.remoteAddress;
199
- let callbackLogLevel = "notice";
200
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
201
- callbackLogLevel = "info";
202
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
203
- callbackLogLevel = "debug";
330
+ // Set the global logger callback for the WebSocketServer
331
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
332
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
333
+ callbackLogLevel = "info" /* LogLevel.INFO */;
334
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
335
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
204
336
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
205
337
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
206
338
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -222,6 +354,7 @@ export class Frontend extends EventEmitter {
222
354
  }
223
355
  });
224
356
  ws.on('error', (error) => {
357
+ // istanbul ignore next
225
358
  this.log.error(`WebSocket client error: ${error}`);
226
359
  });
227
360
  });
@@ -235,6 +368,7 @@ export class Frontend extends EventEmitter {
235
368
  this.webSocketServer.on('error', (ws, error) => {
236
369
  this.log.error(`WebSocketServer error: ${error}`);
237
370
  });
371
+ // Subscribe to cli events
238
372
  cliEmitter.removeAllListeners();
239
373
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
240
374
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -245,6 +379,8 @@ export class Frontend extends EventEmitter {
245
379
  cliEmitter.on('cpu', (cpuUsage) => {
246
380
  this.wssSendCpuUpdate(cpuUsage);
247
381
  });
382
+ // Endpoint to validate login code
383
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
248
384
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
249
385
  const { password } = req.body;
250
386
  this.log.debug('The frontend sent /api/login', password);
@@ -263,23 +399,27 @@ export class Frontend extends EventEmitter {
263
399
  this.log.warn('/api/login error wrong password');
264
400
  res.json({ valid: false });
265
401
  }
402
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
266
403
  }
267
404
  catch (error) {
268
405
  this.log.error('/api/login error getting password');
269
406
  res.json({ valid: false });
270
407
  }
271
408
  });
409
+ // Endpoint to provide health check for docker
272
410
  this.expressApp.get('/health', (req, res) => {
273
411
  this.log.debug('Express received /health');
274
412
  const healthStatus = {
275
- status: 'ok',
276
- uptime: process.uptime(),
277
- timestamp: new Date().toISOString(),
413
+ status: 'ok', // Indicate service is healthy
414
+ uptime: process.uptime(), // Server uptime in seconds
415
+ timestamp: new Date().toISOString(), // Current timestamp
278
416
  };
279
417
  res.status(200).json(healthStatus);
280
418
  });
419
+ // Endpoint to provide memory usage details
281
420
  this.expressApp.get('/memory', async (req, res) => {
282
421
  this.log.debug('Express received /memory');
422
+ // Memory usage from process
283
423
  const memoryUsageRaw = process.memoryUsage();
284
424
  const memoryUsage = {
285
425
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -288,10 +428,13 @@ export class Frontend extends EventEmitter {
288
428
  external: this.formatMemoryUsage(memoryUsageRaw.external),
289
429
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
290
430
  };
431
+ // V8 heap statistics
291
432
  const { default: v8 } = await import('node:v8');
292
433
  const heapStatsRaw = v8.getHeapStatistics();
293
434
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
435
+ // Format heapStats
294
436
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
437
+ // Format heapSpaces
295
438
  const heapSpaces = heapSpacesRaw.map((space) => ({
296
439
  ...space,
297
440
  space_size: this.formatMemoryUsage(space.space_size),
@@ -309,19 +452,23 @@ export class Frontend extends EventEmitter {
309
452
  };
310
453
  res.status(200).json(memoryReport);
311
454
  });
455
+ // Endpoint to provide settings
312
456
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
313
457
  this.log.debug('The frontend sent /api/settings');
314
458
  res.json(await this.getApiSettings());
315
459
  });
460
+ // Endpoint to provide plugins
316
461
  this.expressApp.get('/api/plugins', async (req, res) => {
317
462
  this.log.debug('The frontend sent /api/plugins');
318
463
  res.json(this.getBaseRegisteredPlugins());
319
464
  });
465
+ // Endpoint to provide devices
320
466
  this.expressApp.get('/api/devices', async (req, res) => {
321
467
  this.log.debug('The frontend sent /api/devices');
322
468
  const devices = await this.getDevices();
323
469
  res.json(devices);
324
470
  });
471
+ // Endpoint to view the matterbridge log
325
472
  this.expressApp.get('/api/view-mblog', async (req, res) => {
326
473
  this.log.debug('The frontend sent /api/view-mblog');
327
474
  try {
@@ -334,6 +481,7 @@ export class Frontend extends EventEmitter {
334
481
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
335
482
  }
336
483
  });
484
+ // Endpoint to view the matter.js log
337
485
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
338
486
  this.log.debug('The frontend sent /api/view-mjlog');
339
487
  try {
@@ -346,6 +494,7 @@ export class Frontend extends EventEmitter {
346
494
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
347
495
  }
348
496
  });
497
+ // Endpoint to view the shelly log
349
498
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
350
499
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
351
500
  try {
@@ -358,9 +507,11 @@ export class Frontend extends EventEmitter {
358
507
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
359
508
  }
360
509
  });
510
+ // Endpoint to download the matterbridge log
361
511
  this.expressApp.get('/api/download-mblog', async (req, res) => {
362
512
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
363
513
  try {
514
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
364
515
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), fs.constants.F_OK);
365
516
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), 'utf8');
366
517
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), data, 'utf-8');
@@ -371,15 +522,18 @@ export class Frontend extends EventEmitter {
371
522
  }
372
523
  res.type('text/plain');
373
524
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), 'matterbridge.log', (error) => {
525
+ /* istanbul ignore if */
374
526
  if (error) {
375
527
  this.log.error(`Error downloading log file ${this.matterbridge.matterbridgeLoggerFile}: ${error instanceof Error ? error.message : error}`);
376
528
  res.status(500).send('Error downloading the matterbridge log file');
377
529
  }
378
530
  });
379
531
  });
532
+ // Endpoint to download the matter log
380
533
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
381
534
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
382
535
  try {
536
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
383
537
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
384
538
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
385
539
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
@@ -390,15 +544,18 @@ export class Frontend extends EventEmitter {
390
544
  }
391
545
  res.type('text/plain');
392
546
  res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
547
+ /* istanbul ignore if */
393
548
  if (error) {
394
549
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
395
550
  res.status(500).send('Error downloading the matter log file');
396
551
  }
397
552
  });
398
553
  });
554
+ // Endpoint to download the shelly log
399
555
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
400
556
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
401
557
  try {
558
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
402
559
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
403
560
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
404
561
  await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
@@ -409,74 +566,90 @@ export class Frontend extends EventEmitter {
409
566
  }
410
567
  res.type('text/plain');
411
568
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
569
+ /* istanbul ignore if */
412
570
  if (error) {
413
571
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
414
572
  res.status(500).send('Error downloading Shelly system log file');
415
573
  }
416
574
  });
417
575
  });
576
+ // Endpoint to download the matterbridge storage directory
418
577
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
419
578
  this.log.debug('The frontend sent /api/download-mbstorage');
420
579
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
421
580
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
581
+ /* istanbul ignore if */
422
582
  if (error) {
423
583
  this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
424
584
  res.status(500).send('Error downloading the matterbridge storage file');
425
585
  }
426
586
  });
427
587
  });
588
+ // Endpoint to download the matter storage file
428
589
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
429
590
  this.log.debug('The frontend sent /api/download-mjstorage');
430
591
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
431
592
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => {
593
+ /* istanbul ignore if */
432
594
  if (error) {
433
595
  this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`);
434
596
  res.status(500).send('Error downloading the matter storage zip file');
435
597
  }
436
598
  });
437
599
  });
600
+ // Endpoint to download the matterbridge plugin directory
438
601
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
439
602
  this.log.debug('The frontend sent /api/download-pluginstorage');
440
603
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
441
604
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
605
+ /* istanbul ignore if */
442
606
  if (error) {
443
607
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
444
608
  res.status(500).send('Error downloading the matterbridge plugin storage file');
445
609
  }
446
610
  });
447
611
  });
612
+ // Endpoint to download the matterbridge plugin config files
448
613
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
449
614
  this.log.debug('The frontend sent /api/download-pluginconfig');
450
615
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
451
616
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
617
+ /* istanbul ignore if */
452
618
  if (error) {
453
619
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
454
620
  res.status(500).send('Error downloading the matterbridge plugin config file');
455
621
  }
456
622
  });
457
623
  });
624
+ // Endpoint to download the matterbridge backup (created with the backup command)
458
625
  this.expressApp.get('/api/download-backup', async (req, res) => {
459
626
  this.log.debug('The frontend sent /api/download-backup');
460
627
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
628
+ /* istanbul ignore if */
461
629
  if (error) {
462
630
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
463
631
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
464
632
  }
465
633
  });
466
634
  });
635
+ // Endpoint to upload a package
467
636
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
468
637
  const { filename } = req.body;
469
638
  const file = req.file;
639
+ /* istanbul ignore if */
470
640
  if (!file || !filename) {
471
641
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
472
642
  res.status(400).send('Invalid request: file and filename are required');
473
643
  return;
474
644
  }
475
645
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
646
+ // Define the path where the plugin file will be saved
476
647
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
477
648
  try {
649
+ // Move the uploaded file to the specified path
478
650
  await fs.rename(file.path, filePath);
479
651
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
652
+ // Install the plugin package
480
653
  if (filename.endsWith('.tgz')) {
481
654
  const { spawnCommand } = await import('./utils/spawn.js');
482
655
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
@@ -496,6 +669,7 @@ export class Frontend extends EventEmitter {
496
669
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
497
670
  }
498
671
  });
672
+ // Fallback for routing (must be the last route)
499
673
  this.expressApp.use((req, res) => {
500
674
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
501
675
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -504,13 +678,16 @@ export class Frontend extends EventEmitter {
504
678
  }
505
679
  async stop() {
506
680
  this.log.debug('Stopping the frontend...');
681
+ // Remove listeners from the express app
507
682
  if (this.expressApp) {
508
683
  this.expressApp.removeAllListeners();
509
684
  this.expressApp = undefined;
510
685
  this.log.debug('Frontend app closed successfully');
511
686
  }
687
+ // Close the WebSocket server
512
688
  if (this.webSocketServer) {
513
689
  this.log.debug('Closing WebSocket server...');
690
+ // Close all active connections
514
691
  this.webSocketServer.clients.forEach((client) => {
515
692
  if (client.readyState === WebSocket.OPEN) {
516
693
  client.close();
@@ -531,6 +708,7 @@ export class Frontend extends EventEmitter {
531
708
  this.webSocketServer.removeAllListeners();
532
709
  this.webSocketServer = undefined;
533
710
  }
711
+ // Close the http server
534
712
  if (this.httpServer) {
535
713
  this.log.debug('Closing http server...');
536
714
  await withTimeout(new Promise((resolve) => {
@@ -549,6 +727,7 @@ export class Frontend extends EventEmitter {
549
727
  this.httpServer = undefined;
550
728
  this.log.debug('Frontend http server closed successfully');
551
729
  }
730
+ // Close the https server
552
731
  if (this.httpsServer) {
553
732
  this.log.debug('Closing https server...');
554
733
  await withTimeout(new Promise((resolve) => {
@@ -569,6 +748,7 @@ export class Frontend extends EventEmitter {
569
748
  }
570
749
  this.log.debug('Frontend stopped successfully');
571
750
  }
751
+ // Function to format bytes to KB, MB, or GB
572
752
  formatMemoryUsage = (bytes) => {
573
753
  if (bytes >= 1024 ** 3) {
574
754
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -580,6 +760,7 @@ export class Frontend extends EventEmitter {
580
760
  return `${(bytes / 1024).toFixed(2)} KB`;
581
761
  }
582
762
  };
763
+ // Function to format system uptime with only the most significant unit
583
764
  formatOsUpTime = (seconds) => {
584
765
  if (seconds >= 86400) {
585
766
  const days = Math.floor(seconds / 86400);
@@ -595,7 +776,13 @@ export class Frontend extends EventEmitter {
595
776
  }
596
777
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
597
778
  };
779
+ /**
780
+ * Retrieves the api settings data.
781
+ *
782
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
783
+ */
598
784
  async getApiSettings() {
785
+ // Update the system information
599
786
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
600
787
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
601
788
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -604,6 +791,7 @@ export class Frontend extends EventEmitter {
604
791
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
605
792
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
606
793
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
794
+ // Update the matterbridge information
607
795
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
608
796
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
609
797
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
@@ -615,7 +803,8 @@ export class Frontend extends EventEmitter {
615
803
  this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
616
804
  this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
617
805
  this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
618
- if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode) {
806
+ // Update the matterbridge information in bridge mode
807
+ if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode && !this.matterbridge.hasCleanupStarted) {
619
808
  this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.serverNode.state.commissioning.commissioned;
620
809
  this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.qrPairingCode;
621
810
  this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.manualPairingCode;
@@ -624,6 +813,12 @@ export class Frontend extends EventEmitter {
624
813
  }
625
814
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
626
815
  }
816
+ /**
817
+ * Retrieves the reachable attribute.
818
+ *
819
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
820
+ * @returns {boolean} The reachable attribute.
821
+ */
627
822
  getReachability(device) {
628
823
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
629
824
  return false;
@@ -635,6 +830,12 @@ export class Frontend extends EventEmitter {
635
830
  return true;
636
831
  return false;
637
832
  }
833
+ /**
834
+ * Retrieves the power source attribute.
835
+ *
836
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
837
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
838
+ */
638
839
  getPowerSource(endpoint) {
639
840
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
640
841
  return undefined;
@@ -650,13 +851,21 @@ export class Frontend extends EventEmitter {
650
851
  }
651
852
  return;
652
853
  };
854
+ // Root endpoint
653
855
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
654
856
  return powerSource(endpoint);
857
+ // Child endpoints
655
858
  for (const child of endpoint.getChildEndpoints()) {
656
859
  if (child.hasClusterServer(PowerSource.Cluster.id))
657
860
  return powerSource(child);
658
861
  }
659
862
  }
863
+ /**
864
+ * Retrieves the commissioned status, matter pairing codes, fabrics and sessions from a given device in server mode.
865
+ *
866
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the data from.
867
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
868
+ */
660
869
  getMatterDataFromDevice(device) {
661
870
  if (device.mode === 'server' && device.serverNode) {
662
871
  return {
@@ -668,6 +877,13 @@ export class Frontend extends EventEmitter {
668
877
  };
669
878
  }
670
879
  }
880
+ /**
881
+ * Retrieves the cluster text description from a given device.
882
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
883
+ *
884
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
885
+ * @returns {string} The attributes description of the cluster servers in the device.
886
+ */
671
887
  getClusterTextFromDevice(device) {
672
888
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
673
889
  return '';
@@ -692,6 +908,7 @@ export class Frontend extends EventEmitter {
692
908
  let attributes = '';
693
909
  let supportedModes = [];
694
910
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
911
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
695
912
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
696
913
  return;
697
914
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -781,11 +998,17 @@ export class Frontend extends EventEmitter {
781
998
  if (clusterName === 'userLabel' && attributeName === 'labelList')
782
999
  attributes += `${getUserLabel(device)} `;
783
1000
  });
1001
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
784
1002
  return attributes.trimStart().trimEnd();
785
1003
  }
1004
+ /**
1005
+ * Retrieves the base registered plugins sanitized for res.json().
1006
+ *
1007
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1008
+ */
786
1009
  getBaseRegisteredPlugins() {
787
1010
  if (this.matterbridge.hasCleanupStarted)
788
- return [];
1011
+ return []; // Skip if cleanup has started
789
1012
  const baseRegisteredPlugins = [];
790
1013
  for (const plugin of this.matterbridge.plugins) {
791
1014
  baseRegisteredPlugins.push({
@@ -814,6 +1037,7 @@ export class Frontend extends EventEmitter {
814
1037
  schemaJson: plugin.schemaJson,
815
1038
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
816
1039
  hasBlackList: plugin.configJson?.blackList !== undefined,
1040
+ // Childbridge mode specific data
817
1041
  paired: plugin.serverNode?.state.commissioning.commissioned,
818
1042
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.qrPairingCode,
819
1043
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.manualPairingCode,
@@ -823,13 +1047,21 @@ export class Frontend extends EventEmitter {
823
1047
  }
824
1048
  return baseRegisteredPlugins;
825
1049
  }
1050
+ /**
1051
+ * Retrieves the devices from Matterbridge.
1052
+ *
1053
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1054
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1055
+ */
826
1056
  async getDevices(pluginName) {
827
1057
  if (this.matterbridge.hasCleanupStarted)
828
- return [];
1058
+ return []; // Skip if cleanup has started
829
1059
  const devices = [];
830
1060
  for (const device of this.matterbridge.devices.array()) {
1061
+ // Filter by pluginName if provided
831
1062
  if (pluginName && pluginName !== device.plugin)
832
1063
  continue;
1064
+ // Check if the device has the required properties
833
1065
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
834
1066
  continue;
835
1067
  devices.push({
@@ -849,22 +1081,37 @@ export class Frontend extends EventEmitter {
849
1081
  }
850
1082
  return devices;
851
1083
  }
1084
+ /**
1085
+ * Retrieves the clusters from a given plugin and endpoint number.
1086
+ *
1087
+ * Response for /api/clusters
1088
+ *
1089
+ * @param {string} pluginName - The name of the plugin.
1090
+ * @param {number} endpointNumber - The endpoint number.
1091
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1092
+ */
852
1093
  getClusters(pluginName, endpointNumber) {
853
1094
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
854
1095
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
855
1096
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
856
1097
  return;
857
1098
  }
1099
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1100
+ // Get the device types from the main endpoint
858
1101
  const deviceTypes = [];
859
1102
  const clusters = [];
860
1103
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
861
1104
  deviceTypes.push(d.deviceType);
862
1105
  });
1106
+ // Get the clusters from the main endpoint
863
1107
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
864
1108
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
865
1109
  return;
866
1110
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
867
1111
  return;
1112
+ // console.log(
1113
+ // `${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}`,
1114
+ // );
868
1115
  clusters.push({
869
1116
  endpoint: endpoint.number.toString(),
870
1117
  id: 'main',
@@ -877,12 +1124,18 @@ export class Frontend extends EventEmitter {
877
1124
  attributeLocalValue: attributeValue,
878
1125
  });
879
1126
  });
1127
+ // Get the child endpoints
880
1128
  const childEndpoints = endpoint.getChildEndpoints();
1129
+ // if (childEndpoints.length === 0) {
1130
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1131
+ // }
881
1132
  childEndpoints.forEach((childEndpoint) => {
882
1133
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
883
1134
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
884
1135
  return;
885
1136
  }
1137
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1138
+ // Get the device types of the child endpoint
886
1139
  const deviceTypes = [];
887
1140
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
888
1141
  deviceTypes.push(d.deviceType);
@@ -892,9 +1145,12 @@ export class Frontend extends EventEmitter {
892
1145
  return;
893
1146
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
894
1147
  return;
1148
+ // console.log(
1149
+ // `${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}`,
1150
+ // );
895
1151
  clusters.push({
896
1152
  endpoint: childEndpoint.number.toString(),
897
- id: childEndpoint.maybeId ?? 'null',
1153
+ id: childEndpoint.maybeId ?? 'null', // Never happens
898
1154
  deviceTypes,
899
1155
  clusterName: capitalizeFirstLetter(clusterName),
900
1156
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -907,6 +1163,13 @@ export class Frontend extends EventEmitter {
907
1163
  });
908
1164
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
909
1165
  }
1166
+ /**
1167
+ * Handles incoming websocket messages for the Matterbridge frontend.
1168
+ *
1169
+ * @param {WebSocket} client - The websocket client that sent the message.
1170
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1171
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1172
+ */
910
1173
  async wsMessageHandler(client, message) {
911
1174
  let data;
912
1175
  try {
@@ -953,32 +1216,42 @@ export class Frontend extends EventEmitter {
953
1216
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
954
1217
  const packageName = data.params.packageName.replace(/@.*$/, '');
955
1218
  if (data.params.restart === false && packageName !== 'matterbridge') {
1219
+ // The install comes from InstallPlugins
956
1220
  this.matterbridge.plugins
957
1221
  .add(packageName)
958
1222
  .then((plugin) => {
959
1223
  if (plugin) {
1224
+ // The plugin is not registered
960
1225
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
961
1226
  this.matterbridge.plugins
962
1227
  .load(plugin, true, 'The plugin has been added', true)
1228
+ // eslint-disable-next-line promise/no-nesting
963
1229
  .then(() => {
964
1230
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
965
1231
  this.wssSendRefreshRequired('plugins');
966
1232
  return;
967
1233
  })
1234
+ // eslint-disable-next-line promise/no-nesting
968
1235
  .catch((_error) => {
1236
+ //
969
1237
  });
970
1238
  }
971
1239
  else {
1240
+ // The plugin is already registered
972
1241
  this.wssSendSnackbarMessage(`Restart required`, 0);
973
1242
  this.wssSendRefreshRequired('plugins');
974
1243
  this.wssSendRestartRequired();
975
1244
  }
976
1245
  return;
977
1246
  })
1247
+ // eslint-disable-next-line promise/no-nesting
978
1248
  .catch((_error) => {
1249
+ //
979
1250
  });
980
1251
  }
981
1252
  else {
1253
+ // The package is matterbridge
1254
+ // istanbul ignore next if
982
1255
  if (this.matterbridge.restartMode !== '') {
983
1256
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
984
1257
  this.matterbridge.shutdownProcess();
@@ -1001,6 +1274,7 @@ export class Frontend extends EventEmitter {
1001
1274
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1002
1275
  return;
1003
1276
  }
1277
+ // The package is a plugin
1004
1278
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
1005
1279
  if (plugin) {
1006
1280
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -1009,6 +1283,7 @@ export class Frontend extends EventEmitter {
1009
1283
  this.wssSendRefreshRequired('plugins');
1010
1284
  this.wssSendRefreshRequired('devices');
1011
1285
  }
1286
+ // Uninstall the package
1012
1287
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1013
1288
  const { spawnCommand } = await import('./utils/spawn.js');
1014
1289
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1049,6 +1324,7 @@ export class Frontend extends EventEmitter {
1049
1324
  return;
1050
1325
  })
1051
1326
  .catch((_error) => {
1327
+ //
1052
1328
  });
1053
1329
  }
1054
1330
  else {
@@ -1095,6 +1371,7 @@ export class Frontend extends EventEmitter {
1095
1371
  return;
1096
1372
  })
1097
1373
  .catch((_error) => {
1374
+ //
1098
1375
  });
1099
1376
  }
1100
1377
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1205,6 +1482,8 @@ export class Frontend extends EventEmitter {
1205
1482
  else if (data.method === '/api/advertise') {
1206
1483
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1207
1484
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1485
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1486
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1208
1487
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1209
1488
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1210
1489
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1327,22 +1606,22 @@ export class Frontend extends EventEmitter {
1327
1606
  if (isValidString(data.params.value, 4)) {
1328
1607
  this.log.debug('Matterbridge logger level:', data.params.value);
1329
1608
  if (data.params.value === 'Debug') {
1330
- await this.matterbridge.setLogLevel("debug");
1609
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1331
1610
  }
1332
1611
  else if (data.params.value === 'Info') {
1333
- await this.matterbridge.setLogLevel("info");
1612
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1334
1613
  }
1335
1614
  else if (data.params.value === 'Notice') {
1336
- await this.matterbridge.setLogLevel("notice");
1615
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1337
1616
  }
1338
1617
  else if (data.params.value === 'Warn') {
1339
- await this.matterbridge.setLogLevel("warn");
1618
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1340
1619
  }
1341
1620
  else if (data.params.value === 'Error') {
1342
- await this.matterbridge.setLogLevel("error");
1621
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1343
1622
  }
1344
1623
  else if (data.params.value === 'Fatal') {
1345
- await this.matterbridge.setLogLevel("fatal");
1624
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1346
1625
  }
1347
1626
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1348
1627
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1353,6 +1632,7 @@ export class Frontend extends EventEmitter {
1353
1632
  this.log.debug('Matterbridge file log:', data.params.value);
1354
1633
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1355
1634
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1635
+ // Create the file logger for matterbridge
1356
1636
  if (data.params.value)
1357
1637
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1358
1638
  else
@@ -1399,6 +1679,7 @@ export class Frontend extends EventEmitter {
1399
1679
  });
1400
1680
  }
1401
1681
  catch (error) {
1682
+ /* istanbul ignore next */
1402
1683
  this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1403
1684
  }
1404
1685
  }
@@ -1407,6 +1688,7 @@ export class Frontend extends EventEmitter {
1407
1688
  Logger.removeLogger('matterfilelogger');
1408
1689
  }
1409
1690
  catch (error) {
1691
+ /* istanbul ignore next */
1410
1692
  this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1411
1693
  }
1412
1694
  }
@@ -1519,15 +1801,19 @@ export class Frontend extends EventEmitter {
1519
1801
  return;
1520
1802
  }
1521
1803
  const config = plugin.configJson;
1804
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1522
1805
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1806
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1523
1807
  if (select === 'serial')
1524
1808
  this.log.info(`Selected device serial ${data.params.serial}`);
1525
1809
  if (select === 'name')
1526
1810
  this.log.info(`Selected device name ${data.params.name}`);
1527
1811
  if (config && select && (select === 'serial' || select === 'name')) {
1812
+ // Remove postfix from the serial if it exists
1528
1813
  if (config.postfix) {
1529
1814
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1530
1815
  }
1816
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1531
1817
  if (isValidArray(config.whiteList, 1)) {
1532
1818
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1533
1819
  config.whiteList.push(data.params.serial);
@@ -1536,6 +1822,7 @@ export class Frontend extends EventEmitter {
1536
1822
  config.whiteList.push(data.params.name);
1537
1823
  }
1538
1824
  }
1825
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1539
1826
  if (isValidArray(config.blackList, 1)) {
1540
1827
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1541
1828
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1566,7 +1853,9 @@ export class Frontend extends EventEmitter {
1566
1853
  return;
1567
1854
  }
1568
1855
  const config = plugin.configJson;
1856
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1569
1857
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1858
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1570
1859
  if (select === 'serial')
1571
1860
  this.log.info(`Unselected device serial ${data.params.serial}`);
1572
1861
  if (select === 'name')
@@ -1575,6 +1864,7 @@ export class Frontend extends EventEmitter {
1575
1864
  if (config.postfix) {
1576
1865
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1577
1866
  }
1867
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1578
1868
  if (isValidArray(config.whiteList, 1)) {
1579
1869
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1580
1870
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1583,6 +1873,7 @@ export class Frontend extends EventEmitter {
1583
1873
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1584
1874
  }
1585
1875
  }
1876
+ // Add the serial to the blackList
1586
1877
  if (isValidArray(config.blackList)) {
1587
1878
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1588
1879
  config.blackList.push(data.params.serial);
@@ -1616,114 +1907,230 @@ export class Frontend extends EventEmitter {
1616
1907
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1617
1908
  }
1618
1909
  }
1910
+ /**
1911
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1912
+ *
1913
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1914
+ * @param {string} time - The time string of the message
1915
+ * @param {string} name - The logger name of the message
1916
+ * @param {string} message - The content of the message.
1917
+ *
1918
+ * @remarks
1919
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1920
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1921
+ * The function sends the message to all connected clients.
1922
+ */
1619
1923
  wssSendMessage(level, time, name, message) {
1620
1924
  if (!level || !time || !name || !message)
1621
1925
  return;
1926
+ // Remove ANSI escape codes from the message
1927
+ // eslint-disable-next-line no-control-regex
1622
1928
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1929
+ // Remove leading asterisks from the message
1623
1930
  message = message.replace(/^\*+/, '');
1931
+ // Replace all occurrences of \t and \n
1624
1932
  message = message.replace(/[\t\n]/g, '');
1933
+ // Remove non-printable characters
1934
+ // eslint-disable-next-line no-control-regex
1625
1935
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1936
+ // Replace all occurrences of \" with "
1626
1937
  message = message.replace(/\\"/g, '"');
1938
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1627
1939
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1940
+ // Define the maximum allowed length for continuous characters without a space
1628
1941
  const maxContinuousLength = 100;
1629
1942
  const keepStartLength = 20;
1630
1943
  const keepEndLength = 20;
1944
+ // Split the message into words
1631
1945
  message = message
1632
1946
  .split(' ')
1633
1947
  .map((word) => {
1948
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1634
1949
  if (word.length > maxContinuousLength) {
1635
1950
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1636
1951
  }
1637
1952
  return word;
1638
1953
  })
1639
1954
  .join(' ');
1955
+ // Send the message to all connected clients
1640
1956
  this.webSocketServer?.clients.forEach((client) => {
1641
1957
  if (client.readyState === WebSocket.OPEN) {
1642
1958
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1643
1959
  }
1644
1960
  });
1645
1961
  }
1962
+ /**
1963
+ * Sends a need to refresh WebSocket message to all connected clients.
1964
+ *
1965
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1966
+ * possible values:
1967
+ * - 'matterbridgeLatestVersion'
1968
+ * - 'matterbridgeAdvertise'
1969
+ * - 'online'
1970
+ * - 'offline'
1971
+ * - 'reachability'
1972
+ * - 'settings'
1973
+ * - 'plugins'
1974
+ * - 'pluginsRestart'
1975
+ * - 'devices'
1976
+ * - 'fabrics'
1977
+ * - 'sessions'
1978
+ */
1646
1979
  wssSendRefreshRequired(changed = null) {
1647
1980
  this.log.debug('Sending a refresh required message to all connected clients');
1981
+ // Send the message to all connected clients
1648
1982
  this.webSocketServer?.clients.forEach((client) => {
1649
1983
  if (client.readyState === WebSocket.OPEN) {
1650
1984
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1651
1985
  }
1652
1986
  });
1653
1987
  }
1988
+ /**
1989
+ * Sends a need to restart WebSocket message to all connected clients.
1990
+ *
1991
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients.
1992
+ */
1654
1993
  wssSendRestartRequired(snackbar = true) {
1655
1994
  this.log.debug('Sending a restart required message to all connected clients');
1656
1995
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1657
1996
  if (snackbar === true)
1658
1997
  this.wssSendSnackbarMessage(`Restart required`, 0);
1998
+ // Send the message to all connected clients
1659
1999
  this.webSocketServer?.clients.forEach((client) => {
1660
2000
  if (client.readyState === WebSocket.OPEN) {
1661
2001
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1662
2002
  }
1663
2003
  });
1664
2004
  }
2005
+ /**
2006
+ * Sends a need to update WebSocket message to all connected clients.
2007
+ *
2008
+ */
1665
2009
  wssSendUpdateRequired() {
1666
2010
  this.log.debug('Sending an update required message to all connected clients');
1667
2011
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2012
+ // Send the message to all connected clients
1668
2013
  this.webSocketServer?.clients.forEach((client) => {
1669
2014
  if (client.readyState === WebSocket.OPEN) {
1670
2015
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1671
2016
  }
1672
2017
  });
1673
2018
  }
2019
+ /**
2020
+ * Sends a cpu update message to all connected clients.
2021
+ *
2022
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2023
+ */
1674
2024
  wssSendCpuUpdate(cpuUsage) {
1675
2025
  if (hasParameter('debug'))
1676
2026
  this.log.debug('Sending a cpu update message to all connected clients');
2027
+ // Send the message to all connected clients
1677
2028
  this.webSocketServer?.clients.forEach((client) => {
1678
2029
  if (client.readyState === WebSocket.OPEN) {
1679
2030
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1680
2031
  }
1681
2032
  });
1682
2033
  }
2034
+ /**
2035
+ * Sends a memory update message to all connected clients.
2036
+ *
2037
+ * @param {string} totalMemory - The total memory in bytes.
2038
+ * @param {string} freeMemory - The free memory in bytes.
2039
+ * @param {string} rss - The resident set size in bytes.
2040
+ * @param {string} heapTotal - The total heap memory in bytes.
2041
+ * @param {string} heapUsed - The used heap memory in bytes.
2042
+ * @param {string} external - The external memory in bytes.
2043
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2044
+ */
1683
2045
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1684
2046
  if (hasParameter('debug'))
1685
2047
  this.log.debug('Sending a memory update message to all connected clients');
2048
+ // Send the message to all connected clients
1686
2049
  this.webSocketServer?.clients.forEach((client) => {
1687
2050
  if (client.readyState === WebSocket.OPEN) {
1688
2051
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1689
2052
  }
1690
2053
  });
1691
2054
  }
2055
+ /**
2056
+ * Sends an uptime update message to all connected clients.
2057
+ *
2058
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2059
+ * @param {string} processUptime - The process uptime in a human-readable format.
2060
+ */
1692
2061
  wssSendUptimeUpdate(systemUptime, processUptime) {
1693
2062
  if (hasParameter('debug'))
1694
2063
  this.log.debug('Sending a uptime update message to all connected clients');
2064
+ // Send the message to all connected clients
1695
2065
  this.webSocketServer?.clients.forEach((client) => {
1696
2066
  if (client.readyState === WebSocket.OPEN) {
1697
2067
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1698
2068
  }
1699
2069
  });
1700
2070
  }
2071
+ /**
2072
+ * Sends an open snackbar message to all connected clients.
2073
+ *
2074
+ * @param {string} message - The message to send.
2075
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2076
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2077
+ */
1701
2078
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1702
2079
  this.log.debug('Sending a snackbar message to all connected clients');
2080
+ // Send the message to all connected clients
1703
2081
  this.webSocketServer?.clients.forEach((client) => {
1704
2082
  if (client.readyState === WebSocket.OPEN) {
1705
2083
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1706
2084
  }
1707
2085
  });
1708
2086
  }
2087
+ /**
2088
+ * Sends a close snackbar message to all connected clients.
2089
+ *
2090
+ * @param {string} message - The message to send.
2091
+ */
1709
2092
  wssSendCloseSnackbarMessage(message) {
1710
2093
  this.log.debug('Sending a close snackbar message to all connected clients');
2094
+ // Send the message to all connected clients
1711
2095
  this.webSocketServer?.clients.forEach((client) => {
1712
2096
  if (client.readyState === WebSocket.OPEN) {
1713
2097
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1714
2098
  }
1715
2099
  });
1716
2100
  }
2101
+ /**
2102
+ * Sends an attribute update message to all connected WebSocket clients.
2103
+ *
2104
+ * @param {string | undefined} plugin - The name of the plugin.
2105
+ * @param {string | undefined} serialNumber - The serial number of the device.
2106
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2107
+ * @param {string} cluster - The cluster name where the attribute belongs.
2108
+ * @param {string} attribute - The name of the attribute that changed.
2109
+ * @param {number | string | boolean} value - The new value of the attribute.
2110
+ *
2111
+ * @remarks
2112
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2113
+ * with the updated attribute information.
2114
+ */
1717
2115
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1718
2116
  this.log.debug('Sending an attribute update message to all connected clients');
2117
+ // Send the message to all connected clients
1719
2118
  this.webSocketServer?.clients.forEach((client) => {
1720
2119
  if (client.readyState === WebSocket.OPEN) {
1721
2120
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1722
2121
  }
1723
2122
  });
1724
2123
  }
2124
+ /**
2125
+ * Sends a message to all connected clients.
2126
+ *
2127
+ * @param {number} id - The message id.
2128
+ * @param {string} method - The message method.
2129
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2130
+ */
1725
2131
  wssBroadcastMessage(id, method, params) {
1726
2132
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2133
+ // Send the message to all connected clients
1727
2134
  this.webSocketServer?.clients.forEach((client) => {
1728
2135
  if (client.readyState === WebSocket.OPEN) {
1729
2136
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1731,3 +2138,4 @@ export class Frontend extends EventEmitter {
1731
2138
  });
1732
2139
  }
1733
2140
  }
2141
+ //# sourceMappingURL=frontend.js.map