matterbridge 3.3.5-dev-20251031-3482891 → 3.3.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 (305) hide show
  1. package/CHANGELOG.md +4 -3
  2. package/README-SERVICE-LOCAL.md +13 -13
  3. package/README.md +3 -1
  4. package/dist/broadcastServer.d.ts +112 -0
  5. package/dist/broadcastServer.d.ts.map +1 -0
  6. package/dist/broadcastServer.js +92 -1
  7. package/dist/broadcastServer.js.map +1 -0
  8. package/dist/broadcastServerTypes.d.ts +803 -0
  9. package/dist/broadcastServerTypes.d.ts.map +1 -0
  10. package/dist/broadcastServerTypes.js +24 -0
  11. package/dist/broadcastServerTypes.js.map +1 -0
  12. package/dist/cli.d.ts +30 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +97 -1
  15. package/dist/cli.js.map +1 -0
  16. package/dist/cliEmitter.d.ts +50 -0
  17. package/dist/cliEmitter.d.ts.map +1 -0
  18. package/dist/cliEmitter.js +37 -0
  19. package/dist/cliEmitter.js.map +1 -0
  20. package/dist/cliHistory.d.ts +48 -0
  21. package/dist/cliHistory.d.ts.map +1 -0
  22. package/dist/cliHistory.js +38 -0
  23. package/dist/cliHistory.js.map +1 -0
  24. package/dist/clusters/export.d.ts +2 -0
  25. package/dist/clusters/export.d.ts.map +1 -0
  26. package/dist/clusters/export.js +2 -0
  27. package/dist/clusters/export.js.map +1 -0
  28. package/dist/defaultConfigSchema.d.ts +28 -0
  29. package/dist/defaultConfigSchema.d.ts.map +1 -0
  30. package/dist/defaultConfigSchema.js +24 -0
  31. package/dist/defaultConfigSchema.js.map +1 -0
  32. package/dist/deviceManager.d.ts +117 -0
  33. package/dist/deviceManager.d.ts.map +1 -0
  34. package/dist/deviceManager.js +124 -1
  35. package/dist/deviceManager.js.map +1 -0
  36. package/dist/devices/airConditioner.d.ts +98 -0
  37. package/dist/devices/airConditioner.d.ts.map +1 -0
  38. package/dist/devices/airConditioner.js +57 -0
  39. package/dist/devices/airConditioner.js.map +1 -0
  40. package/dist/devices/batteryStorage.d.ts +48 -0
  41. package/dist/devices/batteryStorage.d.ts.map +1 -0
  42. package/dist/devices/batteryStorage.js +48 -1
  43. package/dist/devices/batteryStorage.js.map +1 -0
  44. package/dist/devices/cooktop.d.ts +60 -0
  45. package/dist/devices/cooktop.d.ts.map +1 -0
  46. package/dist/devices/cooktop.js +55 -0
  47. package/dist/devices/cooktop.js.map +1 -0
  48. package/dist/devices/dishwasher.d.ts +71 -0
  49. package/dist/devices/dishwasher.d.ts.map +1 -0
  50. package/dist/devices/dishwasher.js +57 -0
  51. package/dist/devices/dishwasher.js.map +1 -0
  52. package/dist/devices/evse.d.ts +76 -0
  53. package/dist/devices/evse.d.ts.map +1 -0
  54. package/dist/devices/evse.js +74 -10
  55. package/dist/devices/evse.js.map +1 -0
  56. package/dist/devices/export.d.ts +17 -0
  57. package/dist/devices/export.d.ts.map +1 -0
  58. package/dist/devices/export.js +5 -0
  59. package/dist/devices/export.js.map +1 -0
  60. package/dist/devices/extractorHood.d.ts +46 -0
  61. package/dist/devices/extractorHood.d.ts.map +1 -0
  62. package/dist/devices/extractorHood.js +42 -0
  63. package/dist/devices/extractorHood.js.map +1 -0
  64. package/dist/devices/heatPump.d.ts +47 -0
  65. package/dist/devices/heatPump.d.ts.map +1 -0
  66. package/dist/devices/heatPump.js +50 -2
  67. package/dist/devices/heatPump.js.map +1 -0
  68. package/dist/devices/laundryDryer.d.ts +67 -0
  69. package/dist/devices/laundryDryer.d.ts.map +1 -0
  70. package/dist/devices/laundryDryer.js +62 -3
  71. package/dist/devices/laundryDryer.js.map +1 -0
  72. package/dist/devices/laundryWasher.d.ts +81 -0
  73. package/dist/devices/laundryWasher.d.ts.map +1 -0
  74. package/dist/devices/laundryWasher.js +70 -4
  75. package/dist/devices/laundryWasher.js.map +1 -0
  76. package/dist/devices/microwaveOven.d.ts +168 -0
  77. package/dist/devices/microwaveOven.d.ts.map +1 -0
  78. package/dist/devices/microwaveOven.js +88 -5
  79. package/dist/devices/microwaveOven.js.map +1 -0
  80. package/dist/devices/oven.d.ts +105 -0
  81. package/dist/devices/oven.d.ts.map +1 -0
  82. package/dist/devices/oven.js +85 -0
  83. package/dist/devices/oven.js.map +1 -0
  84. package/dist/devices/refrigerator.d.ts +118 -0
  85. package/dist/devices/refrigerator.d.ts.map +1 -0
  86. package/dist/devices/refrigerator.js +102 -0
  87. package/dist/devices/refrigerator.js.map +1 -0
  88. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  89. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  90. package/dist/devices/roboticVacuumCleaner.js +100 -9
  91. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  92. package/dist/devices/solarPower.d.ts +40 -0
  93. package/dist/devices/solarPower.d.ts.map +1 -0
  94. package/dist/devices/solarPower.js +38 -0
  95. package/dist/devices/solarPower.js.map +1 -0
  96. package/dist/devices/speaker.d.ts +87 -0
  97. package/dist/devices/speaker.d.ts.map +1 -0
  98. package/dist/devices/speaker.js +84 -0
  99. package/dist/devices/speaker.js.map +1 -0
  100. package/dist/devices/temperatureControl.d.ts +166 -0
  101. package/dist/devices/temperatureControl.d.ts.map +1 -0
  102. package/dist/devices/temperatureControl.js +24 -3
  103. package/dist/devices/temperatureControl.js.map +1 -0
  104. package/dist/devices/waterHeater.d.ts +111 -0
  105. package/dist/devices/waterHeater.d.ts.map +1 -0
  106. package/dist/devices/waterHeater.js +82 -2
  107. package/dist/devices/waterHeater.js.map +1 -0
  108. package/dist/dgram/coap.d.ts +205 -0
  109. package/dist/dgram/coap.d.ts.map +1 -0
  110. package/dist/dgram/coap.js +126 -13
  111. package/dist/dgram/coap.js.map +1 -0
  112. package/dist/dgram/dgram.d.ts +141 -0
  113. package/dist/dgram/dgram.d.ts.map +1 -0
  114. package/dist/dgram/dgram.js +114 -2
  115. package/dist/dgram/dgram.js.map +1 -0
  116. package/dist/dgram/mb_coap.d.ts +24 -0
  117. package/dist/dgram/mb_coap.d.ts.map +1 -0
  118. package/dist/dgram/mb_coap.js +41 -3
  119. package/dist/dgram/mb_coap.js.map +1 -0
  120. package/dist/dgram/mb_mdns.d.ts +24 -0
  121. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  122. package/dist/dgram/mb_mdns.js +80 -15
  123. package/dist/dgram/mb_mdns.js.map +1 -0
  124. package/dist/dgram/mdns.d.ts +290 -0
  125. package/dist/dgram/mdns.d.ts.map +1 -0
  126. package/dist/dgram/mdns.js +299 -137
  127. package/dist/dgram/mdns.js.map +1 -0
  128. package/dist/dgram/multicast.d.ts +67 -0
  129. package/dist/dgram/multicast.d.ts.map +1 -0
  130. package/dist/dgram/multicast.js +62 -1
  131. package/dist/dgram/multicast.js.map +1 -0
  132. package/dist/dgram/unicast.d.ts +56 -0
  133. package/dist/dgram/unicast.d.ts.map +1 -0
  134. package/dist/dgram/unicast.js +54 -0
  135. package/dist/dgram/unicast.js.map +1 -0
  136. package/dist/frontend.d.ts +236 -0
  137. package/dist/frontend.d.ts.map +1 -0
  138. package/dist/frontend.js +431 -34
  139. package/dist/frontend.js.map +1 -0
  140. package/dist/frontendTypes.d.ts +529 -0
  141. package/dist/frontendTypes.d.ts.map +1 -0
  142. package/dist/frontendTypes.js +45 -0
  143. package/dist/frontendTypes.js.map +1 -0
  144. package/dist/helpers.d.ts +48 -0
  145. package/dist/helpers.d.ts.map +1 -0
  146. package/dist/helpers.js +53 -0
  147. package/dist/helpers.js.map +1 -0
  148. package/dist/index.d.ts +33 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +25 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/logger/export.d.ts +2 -0
  153. package/dist/logger/export.d.ts.map +1 -0
  154. package/dist/logger/export.js +1 -0
  155. package/dist/logger/export.js.map +1 -0
  156. package/dist/matter/behaviors.d.ts +2 -0
  157. package/dist/matter/behaviors.d.ts.map +1 -0
  158. package/dist/matter/behaviors.js +2 -0
  159. package/dist/matter/behaviors.js.map +1 -0
  160. package/dist/matter/clusters.d.ts +2 -0
  161. package/dist/matter/clusters.d.ts.map +1 -0
  162. package/dist/matter/clusters.js +2 -0
  163. package/dist/matter/clusters.js.map +1 -0
  164. package/dist/matter/devices.d.ts +2 -0
  165. package/dist/matter/devices.d.ts.map +1 -0
  166. package/dist/matter/devices.js +2 -0
  167. package/dist/matter/devices.js.map +1 -0
  168. package/dist/matter/endpoints.d.ts +2 -0
  169. package/dist/matter/endpoints.d.ts.map +1 -0
  170. package/dist/matter/endpoints.js +2 -0
  171. package/dist/matter/endpoints.js.map +1 -0
  172. package/dist/matter/export.d.ts +5 -0
  173. package/dist/matter/export.d.ts.map +1 -0
  174. package/dist/matter/export.js +3 -0
  175. package/dist/matter/export.js.map +1 -0
  176. package/dist/matter/types.d.ts +3 -0
  177. package/dist/matter/types.d.ts.map +1 -0
  178. package/dist/matter/types.js +3 -0
  179. package/dist/matter/types.js.map +1 -0
  180. package/dist/matterbridge.d.ts +476 -0
  181. package/dist/matterbridge.d.ts.map +1 -0
  182. package/dist/matterbridge.js +828 -46
  183. package/dist/matterbridge.js.map +1 -0
  184. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  185. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  186. package/dist/matterbridgeAccessoryPlatform.js +37 -0
  187. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  188. package/dist/matterbridgeBehaviors.d.ts +2404 -0
  189. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  190. package/dist/matterbridgeBehaviors.js +68 -5
  191. package/dist/matterbridgeBehaviors.js.map +1 -0
  192. package/dist/matterbridgeDeviceTypes.d.ts +770 -0
  193. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  194. package/dist/matterbridgeDeviceTypes.js +638 -17
  195. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  196. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  197. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  198. package/dist/matterbridgeDynamicPlatform.js +37 -0
  199. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  200. package/dist/matterbridgeEndpoint.d.ts +1556 -0
  201. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  202. package/dist/matterbridgeEndpoint.js +1408 -52
  203. package/dist/matterbridgeEndpoint.js.map +1 -0
  204. package/dist/matterbridgeEndpointHelpers.d.ts +758 -0
  205. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  206. package/dist/matterbridgeEndpointHelpers.js +464 -19
  207. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  208. package/dist/matterbridgePlatform.d.ts +402 -0
  209. package/dist/matterbridgePlatform.d.ts.map +1 -0
  210. package/dist/matterbridgePlatform.js +341 -1
  211. package/dist/matterbridgePlatform.js.map +1 -0
  212. package/dist/matterbridgeTypes.d.ts +226 -0
  213. package/dist/matterbridgeTypes.d.ts.map +1 -0
  214. package/dist/matterbridgeTypes.js +26 -0
  215. package/dist/matterbridgeTypes.js.map +1 -0
  216. package/dist/pluginManager.d.ts +347 -0
  217. package/dist/pluginManager.d.ts.map +1 -0
  218. package/dist/pluginManager.js +319 -3
  219. package/dist/pluginManager.js.map +1 -0
  220. package/dist/shelly.d.ts +174 -0
  221. package/dist/shelly.d.ts.map +1 -0
  222. package/dist/shelly.js +168 -7
  223. package/dist/shelly.js.map +1 -0
  224. package/dist/storage/export.d.ts +2 -0
  225. package/dist/storage/export.d.ts.map +1 -0
  226. package/dist/storage/export.js +1 -0
  227. package/dist/storage/export.js.map +1 -0
  228. package/dist/update.d.ts +75 -0
  229. package/dist/update.d.ts.map +1 -0
  230. package/dist/update.js +69 -0
  231. package/dist/update.js.map +1 -0
  232. package/dist/utils/colorUtils.d.ts +101 -0
  233. package/dist/utils/colorUtils.d.ts.map +1 -0
  234. package/dist/utils/colorUtils.js +97 -2
  235. package/dist/utils/colorUtils.js.map +1 -0
  236. package/dist/utils/commandLine.d.ts +66 -0
  237. package/dist/utils/commandLine.d.ts.map +1 -0
  238. package/dist/utils/commandLine.js +60 -0
  239. package/dist/utils/commandLine.js.map +1 -0
  240. package/dist/utils/copyDirectory.d.ts +33 -0
  241. package/dist/utils/copyDirectory.d.ts.map +1 -0
  242. package/dist/utils/copyDirectory.js +38 -1
  243. package/dist/utils/copyDirectory.js.map +1 -0
  244. package/dist/utils/createDirectory.d.ts +34 -0
  245. package/dist/utils/createDirectory.d.ts.map +1 -0
  246. package/dist/utils/createDirectory.js +33 -0
  247. package/dist/utils/createDirectory.js.map +1 -0
  248. package/dist/utils/createZip.d.ts +39 -0
  249. package/dist/utils/createZip.d.ts.map +1 -0
  250. package/dist/utils/createZip.js +47 -2
  251. package/dist/utils/createZip.js.map +1 -0
  252. package/dist/utils/deepCopy.d.ts +32 -0
  253. package/dist/utils/deepCopy.d.ts.map +1 -0
  254. package/dist/utils/deepCopy.js +39 -0
  255. package/dist/utils/deepCopy.js.map +1 -0
  256. package/dist/utils/deepEqual.d.ts +54 -0
  257. package/dist/utils/deepEqual.d.ts.map +1 -0
  258. package/dist/utils/deepEqual.js +72 -1
  259. package/dist/utils/deepEqual.js.map +1 -0
  260. package/dist/utils/error.d.ts +44 -0
  261. package/dist/utils/error.d.ts.map +1 -0
  262. package/dist/utils/error.js +41 -0
  263. package/dist/utils/error.js.map +1 -0
  264. package/dist/utils/export.d.ts +13 -0
  265. package/dist/utils/export.d.ts.map +1 -0
  266. package/dist/utils/export.js +1 -0
  267. package/dist/utils/export.js.map +1 -0
  268. package/dist/utils/format.d.ts +53 -0
  269. package/dist/utils/format.d.ts.map +1 -0
  270. package/dist/utils/format.js +49 -0
  271. package/dist/utils/format.js.map +1 -0
  272. package/dist/utils/hex.d.ts +89 -0
  273. package/dist/utils/hex.d.ts.map +1 -0
  274. package/dist/utils/hex.js +124 -0
  275. package/dist/utils/hex.js.map +1 -0
  276. package/dist/utils/inspector.d.ts +87 -0
  277. package/dist/utils/inspector.d.ts.map +1 -0
  278. package/dist/utils/inspector.js +69 -1
  279. package/dist/utils/inspector.js.map +1 -0
  280. package/dist/utils/isvalid.d.ts +103 -0
  281. package/dist/utils/isvalid.d.ts.map +1 -0
  282. package/dist/utils/isvalid.js +101 -0
  283. package/dist/utils/isvalid.js.map +1 -0
  284. package/dist/utils/jestHelpers.d.ts +139 -0
  285. package/dist/utils/jestHelpers.d.ts.map +1 -0
  286. package/dist/utils/jestHelpers.js +153 -3
  287. package/dist/utils/jestHelpers.js.map +1 -0
  288. package/dist/utils/network.d.ts +101 -0
  289. package/dist/utils/network.d.ts.map +1 -0
  290. package/dist/utils/network.js +96 -5
  291. package/dist/utils/network.js.map +1 -0
  292. package/dist/utils/spawn.d.ts +35 -0
  293. package/dist/utils/spawn.d.ts.map +1 -0
  294. package/dist/utils/spawn.js +71 -0
  295. package/dist/utils/spawn.js.map +1 -0
  296. package/dist/utils/tracker.d.ts +108 -0
  297. package/dist/utils/tracker.d.ts.map +1 -0
  298. package/dist/utils/tracker.js +64 -1
  299. package/dist/utils/tracker.js.map +1 -0
  300. package/dist/utils/wait.d.ts +54 -0
  301. package/dist/utils/wait.d.ts.map +1 -0
  302. package/dist/utils/wait.js +60 -8
  303. package/dist/utils/wait.js.map +1 -0
  304. package/npm-shrinkwrap.json +2 -2
  305. package/package.json +2 -1
package/dist/frontend.js CHANGED
@@ -1,8 +1,34 @@
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.3.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
+ // eslint-disable-next-line no-console
1
25
  if (process.argv.includes('--loader') || process.argv.includes('-loader'))
2
26
  console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
27
+ // Node modules
3
28
  import os from 'node:os';
4
29
  import path from 'node:path';
5
30
  import EventEmitter from 'node:events';
31
+ // AnsiLogger module
6
32
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
7
33
  import { Logger, Diagnostic, LogDestination, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/general';
8
34
  import { DeviceAdvertiser, DeviceCommissioner, FabricManager } from '@matter/protocol';
@@ -35,7 +61,7 @@ export class Frontend extends EventEmitter {
35
61
  constructor(matterbridge) {
36
62
  super();
37
63
  this.matterbridge = matterbridge;
38
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
64
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
39
65
  this.log.logNameColor = '\x1b[38;5;97m';
40
66
  this.server = new BroadcastServer('frontend', this.log);
41
67
  this.server.on('broadcast_message', this.msgHandler.bind(this));
@@ -130,23 +156,53 @@ export class Frontend extends EventEmitter {
130
156
  this.port = port;
131
157
  this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
132
158
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
159
+ // Initialize multer with the upload directory
133
160
  const multer = await import('multer');
134
- const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
161
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
135
162
  const upload = multer.default({ dest: uploadDir });
163
+ // Create the express app that serves the frontend
136
164
  const express = await import('express');
137
165
  this.expressApp = express.default();
166
+ // Inject logging/debug wrapper for route/middleware registration
167
+ /*
168
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
169
+ for (const method of methods) {
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
172
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
173
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
174
+ try {
175
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
176
+ return original(path, ...rest);
177
+ } catch (err) {
178
+ console.error(`[ERROR] Failed to register route: ${path}`);
179
+ throw err;
180
+ }
181
+ };
182
+ }
183
+ */
184
+ // Log all requests to the server for debugging
185
+ /*
186
+ this.expressApp.use((req, res, next) => {
187
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
188
+ next();
189
+ });
190
+ */
191
+ // Serve static files from 'frontend/build' directory
138
192
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
193
+ // Create a WebSocket server and attach it to the http or https server
139
194
  this.log.debug(`Creating WebSocketServer...`);
140
195
  const ws = await import('ws');
141
196
  this.webSocketServer = new ws.WebSocketServer({ noServer: true });
142
197
  this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
143
198
  this.webSocketServer.on('connection', (ws, request) => {
144
199
  const clientIp = request.socket.remoteAddress;
145
- let callbackLogLevel = "notice";
146
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
147
- callbackLogLevel = "info";
148
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
149
- callbackLogLevel = "debug";
200
+ // Set the global logger callback for the WebSocketServer
201
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
202
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
203
+ callbackLogLevel = "info" /* LogLevel.INFO */;
204
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
205
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
150
206
  AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
151
207
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
152
208
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -168,16 +224,25 @@ export class Frontend extends EventEmitter {
168
224
  }
169
225
  });
170
226
  ws.on('error', (error) => {
227
+ // istanbul ignore next
171
228
  this.log.error(`WebSocket client error: ${error}`);
172
229
  });
173
230
  });
174
231
  this.webSocketServer.on('close', () => {
175
232
  this.log.debug(`WebSocketServer closed`);
176
233
  });
234
+ /* With { noServer: true } it never fires
235
+ this.webSocketServer.on('listening', () => {
236
+ this.log.info(`The WebSocketServer is listening`);
237
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
238
+ });
239
+ */
240
+ // istanbul ignore next
177
241
  this.webSocketServer.on('error', (ws, error) => {
178
242
  this.log.error(`WebSocketServer error: ${error}`);
179
243
  });
180
244
  if (!hasParameter('ssl')) {
245
+ // Create an HTTP server and attach the express app
181
246
  const http = await import('node:http');
182
247
  try {
183
248
  this.log.debug(`Creating HTTP server...`);
@@ -188,6 +253,7 @@ export class Frontend extends EventEmitter {
188
253
  this.emit('server_error', error);
189
254
  return;
190
255
  }
256
+ // Listen on the specified port
191
257
  if (hasParameter('ingress')) {
192
258
  this.httpServer.listen(this.port, '0.0.0.0', () => {
193
259
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -207,23 +273,29 @@ export class Frontend extends EventEmitter {
207
273
  }
208
274
  this.httpServer.on('upgrade', async (req, socket, head) => {
209
275
  try {
276
+ // Only proceed for real WebSocket upgrades
277
+ // istanbul ignore next cause is only a safety check
210
278
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
211
279
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
212
280
  return socket.destroy();
213
281
  }
282
+ // Build a URL so we can read ?password=...
214
283
  const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
284
+ // Validate WebSocket password
215
285
  const password = url.searchParams.get('password') ?? '';
216
286
  if (password !== this.storedPassword) {
217
287
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
218
288
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
219
289
  return socket.destroy();
220
290
  }
291
+ // Complete the WebSocket handshake
221
292
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
222
293
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
223
294
  this.webSocketServer?.emit('connection', ws, req);
224
295
  });
225
296
  }
226
297
  catch (err) {
298
+ /* istanbul ignore next: only triggered on unexpected internal error */
227
299
  {
228
300
  inspectError(this.log, 'WebSocket upgrade error:', err);
229
301
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -246,6 +318,7 @@ export class Frontend extends EventEmitter {
246
318
  });
247
319
  }
248
320
  else {
321
+ // SSL is enabled, load the certificate and the private key
249
322
  let cert;
250
323
  let key;
251
324
  let ca;
@@ -255,6 +328,7 @@ export class Frontend extends EventEmitter {
255
328
  let httpsServerOptions = {};
256
329
  const fs = await import('node:fs');
257
330
  if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
331
+ // Load the p12 certificate and the passphrase
258
332
  try {
259
333
  pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
260
334
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
@@ -266,7 +340,7 @@ export class Frontend extends EventEmitter {
266
340
  }
267
341
  try {
268
342
  passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
269
- passphrase = passphrase.trim();
343
+ passphrase = passphrase.trim(); // Ensure no extra characters
270
344
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
271
345
  }
272
346
  catch (error) {
@@ -277,6 +351,7 @@ export class Frontend extends EventEmitter {
277
351
  httpsServerOptions = { pfx, passphrase };
278
352
  }
279
353
  else {
354
+ // 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.
280
355
  try {
281
356
  cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
282
357
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
@@ -306,9 +381,10 @@ export class Frontend extends EventEmitter {
306
381
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
307
382
  }
308
383
  if (hasParameter('mtls')) {
309
- httpsServerOptions.requestCert = true;
310
- httpsServerOptions.rejectUnauthorized = true;
384
+ httpsServerOptions.requestCert = true; // Request client certificate
385
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
311
386
  }
387
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
312
388
  const https = await import('node:https');
313
389
  try {
314
390
  this.log.debug(`Creating HTTPS server...`);
@@ -319,6 +395,7 @@ export class Frontend extends EventEmitter {
319
395
  this.emit('server_error', error);
320
396
  return;
321
397
  }
398
+ // Listen on the specified port
322
399
  if (hasParameter('ingress')) {
323
400
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
324
401
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -338,23 +415,29 @@ export class Frontend extends EventEmitter {
338
415
  }
339
416
  this.httpsServer.on('upgrade', async (req, socket, head) => {
340
417
  try {
418
+ // Only proceed for real WebSocket upgrades
419
+ // istanbul ignore next cause is only a safety check
341
420
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
342
421
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
343
422
  return socket.destroy();
344
423
  }
424
+ // Build a URL so we can read ?password=...
345
425
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
426
+ // Validate WebSocket password
346
427
  const password = url.searchParams.get('password') ?? '';
347
428
  if (password !== this.storedPassword) {
348
429
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
349
430
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
350
431
  return socket.destroy();
351
432
  }
433
+ // Complete the WebSocket handshake
352
434
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
353
435
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
354
436
  this.webSocketServer?.emit('connection', ws, req);
355
437
  });
356
438
  }
357
439
  catch (err) {
440
+ /* istanbul ignore next: only triggered on unexpected internal error */
358
441
  {
359
442
  inspectError(this.log, 'WebSocket upgrade error:', err);
360
443
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -376,6 +459,7 @@ export class Frontend extends EventEmitter {
376
459
  return;
377
460
  });
378
461
  }
462
+ // Subscribe to cli events
379
463
  cliEmitter.removeAllListeners();
380
464
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
381
465
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -386,6 +470,8 @@ export class Frontend extends EventEmitter {
386
470
  cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
387
471
  this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
388
472
  });
473
+ // Endpoint to validate login code
474
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
389
475
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
390
476
  const { password } = req.body;
391
477
  this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
@@ -398,17 +484,20 @@ export class Frontend extends EventEmitter {
398
484
  res.json({ valid: false });
399
485
  }
400
486
  });
487
+ // Endpoint to provide health check for docker
401
488
  this.expressApp.get('/health', (req, res) => {
402
489
  this.log.debug('Express received /health');
403
490
  const healthStatus = {
404
- status: 'ok',
405
- uptime: process.uptime(),
406
- timestamp: new Date().toISOString(),
491
+ status: 'ok', // Indicate service is healthy
492
+ uptime: process.uptime(), // Server uptime in seconds
493
+ timestamp: new Date().toISOString(), // Current timestamp
407
494
  };
408
495
  res.status(200).json(healthStatus);
409
496
  });
497
+ // Endpoint to provide memory usage details
410
498
  this.expressApp.get('/memory', async (req, res) => {
411
499
  this.log.debug('Express received /memory');
500
+ // Memory usage from process
412
501
  const memoryUsageRaw = process.memoryUsage();
413
502
  const memoryUsage = {
414
503
  rss: formatBytes(memoryUsageRaw.rss),
@@ -417,10 +506,13 @@ export class Frontend extends EventEmitter {
417
506
  external: formatBytes(memoryUsageRaw.external),
418
507
  arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
419
508
  };
509
+ // V8 heap statistics
420
510
  const { default: v8 } = await import('node:v8');
421
511
  const heapStatsRaw = v8.getHeapStatistics();
422
512
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
513
+ // Format heapStats
423
514
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
515
+ // Format heapSpaces
424
516
  const heapSpaces = heapSpacesRaw.map((space) => ({
425
517
  ...space,
426
518
  space_size: formatBytes(space.space_size),
@@ -439,18 +531,22 @@ export class Frontend extends EventEmitter {
439
531
  };
440
532
  res.status(200).json(memoryReport);
441
533
  });
534
+ // Endpoint to provide settings
442
535
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
443
536
  this.log.debug('The frontend sent /api/settings');
444
537
  res.json(await this.getApiSettings());
445
538
  });
539
+ // Endpoint to provide plugins
446
540
  this.expressApp.get('/api/plugins', async (req, res) => {
447
541
  this.log.debug('The frontend sent /api/plugins');
448
542
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
449
543
  });
544
+ // Endpoint to provide devices
450
545
  this.expressApp.get('/api/devices', async (req, res) => {
451
546
  this.log.debug('The frontend sent /api/devices');
452
547
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
453
548
  });
549
+ // Endpoint to view the matterbridge log
454
550
  this.expressApp.get('/api/view-mblog', async (req, res) => {
455
551
  this.log.debug('The frontend sent /api/view-mblog');
456
552
  try {
@@ -464,6 +560,7 @@ export class Frontend extends EventEmitter {
464
560
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
465
561
  }
466
562
  });
563
+ // Endpoint to view the matter.js log
467
564
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
468
565
  this.log.debug('The frontend sent /api/view-mjlog');
469
566
  try {
@@ -477,6 +574,7 @@ export class Frontend extends EventEmitter {
477
574
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
478
575
  }
479
576
  });
577
+ // Endpoint to view the diagnostic.log
480
578
  this.expressApp.get('/api/view-diagnostic', async (req, res) => {
481
579
  this.log.debug('The frontend sent /api/view-diagnostic');
482
580
  await this.generateDiagnostic();
@@ -487,10 +585,13 @@ export class Frontend extends EventEmitter {
487
585
  res.send(data.slice(29));
488
586
  }
489
587
  catch (error) {
588
+ // istanbul ignore next
490
589
  this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
590
+ // istanbul ignore next
491
591
  res.status(500).send('Error reading diagnostic log file.');
492
592
  }
493
593
  });
594
+ // Endpoint to download the diagnostic.log
494
595
  this.expressApp.get('/api/download-diagnostic', async (req, res) => {
495
596
  this.log.debug(`The frontend sent /api/download-diagnostic`);
496
597
  await this.generateDiagnostic();
@@ -501,16 +602,19 @@ export class Frontend extends EventEmitter {
501
602
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
502
603
  }
503
604
  catch (error) {
605
+ // istanbul ignore next
504
606
  this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
505
607
  }
506
608
  res.type('text/plain');
507
609
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
610
+ /* istanbul ignore if */
508
611
  if (error) {
509
612
  this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
510
613
  res.status(500).send('Error downloading the diagnostic log file');
511
614
  }
512
615
  });
513
616
  });
617
+ // Endpoint to view the history.html
514
618
  this.expressApp.get('/api/viewhistory', async (req, res) => {
515
619
  this.log.debug('The frontend sent /api/viewhistory');
516
620
  try {
@@ -524,6 +628,7 @@ export class Frontend extends EventEmitter {
524
628
  res.status(500).send('Error reading history file.');
525
629
  }
526
630
  });
631
+ // Endpoint to download the history.html
527
632
  this.expressApp.get('/api/downloadhistory', async (req, res) => {
528
633
  this.log.debug(`The frontend sent /api/downloadhistory`);
529
634
  try {
@@ -533,6 +638,7 @@ export class Frontend extends EventEmitter {
533
638
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
534
639
  res.type('text/plain');
535
640
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
641
+ /* istanbul ignore if */
536
642
  if (error) {
537
643
  this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
538
644
  res.status(500).send('Error downloading history file');
@@ -544,6 +650,7 @@ export class Frontend extends EventEmitter {
544
650
  res.status(500).send('Error reading history file.');
545
651
  }
546
652
  });
653
+ // Endpoint to view the shelly log
547
654
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
548
655
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
549
656
  try {
@@ -557,6 +664,7 @@ export class Frontend extends EventEmitter {
557
664
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
558
665
  }
559
666
  });
667
+ // Endpoint to download the matterbridge log
560
668
  this.expressApp.get('/api/download-mblog', async (req, res) => {
561
669
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
562
670
  const fs = await import('node:fs');
@@ -571,12 +679,14 @@ export class Frontend extends EventEmitter {
571
679
  }
572
680
  res.type('text/plain');
573
681
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
682
+ /* istanbul ignore if */
574
683
  if (error) {
575
684
  this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
576
685
  res.status(500).send('Error downloading the matterbridge log file');
577
686
  }
578
687
  });
579
688
  });
689
+ // Endpoint to download the matter log
580
690
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
581
691
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
582
692
  const fs = await import('node:fs');
@@ -591,12 +701,14 @@ export class Frontend extends EventEmitter {
591
701
  }
592
702
  res.type('text/plain');
593
703
  res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
704
+ /* istanbul ignore if */
594
705
  if (error) {
595
706
  this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
596
707
  res.status(500).send('Error downloading the matter log file');
597
708
  }
598
709
  });
599
710
  });
711
+ // Endpoint to download the shelly log
600
712
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
601
713
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
602
714
  const fs = await import('node:fs');
@@ -611,75 +723,91 @@ export class Frontend extends EventEmitter {
611
723
  }
612
724
  res.type('text/plain');
613
725
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
726
+ /* istanbul ignore if */
614
727
  if (error) {
615
728
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
616
729
  res.status(500).send('Error downloading Shelly system log file');
617
730
  }
618
731
  });
619
732
  });
733
+ // Endpoint to download the matterbridge storage directory
620
734
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
621
735
  this.log.debug('The frontend sent /api/download-mbstorage');
622
736
  await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
623
737
  res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
738
+ /* istanbul ignore if */
624
739
  if (error) {
625
740
  this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
626
741
  res.status(500).send('Error downloading the matterbridge storage file');
627
742
  }
628
743
  });
629
744
  });
745
+ // Endpoint to download the matter storage file
630
746
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
631
747
  this.log.debug('The frontend sent /api/download-mjstorage');
632
748
  await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
633
749
  res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
750
+ /* istanbul ignore if */
634
751
  if (error) {
635
752
  this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
636
753
  res.status(500).send('Error downloading the matter storage zip file');
637
754
  }
638
755
  });
639
756
  });
757
+ // Endpoint to download the matterbridge plugin directory
640
758
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
641
759
  this.log.debug('The frontend sent /api/download-pluginstorage');
642
760
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
643
761
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
762
+ /* istanbul ignore if */
644
763
  if (error) {
645
764
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
646
765
  res.status(500).send('Error downloading the matterbridge plugin storage file');
647
766
  }
648
767
  });
649
768
  });
769
+ // Endpoint to download the matterbridge plugin config files
650
770
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
651
771
  this.log.debug('The frontend sent /api/download-pluginconfig');
652
772
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
653
773
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
774
+ /* istanbul ignore if */
654
775
  if (error) {
655
776
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
656
777
  res.status(500).send('Error downloading the matterbridge plugin config file');
657
778
  }
658
779
  });
659
780
  });
781
+ // Endpoint to download the matterbridge backup (created with the backup command)
660
782
  this.expressApp.get('/api/download-backup', async (req, res) => {
661
783
  this.log.debug('The frontend sent /api/download-backup');
662
784
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
785
+ /* istanbul ignore if */
663
786
  if (error) {
664
787
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
665
788
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
666
789
  }
667
790
  });
668
791
  });
792
+ // Endpoint to upload a package
669
793
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
670
794
  const { filename } = req.body;
671
795
  const file = req.file;
796
+ /* istanbul ignore if */
672
797
  if (!file || !filename) {
673
798
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
674
799
  res.status(400).send('Invalid request: file and filename are required');
675
800
  return;
676
801
  }
677
802
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
803
+ // Define the path where the plugin file will be saved
678
804
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
679
805
  try {
806
+ // Move the uploaded file to the specified path
680
807
  const fs = await import('node:fs');
681
808
  await fs.promises.rename(file.path, filePath);
682
809
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
810
+ // Install the plugin package
683
811
  if (filename.endsWith('.tgz')) {
684
812
  const { spawnCommand } = await import('./utils/spawn.js');
685
813
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename);
@@ -699,6 +827,7 @@ export class Frontend extends EventEmitter {
699
827
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
700
828
  }
701
829
  });
830
+ // Fallback for routing (must be the last route)
702
831
  this.expressApp.use((req, res) => {
703
832
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
704
833
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -708,13 +837,16 @@ export class Frontend extends EventEmitter {
708
837
  async stop() {
709
838
  this.log.debug('Stopping the frontend...');
710
839
  const ws = await import('ws');
840
+ // Remove listeners from the express app
711
841
  if (this.expressApp) {
712
842
  this.expressApp.removeAllListeners();
713
843
  this.expressApp = undefined;
714
844
  this.log.debug('Frontend app closed successfully');
715
845
  }
846
+ // Close the WebSocket server
716
847
  if (this.webSocketServer) {
717
848
  this.log.debug('Closing WebSocket server...');
849
+ // Close all active connections
718
850
  this.webSocketServer.clients.forEach((client) => {
719
851
  if (client.readyState === ws.WebSocket.OPEN) {
720
852
  client.close();
@@ -723,6 +855,7 @@ export class Frontend extends EventEmitter {
723
855
  await withTimeout(new Promise((resolve) => {
724
856
  this.webSocketServer?.close((error) => {
725
857
  if (error) {
858
+ // istanbul ignore next
726
859
  this.log.error(`Error closing WebSocket server: ${error}`);
727
860
  }
728
861
  else {
@@ -735,8 +868,27 @@ export class Frontend extends EventEmitter {
735
868
  this.webSocketServer.removeAllListeners();
736
869
  this.webSocketServer = undefined;
737
870
  }
871
+ // Close the http server
738
872
  if (this.httpServer) {
739
873
  this.log.debug('Closing http server...');
874
+ /*
875
+ await withTimeout(
876
+ new Promise<void>((resolve) => {
877
+ this.httpServer?.close((error) => {
878
+ if (error) {
879
+ // istanbul ignore next
880
+ this.log.error(`Error closing http server: ${error}`);
881
+ } else {
882
+ this.log.debug('Http server closed successfully');
883
+ this.emit('server_stopped');
884
+ }
885
+ resolve();
886
+ });
887
+ }),
888
+ 5000,
889
+ false,
890
+ );
891
+ */
740
892
  this.httpServer.close();
741
893
  this.log.debug('Http server closed successfully');
742
894
  this.listening = false;
@@ -745,8 +897,27 @@ export class Frontend extends EventEmitter {
745
897
  this.httpServer = undefined;
746
898
  this.log.debug('Frontend http server closed successfully');
747
899
  }
900
+ // Close the https server
748
901
  if (this.httpsServer) {
749
902
  this.log.debug('Closing https server...');
903
+ /*
904
+ await withTimeout(
905
+ new Promise<void>((resolve) => {
906
+ this.httpsServer?.close((error) => {
907
+ if (error) {
908
+ // istanbul ignore next
909
+ this.log.error(`Error closing https server: ${error}`);
910
+ } else {
911
+ this.log.debug('Https server closed successfully');
912
+ this.emit('server_stopped');
913
+ }
914
+ resolve();
915
+ });
916
+ }),
917
+ 5000,
918
+ false,
919
+ );
920
+ */
750
921
  this.httpsServer.close();
751
922
  this.log.debug('Https server closed successfully');
752
923
  this.listening = false;
@@ -757,7 +928,13 @@ export class Frontend extends EventEmitter {
757
928
  }
758
929
  this.log.debug('Frontend stopped successfully');
759
930
  }
931
+ /**
932
+ * Retrieves the api settings data.
933
+ *
934
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
935
+ */
760
936
  async getApiSettings() {
937
+ // Update the variable system information properties
761
938
  this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
762
939
  this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
763
940
  this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
@@ -767,6 +944,7 @@ export class Frontend extends EventEmitter {
767
944
  this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
768
945
  this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
769
946
  this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
947
+ // Create the matterbridge information
770
948
  const info = {
771
949
  homeDirectory: this.matterbridge.homeDirectory,
772
950
  rootDirectory: this.matterbridge.rootDirectory,
@@ -802,9 +980,15 @@ export class Frontend extends EventEmitter {
802
980
  };
803
981
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
804
982
  }
983
+ /**
984
+ * Retrieves the reachable attribute.
985
+ *
986
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
987
+ * @returns {boolean} The reachable attribute.
988
+ */
805
989
  getReachability(device) {
806
990
  if (this.matterbridge.hasCleanupStarted)
807
- return false;
991
+ return false; // Skip if cleanup has started
808
992
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
809
993
  return false;
810
994
  if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
@@ -815,9 +999,15 @@ export class Frontend extends EventEmitter {
815
999
  return true;
816
1000
  return false;
817
1001
  }
1002
+ /**
1003
+ * Retrieves the power source attribute.
1004
+ *
1005
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1006
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1007
+ */
818
1008
  getPowerSource(endpoint) {
819
1009
  if (this.matterbridge.hasCleanupStarted)
820
- return;
1010
+ return; // Skip if cleanup has started
821
1011
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
822
1012
  return undefined;
823
1013
  const powerSource = (device) => {
@@ -832,16 +1022,25 @@ export class Frontend extends EventEmitter {
832
1022
  }
833
1023
  return;
834
1024
  };
1025
+ // Root endpoint
835
1026
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
836
1027
  return powerSource(endpoint);
1028
+ // Child endpoints
837
1029
  for (const child of endpoint.getChildEndpoints()) {
838
1030
  if (child.hasClusterServer(PowerSource.Cluster.id))
839
1031
  return powerSource(child);
840
1032
  }
841
1033
  }
1034
+ /**
1035
+ * Retrieves the cluster text description from a given device.
1036
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1037
+ *
1038
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1039
+ * @returns {string} The attributes description of the cluster servers in the device.
1040
+ */
842
1041
  getClusterTextFromDevice(device) {
843
1042
  if (this.matterbridge.hasCleanupStarted)
844
- return '';
1043
+ return ''; // Skip if cleanup has started
845
1044
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
846
1045
  return '';
847
1046
  const getUserLabel = (device) => {
@@ -851,6 +1050,7 @@ export class Frontend extends EventEmitter {
851
1050
  if (composed)
852
1051
  return 'Composed: ' + composed.value;
853
1052
  }
1053
+ // istanbul ignore next cause is not reachable
854
1054
  return '';
855
1055
  };
856
1056
  const getFixedLabel = (device) => {
@@ -860,11 +1060,13 @@ export class Frontend extends EventEmitter {
860
1060
  if (composed)
861
1061
  return 'Composed: ' + composed.value;
862
1062
  }
1063
+ // istanbul ignore next cause is not reacheable
863
1064
  return '';
864
1065
  };
865
1066
  let attributes = '';
866
1067
  let supportedModes = [];
867
1068
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1069
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
868
1070
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
869
1071
  return;
870
1072
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -954,11 +1156,17 @@ export class Frontend extends EventEmitter {
954
1156
  if (clusterName === 'userLabel' && attributeName === 'labelList')
955
1157
  attributes += `${getUserLabel(device)} `;
956
1158
  });
1159
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
957
1160
  return attributes.trimStart().trimEnd();
958
1161
  }
1162
+ /**
1163
+ * Retrieves the registered plugins sanitized for res.json().
1164
+ *
1165
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1166
+ */
959
1167
  getPlugins() {
960
1168
  if (this.matterbridge.hasCleanupStarted)
961
- return [];
1169
+ return []; // Skip if cleanup has started
962
1170
  const plugins = [];
963
1171
  for (const plugin of this.matterbridge.plugins.array()) {
964
1172
  plugins.push({
@@ -986,18 +1194,27 @@ export class Frontend extends EventEmitter {
986
1194
  schemaJson: plugin.schemaJson,
987
1195
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
988
1196
  hasBlackList: plugin.configJson?.blackList !== undefined,
1197
+ // Childbridge mode specific data
989
1198
  matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
990
1199
  });
991
1200
  }
992
1201
  return plugins;
993
1202
  }
1203
+ /**
1204
+ * Retrieves the devices from Matterbridge.
1205
+ *
1206
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1207
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1208
+ */
994
1209
  getDevices(pluginName) {
995
1210
  if (this.matterbridge.hasCleanupStarted)
996
- return [];
1211
+ return []; // Skip if cleanup has started
997
1212
  const devices = [];
998
1213
  for (const device of this.matterbridge.devices.array()) {
1214
+ // Filter by pluginName if provided
999
1215
  if (pluginName && pluginName !== device.plugin)
1000
1216
  continue;
1217
+ // Check if the device has the required properties
1001
1218
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1002
1219
  continue;
1003
1220
  devices.push({
@@ -1017,24 +1234,39 @@ export class Frontend extends EventEmitter {
1017
1234
  }
1018
1235
  return devices;
1019
1236
  }
1237
+ /**
1238
+ * Retrieves the clusters from a given plugin and endpoint number.
1239
+ *
1240
+ * Response for /api/clusters
1241
+ *
1242
+ * @param {string} pluginName - The name of the plugin.
1243
+ * @param {number} endpointNumber - The endpoint number.
1244
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1245
+ */
1020
1246
  getClusters(pluginName, endpointNumber) {
1021
1247
  if (this.matterbridge.hasCleanupStarted)
1022
- return;
1248
+ return; // Skip if cleanup has started
1023
1249
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1024
1250
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1025
1251
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1026
1252
  return;
1027
1253
  }
1254
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1255
+ // Get the device types from the main endpoint
1028
1256
  const deviceTypes = [];
1029
1257
  const clusters = [];
1030
1258
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1031
1259
  deviceTypes.push(d.deviceType);
1032
1260
  });
1261
+ // Get the clusters from the main endpoint
1033
1262
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1034
1263
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1035
1264
  return;
1036
1265
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1037
1266
  return;
1267
+ // console.log(
1268
+ // `${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}`,
1269
+ // );
1038
1270
  clusters.push({
1039
1271
  endpoint: endpoint.number.toString(),
1040
1272
  number: endpoint.number,
@@ -1048,12 +1280,19 @@ export class Frontend extends EventEmitter {
1048
1280
  attributeLocalValue: attributeValue,
1049
1281
  });
1050
1282
  });
1283
+ // Get the child endpoints
1051
1284
  const childEndpoints = endpoint.getChildEndpoints();
1285
+ // if (childEndpoints.length === 0) {
1286
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1287
+ // }
1052
1288
  childEndpoints.forEach((childEndpoint) => {
1289
+ // istanbul ignore if cause is not reachable: should never happen but ...
1053
1290
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1054
1291
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1055
1292
  return;
1056
1293
  }
1294
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1295
+ // Get the device types of the child endpoint
1057
1296
  const deviceTypes = [];
1058
1297
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1059
1298
  deviceTypes.push(d.deviceType);
@@ -1063,6 +1302,9 @@ export class Frontend extends EventEmitter {
1063
1302
  return;
1064
1303
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1065
1304
  return;
1305
+ // console.log(
1306
+ // `${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}`,
1307
+ // );
1066
1308
  clusters.push({
1067
1309
  endpoint: childEndpoint.number.toString(),
1068
1310
  number: childEndpoint.number,
@@ -1082,6 +1324,7 @@ export class Frontend extends EventEmitter {
1082
1324
  async generateDiagnostic() {
1083
1325
  this.log.debug('Generating diagnostic...');
1084
1326
  const serverNodes = [];
1327
+ // istanbul ignore else
1085
1328
  if (this.matterbridge.bridgeMode === 'bridge') {
1086
1329
  if (this.matterbridge.serverNode)
1087
1330
  serverNodes.push(this.matterbridge.serverNode);
@@ -1092,6 +1335,7 @@ export class Frontend extends EventEmitter {
1092
1335
  serverNodes.push(plugin.serverNode);
1093
1336
  }
1094
1337
  }
1338
+ // istanbul ignore next
1095
1339
  for (const device of this.matterbridge.devices.array()) {
1096
1340
  if (device.serverNode)
1097
1341
  serverNodes.push(device.serverNode);
@@ -1115,8 +1359,15 @@ export class Frontend extends EventEmitter {
1115
1359
  values: [...serverNodes],
1116
1360
  })));
1117
1361
  delete Logger.destinations.diagnostic;
1118
- await wait(500);
1362
+ await wait(500); // Wait for the log to be written
1119
1363
  }
1364
+ /**
1365
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1366
+ *
1367
+ * @param {WebSocket} client - The websocket client that sent the message.
1368
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1369
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1370
+ */
1120
1371
  async wsMessageHandler(client, message) {
1121
1372
  let data;
1122
1373
  const sendResponse = (data) => {
@@ -1136,7 +1387,7 @@ export class Frontend extends EventEmitter {
1136
1387
  };
1137
1388
  try {
1138
1389
  data = JSON.parse(message.toString());
1139
- if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
1390
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
1140
1391
  this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1141
1392
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1142
1393
  return;
@@ -1210,6 +1461,7 @@ export class Frontend extends EventEmitter {
1210
1461
  return;
1211
1462
  })
1212
1463
  .catch((_error) => {
1464
+ //
1213
1465
  });
1214
1466
  }
1215
1467
  else {
@@ -1257,6 +1509,7 @@ export class Frontend extends EventEmitter {
1257
1509
  return;
1258
1510
  })
1259
1511
  .catch((_error) => {
1512
+ //
1260
1513
  });
1261
1514
  }
1262
1515
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1282,6 +1535,7 @@ export class Frontend extends EventEmitter {
1282
1535
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1283
1536
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1284
1537
  if (plugin.serverNode) {
1538
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1285
1539
  await this.matterbridge.stopServerNode(plugin.serverNode);
1286
1540
  plugin.serverNode = undefined;
1287
1541
  }
@@ -1291,18 +1545,20 @@ export class Frontend extends EventEmitter {
1291
1545
  this.matterbridge.devices.remove(device);
1292
1546
  }
1293
1547
  }
1548
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1294
1549
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1295
1550
  await this.matterbridge.createDynamicPlugin(plugin);
1296
1551
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1297
- plugin.restartRequired = false;
1552
+ plugin.restartRequired = false; // Reset plugin restartRequired
1298
1553
  let needRestart = 0;
1299
1554
  for (const plugin of this.matterbridge.plugins) {
1300
1555
  if (plugin.restartRequired)
1301
1556
  needRestart++;
1302
1557
  }
1303
1558
  if (needRestart === 0) {
1304
- this.wssSendRestartNotRequired(true);
1559
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1305
1560
  }
1561
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1306
1562
  if (plugin.serverNode)
1307
1563
  await this.matterbridge.startServerNode(plugin.serverNode);
1308
1564
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1568,22 +1824,22 @@ export class Frontend extends EventEmitter {
1568
1824
  if (isValidString(data.params.value, 4)) {
1569
1825
  this.log.debug('Matterbridge logger level:', data.params.value);
1570
1826
  if (data.params.value === 'Debug') {
1571
- await this.matterbridge.setLogLevel("debug");
1827
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1572
1828
  }
1573
1829
  else if (data.params.value === 'Info') {
1574
- await this.matterbridge.setLogLevel("info");
1830
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1575
1831
  }
1576
1832
  else if (data.params.value === 'Notice') {
1577
- await this.matterbridge.setLogLevel("notice");
1833
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1578
1834
  }
1579
1835
  else if (data.params.value === 'Warn') {
1580
- await this.matterbridge.setLogLevel("warn");
1836
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1581
1837
  }
1582
1838
  else if (data.params.value === 'Error') {
1583
- await this.matterbridge.setLogLevel("error");
1839
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1584
1840
  }
1585
1841
  else if (data.params.value === 'Fatal') {
1586
- await this.matterbridge.setLogLevel("fatal");
1842
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1587
1843
  }
1588
1844
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1589
1845
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1594,6 +1850,7 @@ export class Frontend extends EventEmitter {
1594
1850
  this.log.debug('Matterbridge file log:', data.params.value);
1595
1851
  this.matterbridge.fileLogger = data.params.value;
1596
1852
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1853
+ // Create the file logger for matterbridge
1597
1854
  if (data.params.value)
1598
1855
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1599
1856
  else
@@ -1622,11 +1879,12 @@ export class Frontend extends EventEmitter {
1622
1879
  else if (data.params.value === 'Fatal') {
1623
1880
  Logger.level = MatterLogLevel.FATAL;
1624
1881
  }
1625
- let callbackLogLevel = "notice";
1626
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1627
- callbackLogLevel = "info";
1628
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1629
- callbackLogLevel = "debug";
1882
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
1883
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
1884
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
1885
+ callbackLogLevel = "info" /* LogLevel.INFO */;
1886
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
1887
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1630
1888
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1631
1889
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1632
1890
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1678,6 +1936,7 @@ export class Frontend extends EventEmitter {
1678
1936
  }
1679
1937
  break;
1680
1938
  case 'setmatterport':
1939
+ // eslint-disable-next-line no-case-declarations
1681
1940
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1682
1941
  if (isValidNumber(port, 5540, 5600)) {
1683
1942
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1697,6 +1956,7 @@ export class Frontend extends EventEmitter {
1697
1956
  }
1698
1957
  break;
1699
1958
  case 'setmatterdiscriminator':
1959
+ // eslint-disable-next-line no-case-declarations
1700
1960
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1701
1961
  if (isValidNumber(discriminator, 0, 4095)) {
1702
1962
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1716,6 +1976,7 @@ export class Frontend extends EventEmitter {
1716
1976
  }
1717
1977
  break;
1718
1978
  case 'setmatterpasscode':
1979
+ // eslint-disable-next-line no-case-declarations
1719
1980
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1720
1981
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1721
1982
  this.matterbridge.passcode = passcode;
@@ -1761,15 +2022,19 @@ export class Frontend extends EventEmitter {
1761
2022
  return;
1762
2023
  }
1763
2024
  const config = plugin.configJson;
2025
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1764
2026
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2027
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1765
2028
  if (select === 'serial')
1766
2029
  this.log.info(`Selected device serial ${data.params.serial}`);
1767
2030
  if (select === 'name')
1768
2031
  this.log.info(`Selected device name ${data.params.name}`);
1769
2032
  if (config && select && (select === 'serial' || select === 'name')) {
2033
+ // Remove postfix from the serial if it exists
1770
2034
  if (config.postfix) {
1771
2035
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1772
2036
  }
2037
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1773
2038
  if (isValidArray(config.whiteList, 1)) {
1774
2039
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1775
2040
  config.whiteList.push(data.params.serial);
@@ -1778,6 +2043,7 @@ export class Frontend extends EventEmitter {
1778
2043
  config.whiteList.push(data.params.name);
1779
2044
  }
1780
2045
  }
2046
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1781
2047
  if (isValidArray(config.blackList, 1)) {
1782
2048
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1783
2049
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1805,7 +2071,9 @@ export class Frontend extends EventEmitter {
1805
2071
  return;
1806
2072
  }
1807
2073
  const config = plugin.configJson;
2074
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1808
2075
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2076
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1809
2077
  if (select === 'serial')
1810
2078
  this.log.info(`Unselected device serial ${data.params.serial}`);
1811
2079
  if (select === 'name')
@@ -1814,6 +2082,7 @@ export class Frontend extends EventEmitter {
1814
2082
  if (config.postfix) {
1815
2083
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1816
2084
  }
2085
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1817
2086
  if (isValidArray(config.whiteList, 1)) {
1818
2087
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1819
2088
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1822,6 +2091,7 @@ export class Frontend extends EventEmitter {
1822
2091
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1823
2092
  }
1824
2093
  }
2094
+ // Add the serial to the blackList
1825
2095
  if (isValidArray(config.blackList)) {
1826
2096
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1827
2097
  config.blackList.push(data.params.serial);
@@ -1844,6 +2114,7 @@ export class Frontend extends EventEmitter {
1844
2114
  }
1845
2115
  }
1846
2116
  else {
2117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1847
2118
  const localData = data;
1848
2119
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1849
2120
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1853,23 +2124,46 @@ export class Frontend extends EventEmitter {
1853
2124
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1854
2125
  }
1855
2126
  }
2127
+ /**
2128
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2129
+ *
2130
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2131
+ * @param {string} time - The time string of the message
2132
+ * @param {string} name - The logger name of the message
2133
+ * @param {string} message - The content of the message.
2134
+ *
2135
+ * @remarks
2136
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2137
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2138
+ * The function sends the message to all connected clients.
2139
+ */
1856
2140
  wssSendLogMessage(level, time, name, message) {
1857
2141
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1858
2142
  return;
1859
2143
  if (!level || !time || !name || !message)
1860
2144
  return;
2145
+ // Remove ANSI escape codes from the message
2146
+ // eslint-disable-next-line no-control-regex
1861
2147
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2148
+ // Remove leading asterisks from the message
1862
2149
  message = message.replace(/^\*+/, '');
2150
+ // Replace all occurrences of \t and \n
1863
2151
  message = message.replace(/[\t\n]/g, '');
2152
+ // Remove non-printable characters
2153
+ // eslint-disable-next-line no-control-regex
1864
2154
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2155
+ // Replace all occurrences of \" with "
1865
2156
  message = message.replace(/\\"/g, '"');
2157
+ // Define the maximum allowed length for continuous characters without a space
1866
2158
  const maxContinuousLength = 100;
1867
2159
  const keepStartLength = 20;
1868
2160
  const keepEndLength = 20;
2161
+ // Split the message into words
1869
2162
  if (level !== 'spawn') {
1870
2163
  message = message
1871
2164
  .split(' ')
1872
2165
  .map((word) => {
2166
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1873
2167
  if (word.length > maxContinuousLength) {
1874
2168
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1875
2169
  }
@@ -1877,14 +2171,34 @@ export class Frontend extends EventEmitter {
1877
2171
  })
1878
2172
  .join(' ');
1879
2173
  }
2174
+ // Send the message to all connected clients
1880
2175
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1881
2176
  }
2177
+ /**
2178
+ * Sends a need to refresh WebSocket message to all connected clients.
2179
+ *
2180
+ * @param {string} changed - The changed value.
2181
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2182
+ * possible values for changed:
2183
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2184
+ * - 'plugins'
2185
+ * - 'devices'
2186
+ * - 'matter' with param 'matter' (QRDiv component)
2187
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2188
+ */
1882
2189
  wssSendRefreshRequired(changed, params) {
1883
2190
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1884
2191
  return;
1885
2192
  this.log.debug('Sending a refresh required message to all connected clients');
2193
+ // Send the message to all connected clients
1886
2194
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1887
2195
  }
2196
+ /**
2197
+ * Sends a need to restart WebSocket message to all connected clients.
2198
+ *
2199
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2200
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2201
+ */
1888
2202
  wssSendRestartRequired(snackbar = true, fixed = false) {
1889
2203
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1890
2204
  return;
@@ -1893,8 +2207,14 @@ export class Frontend extends EventEmitter {
1893
2207
  this.matterbridge.fixedRestartRequired = fixed;
1894
2208
  if (snackbar === true)
1895
2209
  this.wssSendSnackbarMessage(`Restart required`, 0);
2210
+ // Send the message to all connected clients
1896
2211
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1897
2212
  }
2213
+ /**
2214
+ * Sends a no need to restart WebSocket message to all connected clients.
2215
+ *
2216
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2217
+ */
1898
2218
  wssSendRestartNotRequired(snackbar = true) {
1899
2219
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1900
2220
  return;
@@ -1902,57 +2222,133 @@ export class Frontend extends EventEmitter {
1902
2222
  this.matterbridge.restartRequired = false;
1903
2223
  if (snackbar === true)
1904
2224
  this.wssSendCloseSnackbarMessage(`Restart required`);
2225
+ // Send the message to all connected clients
1905
2226
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1906
2227
  }
2228
+ /**
2229
+ * Sends a need to update WebSocket message to all connected clients.
2230
+ *
2231
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2232
+ */
1907
2233
  wssSendUpdateRequired(devVersion = false) {
1908
2234
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1909
2235
  return;
1910
2236
  this.log.debug('Sending an update required message to all connected clients');
1911
2237
  this.matterbridge.updateRequired = true;
2238
+ // Send the message to all connected clients
1912
2239
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1913
2240
  }
2241
+ /**
2242
+ * Sends a cpu update message to all connected clients.
2243
+ *
2244
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2245
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2246
+ */
1914
2247
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1915
2248
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1916
2249
  return;
1917
2250
  if (hasParameter('debug'))
1918
2251
  this.log.debug('Sending a cpu update message to all connected clients');
2252
+ // Send the message to all connected clients
1919
2253
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', success: true, response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 } });
1920
2254
  }
2255
+ /**
2256
+ * Sends a memory update message to all connected clients.
2257
+ *
2258
+ * @param {string} totalMemory - The total memory in bytes.
2259
+ * @param {string} freeMemory - The free memory in bytes.
2260
+ * @param {string} rss - The resident set size in bytes.
2261
+ * @param {string} heapTotal - The total heap memory in bytes.
2262
+ * @param {string} heapUsed - The used heap memory in bytes.
2263
+ * @param {string} external - The external memory in bytes.
2264
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2265
+ */
1921
2266
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1922
2267
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1923
2268
  return;
1924
2269
  if (hasParameter('debug'))
1925
2270
  this.log.debug('Sending a memory update message to all connected clients');
2271
+ // Send the message to all connected clients
1926
2272
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1927
2273
  }
2274
+ /**
2275
+ * Sends an uptime update message to all connected clients.
2276
+ *
2277
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2278
+ * @param {string} processUptime - The process uptime in a human-readable format.
2279
+ */
1928
2280
  wssSendUptimeUpdate(systemUptime, processUptime) {
1929
2281
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1930
2282
  return;
1931
2283
  if (hasParameter('debug'))
1932
2284
  this.log.debug('Sending a uptime update message to all connected clients');
2285
+ // Send the message to all connected clients
1933
2286
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1934
2287
  }
2288
+ /**
2289
+ * Sends an open snackbar message to all connected clients.
2290
+ *
2291
+ * @param {string} message - The message to send.
2292
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2293
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2294
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2295
+ *
2296
+ * @remarks
2297
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2298
+ */
1935
2299
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1936
2300
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1937
2301
  return;
1938
2302
  this.log.debug('Sending a snackbar message to all connected clients');
2303
+ // Send the message to all connected clients
1939
2304
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
1940
2305
  }
2306
+ /**
2307
+ * Sends a close snackbar message to all connected clients.
2308
+ * It will close the snackbar message with the same message and timeout = 0.
2309
+ *
2310
+ * @param {string} message - The message to send.
2311
+ */
1941
2312
  wssSendCloseSnackbarMessage(message) {
1942
2313
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1943
2314
  return;
1944
2315
  this.log.debug('Sending a close snackbar message to all connected clients');
2316
+ // Send the message to all connected clients
1945
2317
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
1946
2318
  }
2319
+ /**
2320
+ * Sends an attribute update message to all connected WebSocket clients.
2321
+ *
2322
+ * @param {string | undefined} plugin - The name of the plugin.
2323
+ * @param {string | undefined} serialNumber - The serial number of the device.
2324
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2325
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2326
+ * @param {string} id - The endpoint id where the attribute belongs.
2327
+ * @param {string} cluster - The cluster name where the attribute belongs.
2328
+ * @param {string} attribute - The name of the attribute that changed.
2329
+ * @param {number | string | boolean} value - The new value of the attribute.
2330
+ *
2331
+ * @remarks
2332
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2333
+ * with the updated attribute information.
2334
+ */
1947
2335
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
1948
2336
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1949
2337
  return;
1950
2338
  this.log.debug('Sending an attribute update message to all connected clients');
2339
+ // Send the message to all connected clients
1951
2340
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
1952
2341
  }
2342
+ /**
2343
+ * Sends a message to all connected clients.
2344
+ * This is an helper function to send a broadcast message to all connected clients.
2345
+ *
2346
+ * @param {WsMessageBroadcast} msg - The message to send.
2347
+ */
1953
2348
  wssBroadcastMessage(msg) {
1954
2349
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1955
2350
  return;
2351
+ // Send the message to all connected clients
1956
2352
  const stringifiedMsg = JSON.stringify(msg);
1957
2353
  if (msg.method !== 'log')
1958
2354
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
@@ -1963,3 +2359,4 @@ export class Frontend extends EventEmitter {
1963
2359
  });
1964
2360
  }
1965
2361
  }
2362
+ //# sourceMappingURL=frontend.js.map