matterbridge 3.1.7-dev-20250724-c3522e6 → 3.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +6 -15
  2. package/README-SERVICE.md +1 -0
  3. package/dist/cli.d.ts +26 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +91 -2
  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 +30 -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/dishwasher.d.ts +91 -0
  28. package/dist/devices/dishwasher.d.ts.map +1 -0
  29. package/dist/devices/dishwasher.js +78 -3
  30. package/dist/devices/dishwasher.js.map +1 -0
  31. package/dist/devices/evse.d.ts +75 -0
  32. package/dist/devices/evse.d.ts.map +1 -0
  33. package/dist/devices/evse.js +74 -10
  34. package/dist/devices/evse.js.map +1 -0
  35. package/dist/devices/export.d.ts +11 -0
  36. package/dist/devices/export.d.ts.map +1 -0
  37. package/dist/devices/export.js +2 -0
  38. package/dist/devices/export.js.map +1 -0
  39. package/dist/devices/extractorHood.d.ts +46 -0
  40. package/dist/devices/extractorHood.d.ts.map +1 -0
  41. package/dist/devices/extractorHood.js +42 -0
  42. package/dist/devices/extractorHood.js.map +1 -0
  43. package/dist/devices/heatPump.d.ts +47 -0
  44. package/dist/devices/heatPump.d.ts.map +1 -0
  45. package/dist/devices/heatPump.js +50 -2
  46. package/dist/devices/heatPump.js.map +1 -0
  47. package/dist/devices/laundryDryer.d.ts +87 -0
  48. package/dist/devices/laundryDryer.d.ts.map +1 -0
  49. package/dist/devices/laundryDryer.js +83 -6
  50. package/dist/devices/laundryDryer.js.map +1 -0
  51. package/dist/devices/laundryWasher.d.ts +242 -0
  52. package/dist/devices/laundryWasher.d.ts.map +1 -0
  53. package/dist/devices/laundryWasher.js +91 -7
  54. package/dist/devices/laundryWasher.js.map +1 -0
  55. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  56. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  57. package/dist/devices/roboticVacuumCleaner.js +93 -7
  58. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  59. package/dist/devices/solarPower.d.ts +40 -0
  60. package/dist/devices/solarPower.d.ts.map +1 -0
  61. package/dist/devices/solarPower.js +38 -0
  62. package/dist/devices/solarPower.js.map +1 -0
  63. package/dist/devices/waterHeater.d.ts +111 -0
  64. package/dist/devices/waterHeater.d.ts.map +1 -0
  65. package/dist/devices/waterHeater.js +82 -2
  66. package/dist/devices/waterHeater.js.map +1 -0
  67. package/dist/dgram/coap.d.ts +205 -0
  68. package/dist/dgram/coap.d.ts.map +1 -0
  69. package/dist/dgram/coap.js +126 -13
  70. package/dist/dgram/coap.js.map +1 -0
  71. package/dist/dgram/dgram.d.ts +140 -0
  72. package/dist/dgram/dgram.d.ts.map +1 -0
  73. package/dist/dgram/dgram.js +113 -2
  74. package/dist/dgram/dgram.js.map +1 -0
  75. package/dist/dgram/mb_coap.d.ts +24 -0
  76. package/dist/dgram/mb_coap.d.ts.map +1 -0
  77. package/dist/dgram/mb_coap.js +41 -3
  78. package/dist/dgram/mb_coap.js.map +1 -0
  79. package/dist/dgram/mb_mdns.d.ts +24 -0
  80. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  81. package/dist/dgram/mb_mdns.js +51 -13
  82. package/dist/dgram/mb_mdns.js.map +1 -0
  83. package/dist/dgram/mdns.d.ts +288 -0
  84. package/dist/dgram/mdns.d.ts.map +1 -0
  85. package/dist/dgram/mdns.js +300 -135
  86. package/dist/dgram/mdns.js.map +1 -0
  87. package/dist/dgram/multicast.d.ts +65 -0
  88. package/dist/dgram/multicast.d.ts.map +1 -0
  89. package/dist/dgram/multicast.js +60 -1
  90. package/dist/dgram/multicast.js.map +1 -0
  91. package/dist/dgram/unicast.d.ts +56 -0
  92. package/dist/dgram/unicast.d.ts.map +1 -0
  93. package/dist/dgram/unicast.js +54 -0
  94. package/dist/dgram/unicast.js.map +1 -0
  95. package/dist/frontend.d.ts +304 -0
  96. package/dist/frontend.d.ts.map +1 -0
  97. package/dist/frontend.js +435 -21
  98. package/dist/frontend.js.map +1 -0
  99. package/dist/globalMatterbridge.d.ts +59 -0
  100. package/dist/globalMatterbridge.d.ts.map +1 -0
  101. package/dist/globalMatterbridge.js +47 -0
  102. package/dist/globalMatterbridge.js.map +1 -0
  103. package/dist/helpers.d.ts +48 -0
  104. package/dist/helpers.d.ts.map +1 -0
  105. package/dist/helpers.js +53 -0
  106. package/dist/helpers.js.map +1 -0
  107. package/dist/index.d.ts +33 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +30 -1
  110. package/dist/index.js.map +1 -0
  111. package/dist/logger/export.d.ts +2 -0
  112. package/dist/logger/export.d.ts.map +1 -0
  113. package/dist/logger/export.js +1 -0
  114. package/dist/logger/export.js.map +1 -0
  115. package/dist/matter/behaviors.d.ts +2 -0
  116. package/dist/matter/behaviors.d.ts.map +1 -0
  117. package/dist/matter/behaviors.js +2 -0
  118. package/dist/matter/behaviors.js.map +1 -0
  119. package/dist/matter/clusters.d.ts +2 -0
  120. package/dist/matter/clusters.d.ts.map +1 -0
  121. package/dist/matter/clusters.js +2 -0
  122. package/dist/matter/clusters.js.map +1 -0
  123. package/dist/matter/devices.d.ts +2 -0
  124. package/dist/matter/devices.d.ts.map +1 -0
  125. package/dist/matter/devices.js +2 -0
  126. package/dist/matter/devices.js.map +1 -0
  127. package/dist/matter/endpoints.d.ts +2 -0
  128. package/dist/matter/endpoints.d.ts.map +1 -0
  129. package/dist/matter/endpoints.js +2 -0
  130. package/dist/matter/endpoints.js.map +1 -0
  131. package/dist/matter/export.d.ts +5 -0
  132. package/dist/matter/export.d.ts.map +1 -0
  133. package/dist/matter/export.js +3 -0
  134. package/dist/matter/export.js.map +1 -0
  135. package/dist/matter/types.d.ts +3 -0
  136. package/dist/matter/types.d.ts.map +1 -0
  137. package/dist/matter/types.js +3 -0
  138. package/dist/matter/types.js.map +1 -0
  139. package/dist/matterbridge.d.ts +463 -0
  140. package/dist/matterbridge.d.ts.map +1 -0
  141. package/dist/matterbridge.js +843 -58
  142. package/dist/matterbridge.js.map +1 -0
  143. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  144. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  145. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  146. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  147. package/dist/matterbridgeBehaviors.d.ts +1351 -0
  148. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  149. package/dist/matterbridgeBehaviors.js +65 -5
  150. package/dist/matterbridgeBehaviors.js.map +1 -0
  151. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  152. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  153. package/dist/matterbridgeDeviceTypes.js +579 -15
  154. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  155. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  156. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  157. package/dist/matterbridgeDynamicPlatform.js +36 -0
  158. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  159. package/dist/matterbridgeEndpoint.d.ts +1348 -0
  160. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  161. package/dist/matterbridgeEndpoint.js +1220 -51
  162. package/dist/matterbridgeEndpoint.js.map +1 -0
  163. package/dist/matterbridgeEndpointHelpers.d.ts +406 -0
  164. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  165. package/dist/matterbridgeEndpointHelpers.js +350 -18
  166. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  167. package/dist/matterbridgePlatform.d.ts +310 -0
  168. package/dist/matterbridgePlatform.d.ts.map +1 -0
  169. package/dist/matterbridgePlatform.js +233 -0
  170. package/dist/matterbridgePlatform.js.map +1 -0
  171. package/dist/matterbridgeTypes.d.ts +195 -0
  172. package/dist/matterbridgeTypes.d.ts.map +1 -0
  173. package/dist/matterbridgeTypes.js +25 -0
  174. package/dist/matterbridgeTypes.js.map +1 -0
  175. package/dist/pluginManager.d.ts +270 -0
  176. package/dist/pluginManager.d.ts.map +1 -0
  177. package/dist/pluginManager.js +249 -3
  178. package/dist/pluginManager.js.map +1 -0
  179. package/dist/shelly.d.ts +174 -0
  180. package/dist/shelly.d.ts.map +1 -0
  181. package/dist/shelly.js +168 -7
  182. package/dist/shelly.js.map +1 -0
  183. package/dist/storage/export.d.ts +2 -0
  184. package/dist/storage/export.d.ts.map +1 -0
  185. package/dist/storage/export.js +1 -0
  186. package/dist/storage/export.js.map +1 -0
  187. package/dist/update.d.ts +59 -0
  188. package/dist/update.d.ts.map +1 -0
  189. package/dist/update.js +54 -0
  190. package/dist/update.js.map +1 -0
  191. package/dist/utils/colorUtils.d.ts +117 -0
  192. package/dist/utils/colorUtils.d.ts.map +1 -0
  193. package/dist/utils/colorUtils.js +263 -2
  194. package/dist/utils/colorUtils.js.map +1 -0
  195. package/dist/utils/commandLine.d.ts +59 -0
  196. package/dist/utils/commandLine.d.ts.map +1 -0
  197. package/dist/utils/commandLine.js +54 -0
  198. package/dist/utils/commandLine.js.map +1 -0
  199. package/dist/utils/copyDirectory.d.ts +33 -0
  200. package/dist/utils/copyDirectory.d.ts.map +1 -0
  201. package/dist/utils/copyDirectory.js +38 -1
  202. package/dist/utils/copyDirectory.js.map +1 -0
  203. package/dist/utils/createDirectory.d.ts +34 -0
  204. package/dist/utils/createDirectory.d.ts.map +1 -0
  205. package/dist/utils/createDirectory.js +33 -0
  206. package/dist/utils/createDirectory.js.map +1 -0
  207. package/dist/utils/createZip.d.ts +39 -0
  208. package/dist/utils/createZip.d.ts.map +1 -0
  209. package/dist/utils/createZip.js +47 -2
  210. package/dist/utils/createZip.js.map +1 -0
  211. package/dist/utils/deepCopy.d.ts +32 -0
  212. package/dist/utils/deepCopy.d.ts.map +1 -0
  213. package/dist/utils/deepCopy.js +39 -0
  214. package/dist/utils/deepCopy.js.map +1 -0
  215. package/dist/utils/deepEqual.d.ts +54 -0
  216. package/dist/utils/deepEqual.d.ts.map +1 -0
  217. package/dist/utils/deepEqual.js +72 -1
  218. package/dist/utils/deepEqual.js.map +1 -0
  219. package/dist/utils/error.d.ts +44 -0
  220. package/dist/utils/error.d.ts.map +1 -0
  221. package/dist/utils/error.js +41 -0
  222. package/dist/utils/error.js.map +1 -0
  223. package/dist/utils/export.d.ts +12 -0
  224. package/dist/utils/export.d.ts.map +1 -0
  225. package/dist/utils/export.js +1 -0
  226. package/dist/utils/export.js.map +1 -0
  227. package/dist/utils/hex.d.ts +49 -0
  228. package/dist/utils/hex.d.ts.map +1 -0
  229. package/dist/utils/hex.js +58 -0
  230. package/dist/utils/hex.js.map +1 -0
  231. package/dist/utils/isvalid.d.ts +103 -0
  232. package/dist/utils/isvalid.d.ts.map +1 -0
  233. package/dist/utils/isvalid.js +101 -0
  234. package/dist/utils/isvalid.js.map +1 -0
  235. package/dist/utils/network.d.ts +74 -0
  236. package/dist/utils/network.d.ts.map +1 -0
  237. package/dist/utils/network.js +81 -5
  238. package/dist/utils/network.js.map +1 -0
  239. package/dist/utils/spawn.d.ts +33 -0
  240. package/dist/utils/spawn.d.ts.map +1 -0
  241. package/dist/utils/spawn.js +40 -0
  242. package/dist/utils/spawn.js.map +1 -0
  243. package/dist/utils/wait.d.ts +56 -0
  244. package/dist/utils/wait.d.ts.map +1 -0
  245. package/dist/utils/wait.js +62 -9
  246. package/dist/utils/wait.js.map +1 -0
  247. package/npm-shrinkwrap.json +2 -2
  248. 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();
@@ -519,6 +696,7 @@ export class Frontend extends EventEmitter {
519
696
  await withTimeout(new Promise((resolve) => {
520
697
  this.webSocketServer?.close((error) => {
521
698
  if (error) {
699
+ // istanbul ignore next
522
700
  this.log.error(`Error closing WebSocket server: ${error}`);
523
701
  }
524
702
  else {
@@ -531,11 +709,13 @@ export class Frontend extends EventEmitter {
531
709
  this.webSocketServer.removeAllListeners();
532
710
  this.webSocketServer = undefined;
533
711
  }
712
+ // Close the http server
534
713
  if (this.httpServer) {
535
714
  this.log.debug('Closing http server...');
536
715
  await withTimeout(new Promise((resolve) => {
537
716
  this.httpServer?.close((error) => {
538
717
  if (error) {
718
+ // istanbul ignore next
539
719
  this.log.error(`Error closing http server: ${error}`);
540
720
  }
541
721
  else {
@@ -549,11 +729,13 @@ export class Frontend extends EventEmitter {
549
729
  this.httpServer = undefined;
550
730
  this.log.debug('Frontend http server closed successfully');
551
731
  }
732
+ // Close the https server
552
733
  if (this.httpsServer) {
553
734
  this.log.debug('Closing https server...');
554
735
  await withTimeout(new Promise((resolve) => {
555
736
  this.httpsServer?.close((error) => {
556
737
  if (error) {
738
+ // istanbul ignore next
557
739
  this.log.error(`Error closing https server: ${error}`);
558
740
  }
559
741
  else {
@@ -569,6 +751,7 @@ export class Frontend extends EventEmitter {
569
751
  }
570
752
  this.log.debug('Frontend stopped successfully');
571
753
  }
754
+ // Function to format bytes to KB, MB, or GB
572
755
  formatMemoryUsage = (bytes) => {
573
756
  if (bytes >= 1024 ** 3) {
574
757
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -580,6 +763,7 @@ export class Frontend extends EventEmitter {
580
763
  return `${(bytes / 1024).toFixed(2)} KB`;
581
764
  }
582
765
  };
766
+ // Function to format system uptime with only the most significant unit
583
767
  formatOsUpTime = (seconds) => {
584
768
  if (seconds >= 86400) {
585
769
  const days = Math.floor(seconds / 86400);
@@ -595,7 +779,13 @@ export class Frontend extends EventEmitter {
595
779
  }
596
780
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
597
781
  };
782
+ /**
783
+ * Retrieves the api settings data.
784
+ *
785
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
786
+ */
598
787
  async getApiSettings() {
788
+ // Update the system information
599
789
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
600
790
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
601
791
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -604,6 +794,7 @@ export class Frontend extends EventEmitter {
604
794
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
605
795
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
606
796
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
797
+ // Update the matterbridge information
607
798
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
608
799
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
609
800
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
@@ -615,6 +806,7 @@ export class Frontend extends EventEmitter {
615
806
  this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
616
807
  this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
617
808
  this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
809
+ // Update the matterbridge information in bridge mode
618
810
  if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode && !this.matterbridge.hasCleanupStarted) {
619
811
  this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.serverNode.state.commissioning.commissioned;
620
812
  this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.qrPairingCode;
@@ -624,6 +816,12 @@ export class Frontend extends EventEmitter {
624
816
  }
625
817
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
626
818
  }
819
+ /**
820
+ * Retrieves the reachable attribute.
821
+ *
822
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
823
+ * @returns {boolean} The reachable attribute.
824
+ */
627
825
  getReachability(device) {
628
826
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
629
827
  return false;
@@ -635,6 +833,12 @@ export class Frontend extends EventEmitter {
635
833
  return true;
636
834
  return false;
637
835
  }
836
+ /**
837
+ * Retrieves the power source attribute.
838
+ *
839
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
840
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
841
+ */
638
842
  getPowerSource(endpoint) {
639
843
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
640
844
  return undefined;
@@ -650,13 +854,21 @@ export class Frontend extends EventEmitter {
650
854
  }
651
855
  return;
652
856
  };
857
+ // Root endpoint
653
858
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
654
859
  return powerSource(endpoint);
860
+ // Child endpoints
655
861
  for (const child of endpoint.getChildEndpoints()) {
656
862
  if (child.hasClusterServer(PowerSource.Cluster.id))
657
863
  return powerSource(child);
658
864
  }
659
865
  }
866
+ /**
867
+ * Retrieves the commissioned status, matter pairing codes, fabrics and sessions from a given device in server mode.
868
+ *
869
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the data from.
870
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
871
+ */
660
872
  getMatterDataFromDevice(device) {
661
873
  if (device.mode === 'server' && device.serverNode) {
662
874
  return {
@@ -668,6 +880,13 @@ export class Frontend extends EventEmitter {
668
880
  };
669
881
  }
670
882
  }
883
+ /**
884
+ * Retrieves the cluster text description from a given device.
885
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
886
+ *
887
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
888
+ * @returns {string} The attributes description of the cluster servers in the device.
889
+ */
671
890
  getClusterTextFromDevice(device) {
672
891
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
673
892
  return '';
@@ -678,6 +897,7 @@ export class Frontend extends EventEmitter {
678
897
  if (composed)
679
898
  return 'Composed: ' + composed.value;
680
899
  }
900
+ // istanbul ignore next cause is not reachable
681
901
  return '';
682
902
  };
683
903
  const getFixedLabel = (device) => {
@@ -687,11 +907,13 @@ export class Frontend extends EventEmitter {
687
907
  if (composed)
688
908
  return 'Composed: ' + composed.value;
689
909
  }
910
+ // istanbul ignore next cause is not reacheable
690
911
  return '';
691
912
  };
692
913
  let attributes = '';
693
914
  let supportedModes = [];
694
915
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
916
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
695
917
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
696
918
  return;
697
919
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -781,11 +1003,17 @@ export class Frontend extends EventEmitter {
781
1003
  if (clusterName === 'userLabel' && attributeName === 'labelList')
782
1004
  attributes += `${getUserLabel(device)} `;
783
1005
  });
1006
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
784
1007
  return attributes.trimStart().trimEnd();
785
1008
  }
1009
+ /**
1010
+ * Retrieves the base registered plugins sanitized for res.json().
1011
+ *
1012
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1013
+ */
786
1014
  getBaseRegisteredPlugins() {
787
1015
  if (this.matterbridge.hasCleanupStarted)
788
- return [];
1016
+ return []; // Skip if cleanup has started
789
1017
  const baseRegisteredPlugins = [];
790
1018
  for (const plugin of this.matterbridge.plugins) {
791
1019
  baseRegisteredPlugins.push({
@@ -814,6 +1042,7 @@ export class Frontend extends EventEmitter {
814
1042
  schemaJson: plugin.schemaJson,
815
1043
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
816
1044
  hasBlackList: plugin.configJson?.blackList !== undefined,
1045
+ // Childbridge mode specific data
817
1046
  paired: plugin.serverNode?.state.commissioning.commissioned,
818
1047
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.qrPairingCode,
819
1048
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.manualPairingCode,
@@ -823,13 +1052,21 @@ export class Frontend extends EventEmitter {
823
1052
  }
824
1053
  return baseRegisteredPlugins;
825
1054
  }
1055
+ /**
1056
+ * Retrieves the devices from Matterbridge.
1057
+ *
1058
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1059
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1060
+ */
826
1061
  async getDevices(pluginName) {
827
1062
  if (this.matterbridge.hasCleanupStarted)
828
- return [];
1063
+ return []; // Skip if cleanup has started
829
1064
  const devices = [];
830
1065
  for (const device of this.matterbridge.devices.array()) {
1066
+ // Filter by pluginName if provided
831
1067
  if (pluginName && pluginName !== device.plugin)
832
1068
  continue;
1069
+ // Check if the device has the required properties
833
1070
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
834
1071
  continue;
835
1072
  devices.push({
@@ -849,22 +1086,37 @@ export class Frontend extends EventEmitter {
849
1086
  }
850
1087
  return devices;
851
1088
  }
1089
+ /**
1090
+ * Retrieves the clusters from a given plugin and endpoint number.
1091
+ *
1092
+ * Response for /api/clusters
1093
+ *
1094
+ * @param {string} pluginName - The name of the plugin.
1095
+ * @param {number} endpointNumber - The endpoint number.
1096
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1097
+ */
852
1098
  getClusters(pluginName, endpointNumber) {
853
1099
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
854
1100
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
855
1101
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
856
1102
  return;
857
1103
  }
1104
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1105
+ // Get the device types from the main endpoint
858
1106
  const deviceTypes = [];
859
1107
  const clusters = [];
860
1108
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
861
1109
  deviceTypes.push(d.deviceType);
862
1110
  });
1111
+ // Get the clusters from the main endpoint
863
1112
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
864
1113
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
865
1114
  return;
866
1115
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
867
1116
  return;
1117
+ // console.log(
1118
+ // `${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}`,
1119
+ // );
868
1120
  clusters.push({
869
1121
  endpoint: endpoint.number.toString(),
870
1122
  id: 'main',
@@ -877,12 +1129,19 @@ export class Frontend extends EventEmitter {
877
1129
  attributeLocalValue: attributeValue,
878
1130
  });
879
1131
  });
1132
+ // Get the child endpoints
880
1133
  const childEndpoints = endpoint.getChildEndpoints();
1134
+ // if (childEndpoints.length === 0) {
1135
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1136
+ // }
881
1137
  childEndpoints.forEach((childEndpoint) => {
1138
+ // istanbul ignore if cause is not reachable: should never happen but ...
882
1139
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
883
1140
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
884
1141
  return;
885
1142
  }
1143
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1144
+ // Get the device types of the child endpoint
886
1145
  const deviceTypes = [];
887
1146
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
888
1147
  deviceTypes.push(d.deviceType);
@@ -892,9 +1151,12 @@ export class Frontend extends EventEmitter {
892
1151
  return;
893
1152
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
894
1153
  return;
1154
+ // console.log(
1155
+ // `${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}`,
1156
+ // );
895
1157
  clusters.push({
896
1158
  endpoint: childEndpoint.number.toString(),
897
- id: childEndpoint.maybeId ?? 'null',
1159
+ id: childEndpoint.maybeId ?? 'null', // Never happens
898
1160
  deviceTypes,
899
1161
  clusterName: capitalizeFirstLetter(clusterName),
900
1162
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -907,6 +1169,13 @@ export class Frontend extends EventEmitter {
907
1169
  });
908
1170
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
909
1171
  }
1172
+ /**
1173
+ * Handles incoming websocket messages for the Matterbridge frontend.
1174
+ *
1175
+ * @param {WebSocket} client - The websocket client that sent the message.
1176
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1177
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1178
+ */
910
1179
  async wsMessageHandler(client, message) {
911
1180
  let data;
912
1181
  try {
@@ -953,32 +1222,42 @@ export class Frontend extends EventEmitter {
953
1222
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
954
1223
  const packageName = data.params.packageName.replace(/@.*$/, '');
955
1224
  if (data.params.restart === false && packageName !== 'matterbridge') {
1225
+ // The install comes from InstallPlugins
956
1226
  this.matterbridge.plugins
957
1227
  .add(packageName)
958
1228
  .then((plugin) => {
959
1229
  if (plugin) {
1230
+ // The plugin is not registered
960
1231
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
961
1232
  this.matterbridge.plugins
962
1233
  .load(plugin, true, 'The plugin has been added', true)
1234
+ // eslint-disable-next-line promise/no-nesting
963
1235
  .then(() => {
964
1236
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
965
1237
  this.wssSendRefreshRequired('plugins');
966
1238
  return;
967
1239
  })
1240
+ // eslint-disable-next-line promise/no-nesting
968
1241
  .catch((_error) => {
1242
+ //
969
1243
  });
970
1244
  }
971
1245
  else {
1246
+ // The plugin is already registered
972
1247
  this.wssSendSnackbarMessage(`Restart required`, 0);
973
1248
  this.wssSendRefreshRequired('plugins');
974
1249
  this.wssSendRestartRequired();
975
1250
  }
976
1251
  return;
977
1252
  })
1253
+ // eslint-disable-next-line promise/no-nesting
978
1254
  .catch((_error) => {
1255
+ //
979
1256
  });
980
1257
  }
981
1258
  else {
1259
+ // The package is matterbridge
1260
+ // istanbul ignore next if
982
1261
  if (this.matterbridge.restartMode !== '') {
983
1262
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
984
1263
  this.matterbridge.shutdownProcess();
@@ -1001,6 +1280,7 @@ export class Frontend extends EventEmitter {
1001
1280
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1002
1281
  return;
1003
1282
  }
1283
+ // The package is a plugin
1004
1284
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
1005
1285
  if (plugin) {
1006
1286
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -1009,6 +1289,7 @@ export class Frontend extends EventEmitter {
1009
1289
  this.wssSendRefreshRequired('plugins');
1010
1290
  this.wssSendRefreshRequired('devices');
1011
1291
  }
1292
+ // Uninstall the package
1012
1293
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1013
1294
  const { spawnCommand } = await import('./utils/spawn.js');
1014
1295
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1049,6 +1330,7 @@ export class Frontend extends EventEmitter {
1049
1330
  return;
1050
1331
  })
1051
1332
  .catch((_error) => {
1333
+ //
1052
1334
  });
1053
1335
  }
1054
1336
  else {
@@ -1095,6 +1377,7 @@ export class Frontend extends EventEmitter {
1095
1377
  return;
1096
1378
  })
1097
1379
  .catch((_error) => {
1380
+ //
1098
1381
  });
1099
1382
  }
1100
1383
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1205,6 +1488,8 @@ export class Frontend extends EventEmitter {
1205
1488
  else if (data.method === '/api/advertise') {
1206
1489
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1207
1490
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1491
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1492
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1208
1493
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1209
1494
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1210
1495
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1327,22 +1612,22 @@ export class Frontend extends EventEmitter {
1327
1612
  if (isValidString(data.params.value, 4)) {
1328
1613
  this.log.debug('Matterbridge logger level:', data.params.value);
1329
1614
  if (data.params.value === 'Debug') {
1330
- await this.matterbridge.setLogLevel("debug");
1615
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1331
1616
  }
1332
1617
  else if (data.params.value === 'Info') {
1333
- await this.matterbridge.setLogLevel("info");
1618
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1334
1619
  }
1335
1620
  else if (data.params.value === 'Notice') {
1336
- await this.matterbridge.setLogLevel("notice");
1621
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1337
1622
  }
1338
1623
  else if (data.params.value === 'Warn') {
1339
- await this.matterbridge.setLogLevel("warn");
1624
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1340
1625
  }
1341
1626
  else if (data.params.value === 'Error') {
1342
- await this.matterbridge.setLogLevel("error");
1627
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1343
1628
  }
1344
1629
  else if (data.params.value === 'Fatal') {
1345
- await this.matterbridge.setLogLevel("fatal");
1630
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1346
1631
  }
1347
1632
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1348
1633
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1353,6 +1638,7 @@ export class Frontend extends EventEmitter {
1353
1638
  this.log.debug('Matterbridge file log:', data.params.value);
1354
1639
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1355
1640
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1641
+ // Create the file logger for matterbridge
1356
1642
  if (data.params.value)
1357
1643
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1358
1644
  else
@@ -1399,6 +1685,7 @@ export class Frontend extends EventEmitter {
1399
1685
  });
1400
1686
  }
1401
1687
  catch (error) {
1688
+ /* istanbul ignore next */
1402
1689
  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
1690
  }
1404
1691
  }
@@ -1407,6 +1694,7 @@ export class Frontend extends EventEmitter {
1407
1694
  Logger.removeLogger('matterfilelogger');
1408
1695
  }
1409
1696
  catch (error) {
1697
+ /* istanbul ignore next */
1410
1698
  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
1699
  }
1412
1700
  }
@@ -1519,15 +1807,19 @@ export class Frontend extends EventEmitter {
1519
1807
  return;
1520
1808
  }
1521
1809
  const config = plugin.configJson;
1810
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1522
1811
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1812
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1523
1813
  if (select === 'serial')
1524
1814
  this.log.info(`Selected device serial ${data.params.serial}`);
1525
1815
  if (select === 'name')
1526
1816
  this.log.info(`Selected device name ${data.params.name}`);
1527
1817
  if (config && select && (select === 'serial' || select === 'name')) {
1818
+ // Remove postfix from the serial if it exists
1528
1819
  if (config.postfix) {
1529
1820
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1530
1821
  }
1822
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1531
1823
  if (isValidArray(config.whiteList, 1)) {
1532
1824
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1533
1825
  config.whiteList.push(data.params.serial);
@@ -1536,6 +1828,7 @@ export class Frontend extends EventEmitter {
1536
1828
  config.whiteList.push(data.params.name);
1537
1829
  }
1538
1830
  }
1831
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1539
1832
  if (isValidArray(config.blackList, 1)) {
1540
1833
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1541
1834
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1566,7 +1859,9 @@ export class Frontend extends EventEmitter {
1566
1859
  return;
1567
1860
  }
1568
1861
  const config = plugin.configJson;
1862
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1569
1863
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1864
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1570
1865
  if (select === 'serial')
1571
1866
  this.log.info(`Unselected device serial ${data.params.serial}`);
1572
1867
  if (select === 'name')
@@ -1575,6 +1870,7 @@ export class Frontend extends EventEmitter {
1575
1870
  if (config.postfix) {
1576
1871
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1577
1872
  }
1873
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1578
1874
  if (isValidArray(config.whiteList, 1)) {
1579
1875
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1580
1876
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1583,6 +1879,7 @@ export class Frontend extends EventEmitter {
1583
1879
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1584
1880
  }
1585
1881
  }
1882
+ // Add the serial to the blackList
1586
1883
  if (isValidArray(config.blackList)) {
1587
1884
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1588
1885
  config.blackList.push(data.params.serial);
@@ -1616,114 +1913,230 @@ export class Frontend extends EventEmitter {
1616
1913
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1617
1914
  }
1618
1915
  }
1916
+ /**
1917
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1918
+ *
1919
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1920
+ * @param {string} time - The time string of the message
1921
+ * @param {string} name - The logger name of the message
1922
+ * @param {string} message - The content of the message.
1923
+ *
1924
+ * @remarks
1925
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1926
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1927
+ * The function sends the message to all connected clients.
1928
+ */
1619
1929
  wssSendMessage(level, time, name, message) {
1620
1930
  if (!level || !time || !name || !message)
1621
1931
  return;
1932
+ // Remove ANSI escape codes from the message
1933
+ // eslint-disable-next-line no-control-regex
1622
1934
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1935
+ // Remove leading asterisks from the message
1623
1936
  message = message.replace(/^\*+/, '');
1937
+ // Replace all occurrences of \t and \n
1624
1938
  message = message.replace(/[\t\n]/g, '');
1939
+ // Remove non-printable characters
1940
+ // eslint-disable-next-line no-control-regex
1625
1941
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1942
+ // Replace all occurrences of \" with "
1626
1943
  message = message.replace(/\\"/g, '"');
1944
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1627
1945
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1946
+ // Define the maximum allowed length for continuous characters without a space
1628
1947
  const maxContinuousLength = 100;
1629
1948
  const keepStartLength = 20;
1630
1949
  const keepEndLength = 20;
1950
+ // Split the message into words
1631
1951
  message = message
1632
1952
  .split(' ')
1633
1953
  .map((word) => {
1954
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1634
1955
  if (word.length > maxContinuousLength) {
1635
1956
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1636
1957
  }
1637
1958
  return word;
1638
1959
  })
1639
1960
  .join(' ');
1961
+ // Send the message to all connected clients
1640
1962
  this.webSocketServer?.clients.forEach((client) => {
1641
1963
  if (client.readyState === WebSocket.OPEN) {
1642
1964
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1643
1965
  }
1644
1966
  });
1645
1967
  }
1968
+ /**
1969
+ * Sends a need to refresh WebSocket message to all connected clients.
1970
+ *
1971
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1972
+ * possible values:
1973
+ * - 'matterbridgeLatestVersion'
1974
+ * - 'matterbridgeAdvertise'
1975
+ * - 'online'
1976
+ * - 'offline'
1977
+ * - 'reachability'
1978
+ * - 'settings'
1979
+ * - 'plugins'
1980
+ * - 'pluginsRestart'
1981
+ * - 'devices'
1982
+ * - 'fabrics'
1983
+ * - 'sessions'
1984
+ */
1646
1985
  wssSendRefreshRequired(changed = null) {
1647
1986
  this.log.debug('Sending a refresh required message to all connected clients');
1987
+ // Send the message to all connected clients
1648
1988
  this.webSocketServer?.clients.forEach((client) => {
1649
1989
  if (client.readyState === WebSocket.OPEN) {
1650
1990
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1651
1991
  }
1652
1992
  });
1653
1993
  }
1994
+ /**
1995
+ * Sends a need to restart WebSocket message to all connected clients.
1996
+ *
1997
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients.
1998
+ */
1654
1999
  wssSendRestartRequired(snackbar = true) {
1655
2000
  this.log.debug('Sending a restart required message to all connected clients');
1656
2001
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1657
2002
  if (snackbar === true)
1658
2003
  this.wssSendSnackbarMessage(`Restart required`, 0);
2004
+ // Send the message to all connected clients
1659
2005
  this.webSocketServer?.clients.forEach((client) => {
1660
2006
  if (client.readyState === WebSocket.OPEN) {
1661
2007
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1662
2008
  }
1663
2009
  });
1664
2010
  }
2011
+ /**
2012
+ * Sends a need to update WebSocket message to all connected clients.
2013
+ *
2014
+ */
1665
2015
  wssSendUpdateRequired() {
1666
2016
  this.log.debug('Sending an update required message to all connected clients');
1667
2017
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2018
+ // Send the message to all connected clients
1668
2019
  this.webSocketServer?.clients.forEach((client) => {
1669
2020
  if (client.readyState === WebSocket.OPEN) {
1670
2021
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1671
2022
  }
1672
2023
  });
1673
2024
  }
2025
+ /**
2026
+ * Sends a cpu update message to all connected clients.
2027
+ *
2028
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2029
+ */
1674
2030
  wssSendCpuUpdate(cpuUsage) {
1675
2031
  if (hasParameter('debug'))
1676
2032
  this.log.debug('Sending a cpu update message to all connected clients');
2033
+ // Send the message to all connected clients
1677
2034
  this.webSocketServer?.clients.forEach((client) => {
1678
2035
  if (client.readyState === WebSocket.OPEN) {
1679
2036
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1680
2037
  }
1681
2038
  });
1682
2039
  }
2040
+ /**
2041
+ * Sends a memory update message to all connected clients.
2042
+ *
2043
+ * @param {string} totalMemory - The total memory in bytes.
2044
+ * @param {string} freeMemory - The free memory in bytes.
2045
+ * @param {string} rss - The resident set size in bytes.
2046
+ * @param {string} heapTotal - The total heap memory in bytes.
2047
+ * @param {string} heapUsed - The used heap memory in bytes.
2048
+ * @param {string} external - The external memory in bytes.
2049
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2050
+ */
1683
2051
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1684
2052
  if (hasParameter('debug'))
1685
2053
  this.log.debug('Sending a memory update message to all connected clients');
2054
+ // Send the message to all connected clients
1686
2055
  this.webSocketServer?.clients.forEach((client) => {
1687
2056
  if (client.readyState === WebSocket.OPEN) {
1688
2057
  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
2058
  }
1690
2059
  });
1691
2060
  }
2061
+ /**
2062
+ * Sends an uptime update message to all connected clients.
2063
+ *
2064
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2065
+ * @param {string} processUptime - The process uptime in a human-readable format.
2066
+ */
1692
2067
  wssSendUptimeUpdate(systemUptime, processUptime) {
1693
2068
  if (hasParameter('debug'))
1694
2069
  this.log.debug('Sending a uptime update message to all connected clients');
2070
+ // Send the message to all connected clients
1695
2071
  this.webSocketServer?.clients.forEach((client) => {
1696
2072
  if (client.readyState === WebSocket.OPEN) {
1697
2073
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1698
2074
  }
1699
2075
  });
1700
2076
  }
2077
+ /**
2078
+ * Sends an open snackbar message to all connected clients.
2079
+ *
2080
+ * @param {string} message - The message to send.
2081
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2082
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2083
+ */
1701
2084
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1702
2085
  this.log.debug('Sending a snackbar message to all connected clients');
2086
+ // Send the message to all connected clients
1703
2087
  this.webSocketServer?.clients.forEach((client) => {
1704
2088
  if (client.readyState === WebSocket.OPEN) {
1705
2089
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1706
2090
  }
1707
2091
  });
1708
2092
  }
2093
+ /**
2094
+ * Sends a close snackbar message to all connected clients.
2095
+ *
2096
+ * @param {string} message - The message to send.
2097
+ */
1709
2098
  wssSendCloseSnackbarMessage(message) {
1710
2099
  this.log.debug('Sending a close snackbar message to all connected clients');
2100
+ // Send the message to all connected clients
1711
2101
  this.webSocketServer?.clients.forEach((client) => {
1712
2102
  if (client.readyState === WebSocket.OPEN) {
1713
2103
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1714
2104
  }
1715
2105
  });
1716
2106
  }
2107
+ /**
2108
+ * Sends an attribute update message to all connected WebSocket clients.
2109
+ *
2110
+ * @param {string | undefined} plugin - The name of the plugin.
2111
+ * @param {string | undefined} serialNumber - The serial number of the device.
2112
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2113
+ * @param {string} cluster - The cluster name where the attribute belongs.
2114
+ * @param {string} attribute - The name of the attribute that changed.
2115
+ * @param {number | string | boolean} value - The new value of the attribute.
2116
+ *
2117
+ * @remarks
2118
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2119
+ * with the updated attribute information.
2120
+ */
1717
2121
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1718
2122
  this.log.debug('Sending an attribute update message to all connected clients');
2123
+ // Send the message to all connected clients
1719
2124
  this.webSocketServer?.clients.forEach((client) => {
1720
2125
  if (client.readyState === WebSocket.OPEN) {
1721
2126
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1722
2127
  }
1723
2128
  });
1724
2129
  }
2130
+ /**
2131
+ * Sends a message to all connected clients.
2132
+ *
2133
+ * @param {number} id - The message id.
2134
+ * @param {string} method - The message method.
2135
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2136
+ */
1725
2137
  wssBroadcastMessage(id, method, params) {
1726
2138
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2139
+ // Send the message to all connected clients
1727
2140
  this.webSocketServer?.clients.forEach((client) => {
1728
2141
  if (client.readyState === WebSocket.OPEN) {
1729
2142
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1731,3 +2144,4 @@ export class Frontend extends EventEmitter {
1731
2144
  });
1732
2145
  }
1733
2146
  }
2147
+ //# sourceMappingURL=frontend.js.map