matterbridge 3.3.7-dev-20251109-a306ab9 → 3.3.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 (302) hide show
  1. package/dist/broadcastServer.d.ts +115 -0
  2. package/dist/broadcastServer.d.ts.map +1 -0
  3. package/dist/broadcastServer.js +93 -1
  4. package/dist/broadcastServer.js.map +1 -0
  5. package/dist/broadcastServerTypes.d.ts +806 -0
  6. package/dist/broadcastServerTypes.d.ts.map +1 -0
  7. package/dist/broadcastServerTypes.js +24 -0
  8. package/dist/broadcastServerTypes.js.map +1 -0
  9. package/dist/cli.d.ts +30 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +97 -1
  12. package/dist/cli.js.map +1 -0
  13. package/dist/cliEmitter.d.ts +50 -0
  14. package/dist/cliEmitter.d.ts.map +1 -0
  15. package/dist/cliEmitter.js +37 -0
  16. package/dist/cliEmitter.js.map +1 -0
  17. package/dist/cliHistory.d.ts +48 -0
  18. package/dist/cliHistory.d.ts.map +1 -0
  19. package/dist/cliHistory.js +38 -0
  20. package/dist/cliHistory.js.map +1 -0
  21. package/dist/clusters/export.d.ts +2 -0
  22. package/dist/clusters/export.d.ts.map +1 -0
  23. package/dist/clusters/export.js +2 -0
  24. package/dist/clusters/export.js.map +1 -0
  25. package/dist/defaultConfigSchema.d.ts +28 -0
  26. package/dist/defaultConfigSchema.d.ts.map +1 -0
  27. package/dist/defaultConfigSchema.js +24 -0
  28. package/dist/defaultConfigSchema.js.map +1 -0
  29. package/dist/deviceManager.d.ts +128 -0
  30. package/dist/deviceManager.d.ts.map +1 -0
  31. package/dist/deviceManager.js +105 -1
  32. package/dist/deviceManager.js.map +1 -0
  33. package/dist/devices/airConditioner.d.ts +98 -0
  34. package/dist/devices/airConditioner.d.ts.map +1 -0
  35. package/dist/devices/airConditioner.js +57 -0
  36. package/dist/devices/airConditioner.js.map +1 -0
  37. package/dist/devices/batteryStorage.d.ts +48 -0
  38. package/dist/devices/batteryStorage.d.ts.map +1 -0
  39. package/dist/devices/batteryStorage.js +48 -1
  40. package/dist/devices/batteryStorage.js.map +1 -0
  41. package/dist/devices/cooktop.d.ts +60 -0
  42. package/dist/devices/cooktop.d.ts.map +1 -0
  43. package/dist/devices/cooktop.js +55 -0
  44. package/dist/devices/cooktop.js.map +1 -0
  45. package/dist/devices/dishwasher.d.ts +71 -0
  46. package/dist/devices/dishwasher.d.ts.map +1 -0
  47. package/dist/devices/dishwasher.js +57 -0
  48. package/dist/devices/dishwasher.js.map +1 -0
  49. package/dist/devices/evse.d.ts +76 -0
  50. package/dist/devices/evse.d.ts.map +1 -0
  51. package/dist/devices/evse.js +74 -10
  52. package/dist/devices/evse.js.map +1 -0
  53. package/dist/devices/export.d.ts +17 -0
  54. package/dist/devices/export.d.ts.map +1 -0
  55. package/dist/devices/export.js +5 -0
  56. package/dist/devices/export.js.map +1 -0
  57. package/dist/devices/extractorHood.d.ts +46 -0
  58. package/dist/devices/extractorHood.d.ts.map +1 -0
  59. package/dist/devices/extractorHood.js +42 -0
  60. package/dist/devices/extractorHood.js.map +1 -0
  61. package/dist/devices/heatPump.d.ts +47 -0
  62. package/dist/devices/heatPump.d.ts.map +1 -0
  63. package/dist/devices/heatPump.js +50 -2
  64. package/dist/devices/heatPump.js.map +1 -0
  65. package/dist/devices/laundryDryer.d.ts +67 -0
  66. package/dist/devices/laundryDryer.d.ts.map +1 -0
  67. package/dist/devices/laundryDryer.js +62 -3
  68. package/dist/devices/laundryDryer.js.map +1 -0
  69. package/dist/devices/laundryWasher.d.ts +81 -0
  70. package/dist/devices/laundryWasher.d.ts.map +1 -0
  71. package/dist/devices/laundryWasher.js +70 -4
  72. package/dist/devices/laundryWasher.js.map +1 -0
  73. package/dist/devices/microwaveOven.d.ts +168 -0
  74. package/dist/devices/microwaveOven.d.ts.map +1 -0
  75. package/dist/devices/microwaveOven.js +88 -5
  76. package/dist/devices/microwaveOven.js.map +1 -0
  77. package/dist/devices/oven.d.ts +105 -0
  78. package/dist/devices/oven.d.ts.map +1 -0
  79. package/dist/devices/oven.js +85 -0
  80. package/dist/devices/oven.js.map +1 -0
  81. package/dist/devices/refrigerator.d.ts +118 -0
  82. package/dist/devices/refrigerator.d.ts.map +1 -0
  83. package/dist/devices/refrigerator.js +102 -0
  84. package/dist/devices/refrigerator.js.map +1 -0
  85. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  86. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  87. package/dist/devices/roboticVacuumCleaner.js +100 -9
  88. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  89. package/dist/devices/solarPower.d.ts +40 -0
  90. package/dist/devices/solarPower.d.ts.map +1 -0
  91. package/dist/devices/solarPower.js +38 -0
  92. package/dist/devices/solarPower.js.map +1 -0
  93. package/dist/devices/speaker.d.ts +87 -0
  94. package/dist/devices/speaker.d.ts.map +1 -0
  95. package/dist/devices/speaker.js +84 -0
  96. package/dist/devices/speaker.js.map +1 -0
  97. package/dist/devices/temperatureControl.d.ts +166 -0
  98. package/dist/devices/temperatureControl.d.ts.map +1 -0
  99. package/dist/devices/temperatureControl.js +24 -3
  100. package/dist/devices/temperatureControl.js.map +1 -0
  101. package/dist/devices/waterHeater.d.ts +111 -0
  102. package/dist/devices/waterHeater.d.ts.map +1 -0
  103. package/dist/devices/waterHeater.js +82 -2
  104. package/dist/devices/waterHeater.js.map +1 -0
  105. package/dist/dgram/coap.d.ts +205 -0
  106. package/dist/dgram/coap.d.ts.map +1 -0
  107. package/dist/dgram/coap.js +126 -13
  108. package/dist/dgram/coap.js.map +1 -0
  109. package/dist/dgram/dgram.d.ts +141 -0
  110. package/dist/dgram/dgram.d.ts.map +1 -0
  111. package/dist/dgram/dgram.js +114 -2
  112. package/dist/dgram/dgram.js.map +1 -0
  113. package/dist/dgram/mb_coap.d.ts +24 -0
  114. package/dist/dgram/mb_coap.d.ts.map +1 -0
  115. package/dist/dgram/mb_coap.js +41 -3
  116. package/dist/dgram/mb_coap.js.map +1 -0
  117. package/dist/dgram/mb_mdns.d.ts +24 -0
  118. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  119. package/dist/dgram/mb_mdns.js +80 -15
  120. package/dist/dgram/mb_mdns.js.map +1 -0
  121. package/dist/dgram/mdns.d.ts +290 -0
  122. package/dist/dgram/mdns.d.ts.map +1 -0
  123. package/dist/dgram/mdns.js +299 -137
  124. package/dist/dgram/mdns.js.map +1 -0
  125. package/dist/dgram/multicast.d.ts +67 -0
  126. package/dist/dgram/multicast.d.ts.map +1 -0
  127. package/dist/dgram/multicast.js +62 -1
  128. package/dist/dgram/multicast.js.map +1 -0
  129. package/dist/dgram/unicast.d.ts +56 -0
  130. package/dist/dgram/unicast.d.ts.map +1 -0
  131. package/dist/dgram/unicast.js +54 -0
  132. package/dist/dgram/unicast.js.map +1 -0
  133. package/dist/frontend.d.ts +238 -0
  134. package/dist/frontend.d.ts.map +1 -0
  135. package/dist/frontend.js +451 -35
  136. package/dist/frontend.js.map +1 -0
  137. package/dist/frontendTypes.d.ts +529 -0
  138. package/dist/frontendTypes.d.ts.map +1 -0
  139. package/dist/frontendTypes.js +45 -0
  140. package/dist/frontendTypes.js.map +1 -0
  141. package/dist/helpers.d.ts +48 -0
  142. package/dist/helpers.d.ts.map +1 -0
  143. package/dist/helpers.js +53 -0
  144. package/dist/helpers.js.map +1 -0
  145. package/dist/index.d.ts +33 -0
  146. package/dist/index.d.ts.map +1 -0
  147. package/dist/index.js +25 -0
  148. package/dist/index.js.map +1 -0
  149. package/dist/logger/export.d.ts +2 -0
  150. package/dist/logger/export.d.ts.map +1 -0
  151. package/dist/logger/export.js +1 -0
  152. package/dist/logger/export.js.map +1 -0
  153. package/dist/matter/behaviors.d.ts +2 -0
  154. package/dist/matter/behaviors.d.ts.map +1 -0
  155. package/dist/matter/behaviors.js +2 -0
  156. package/dist/matter/behaviors.js.map +1 -0
  157. package/dist/matter/clusters.d.ts +2 -0
  158. package/dist/matter/clusters.d.ts.map +1 -0
  159. package/dist/matter/clusters.js +2 -0
  160. package/dist/matter/clusters.js.map +1 -0
  161. package/dist/matter/devices.d.ts +2 -0
  162. package/dist/matter/devices.d.ts.map +1 -0
  163. package/dist/matter/devices.js +2 -0
  164. package/dist/matter/devices.js.map +1 -0
  165. package/dist/matter/endpoints.d.ts +2 -0
  166. package/dist/matter/endpoints.d.ts.map +1 -0
  167. package/dist/matter/endpoints.js +2 -0
  168. package/dist/matter/endpoints.js.map +1 -0
  169. package/dist/matter/export.d.ts +5 -0
  170. package/dist/matter/export.d.ts.map +1 -0
  171. package/dist/matter/export.js +3 -0
  172. package/dist/matter/export.js.map +1 -0
  173. package/dist/matter/types.d.ts +3 -0
  174. package/dist/matter/types.d.ts.map +1 -0
  175. package/dist/matter/types.js +3 -0
  176. package/dist/matter/types.js.map +1 -0
  177. package/dist/matterbridge.d.ts +478 -0
  178. package/dist/matterbridge.d.ts.map +1 -0
  179. package/dist/matterbridge.js +828 -46
  180. package/dist/matterbridge.js.map +1 -0
  181. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  182. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  183. package/dist/matterbridgeAccessoryPlatform.js +37 -0
  184. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  185. package/dist/matterbridgeBehaviors.d.ts +2404 -0
  186. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  187. package/dist/matterbridgeBehaviors.js +68 -5
  188. package/dist/matterbridgeBehaviors.js.map +1 -0
  189. package/dist/matterbridgeDeviceTypes.d.ts +770 -0
  190. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  191. package/dist/matterbridgeDeviceTypes.js +638 -17
  192. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  193. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  194. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  195. package/dist/matterbridgeDynamicPlatform.js +37 -0
  196. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  197. package/dist/matterbridgeEndpoint.d.ts +1556 -0
  198. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  199. package/dist/matterbridgeEndpoint.js +1408 -52
  200. package/dist/matterbridgeEndpoint.js.map +1 -0
  201. package/dist/matterbridgeEndpointHelpers.d.ts +758 -0
  202. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  203. package/dist/matterbridgeEndpointHelpers.js +464 -19
  204. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  205. package/dist/matterbridgePlatform.d.ts +402 -0
  206. package/dist/matterbridgePlatform.d.ts.map +1 -0
  207. package/dist/matterbridgePlatform.js +341 -1
  208. package/dist/matterbridgePlatform.js.map +1 -0
  209. package/dist/matterbridgeTypes.d.ts +239 -0
  210. package/dist/matterbridgeTypes.d.ts.map +1 -0
  211. package/dist/matterbridgeTypes.js +26 -0
  212. package/dist/matterbridgeTypes.js.map +1 -0
  213. package/dist/pluginManager.d.ts +371 -0
  214. package/dist/pluginManager.d.ts.map +1 -0
  215. package/dist/pluginManager.js +339 -4
  216. package/dist/pluginManager.js.map +1 -0
  217. package/dist/shelly.d.ts +174 -0
  218. package/dist/shelly.d.ts.map +1 -0
  219. package/dist/shelly.js +168 -7
  220. package/dist/shelly.js.map +1 -0
  221. package/dist/storage/export.d.ts +2 -0
  222. package/dist/storage/export.d.ts.map +1 -0
  223. package/dist/storage/export.js +1 -0
  224. package/dist/storage/export.js.map +1 -0
  225. package/dist/update.d.ts +75 -0
  226. package/dist/update.d.ts.map +1 -0
  227. package/dist/update.js +69 -0
  228. package/dist/update.js.map +1 -0
  229. package/dist/utils/colorUtils.d.ts +101 -0
  230. package/dist/utils/colorUtils.d.ts.map +1 -0
  231. package/dist/utils/colorUtils.js +97 -2
  232. package/dist/utils/colorUtils.js.map +1 -0
  233. package/dist/utils/commandLine.d.ts +66 -0
  234. package/dist/utils/commandLine.d.ts.map +1 -0
  235. package/dist/utils/commandLine.js +60 -0
  236. package/dist/utils/commandLine.js.map +1 -0
  237. package/dist/utils/copyDirectory.d.ts +33 -0
  238. package/dist/utils/copyDirectory.d.ts.map +1 -0
  239. package/dist/utils/copyDirectory.js +38 -1
  240. package/dist/utils/copyDirectory.js.map +1 -0
  241. package/dist/utils/createDirectory.d.ts +34 -0
  242. package/dist/utils/createDirectory.d.ts.map +1 -0
  243. package/dist/utils/createDirectory.js +33 -0
  244. package/dist/utils/createDirectory.js.map +1 -0
  245. package/dist/utils/createZip.d.ts +39 -0
  246. package/dist/utils/createZip.d.ts.map +1 -0
  247. package/dist/utils/createZip.js +47 -2
  248. package/dist/utils/createZip.js.map +1 -0
  249. package/dist/utils/deepCopy.d.ts +32 -0
  250. package/dist/utils/deepCopy.d.ts.map +1 -0
  251. package/dist/utils/deepCopy.js +39 -0
  252. package/dist/utils/deepCopy.js.map +1 -0
  253. package/dist/utils/deepEqual.d.ts +54 -0
  254. package/dist/utils/deepEqual.d.ts.map +1 -0
  255. package/dist/utils/deepEqual.js +72 -1
  256. package/dist/utils/deepEqual.js.map +1 -0
  257. package/dist/utils/error.d.ts +44 -0
  258. package/dist/utils/error.d.ts.map +1 -0
  259. package/dist/utils/error.js +41 -0
  260. package/dist/utils/error.js.map +1 -0
  261. package/dist/utils/export.d.ts +13 -0
  262. package/dist/utils/export.d.ts.map +1 -0
  263. package/dist/utils/export.js +1 -0
  264. package/dist/utils/export.js.map +1 -0
  265. package/dist/utils/format.d.ts +53 -0
  266. package/dist/utils/format.d.ts.map +1 -0
  267. package/dist/utils/format.js +49 -0
  268. package/dist/utils/format.js.map +1 -0
  269. package/dist/utils/hex.d.ts +89 -0
  270. package/dist/utils/hex.d.ts.map +1 -0
  271. package/dist/utils/hex.js +124 -0
  272. package/dist/utils/hex.js.map +1 -0
  273. package/dist/utils/inspector.d.ts +87 -0
  274. package/dist/utils/inspector.d.ts.map +1 -0
  275. package/dist/utils/inspector.js +69 -1
  276. package/dist/utils/inspector.js.map +1 -0
  277. package/dist/utils/isvalid.d.ts +103 -0
  278. package/dist/utils/isvalid.d.ts.map +1 -0
  279. package/dist/utils/isvalid.js +101 -0
  280. package/dist/utils/isvalid.js.map +1 -0
  281. package/dist/utils/jestHelpers.d.ts +139 -0
  282. package/dist/utils/jestHelpers.d.ts.map +1 -0
  283. package/dist/utils/jestHelpers.js +153 -3
  284. package/dist/utils/jestHelpers.js.map +1 -0
  285. package/dist/utils/network.d.ts +101 -0
  286. package/dist/utils/network.d.ts.map +1 -0
  287. package/dist/utils/network.js +96 -5
  288. package/dist/utils/network.js.map +1 -0
  289. package/dist/utils/spawn.d.ts +35 -0
  290. package/dist/utils/spawn.d.ts.map +1 -0
  291. package/dist/utils/spawn.js +71 -0
  292. package/dist/utils/spawn.js.map +1 -0
  293. package/dist/utils/tracker.d.ts +108 -0
  294. package/dist/utils/tracker.d.ts.map +1 -0
  295. package/dist/utils/tracker.js +64 -1
  296. package/dist/utils/tracker.js.map +1 -0
  297. package/dist/utils/wait.d.ts +54 -0
  298. package/dist/utils/wait.d.ts.map +1 -0
  299. package/dist/utils/wait.js +60 -8
  300. package/dist/utils/wait.js.map +1 -0
  301. package/npm-shrinkwrap.json +2 -2
  302. 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';
@@ -37,7 +63,7 @@ export class Frontend extends EventEmitter {
37
63
  constructor(matterbridge) {
38
64
  super();
39
65
  this.matterbridge = matterbridge;
40
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
66
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
41
67
  this.log.logNameColor = '\x1b[38;5;97m';
42
68
  this.server = new BroadcastServer('frontend', this.log);
43
69
  this.server.on('broadcast_message', this.msgHandler.bind(this));
@@ -136,23 +162,53 @@ export class Frontend extends EventEmitter {
136
162
  this.port = port;
137
163
  this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
138
164
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
165
+ // Initialize multer with the upload directory
139
166
  const multer = await import('multer');
140
- const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
167
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
141
168
  const upload = multer.default({ dest: uploadDir });
169
+ // Create the express app that serves the frontend
142
170
  const express = await import('express');
143
171
  this.expressApp = express.default();
172
+ // Inject logging/debug wrapper for route/middleware registration
173
+ /*
174
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
175
+ for (const method of methods) {
176
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
180
+ try {
181
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
182
+ return original(path, ...rest);
183
+ } catch (err) {
184
+ console.error(`[ERROR] Failed to register route: ${path}`);
185
+ throw err;
186
+ }
187
+ };
188
+ }
189
+ */
190
+ // Log all requests to the server for debugging
191
+ /*
192
+ this.expressApp.use((req, res, next) => {
193
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
194
+ next();
195
+ });
196
+ */
197
+ // Serve static files from 'frontend/build' directory
144
198
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend', 'build')));
199
+ // Create a WebSocket server and attach it to the http or https server
145
200
  this.log.debug(`Creating WebSocketServer...`);
146
201
  const ws = await import('ws');
147
202
  this.webSocketServer = new ws.WebSocketServer({ noServer: true });
148
203
  this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
149
204
  this.webSocketServer.on('connection', (ws, request) => {
150
205
  const clientIp = request.socket.remoteAddress;
151
- let callbackLogLevel = "notice";
152
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
153
- callbackLogLevel = "info";
154
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
155
- callbackLogLevel = "debug";
206
+ // Set the global logger callback for the WebSocketServer
207
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
208
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
209
+ callbackLogLevel = "info" /* LogLevel.INFO */;
210
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
211
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
156
212
  AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
157
213
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
158
214
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -174,16 +230,25 @@ export class Frontend extends EventEmitter {
174
230
  }
175
231
  });
176
232
  ws.on('error', (error) => {
233
+ // istanbul ignore next
177
234
  this.log.error(`WebSocket client error: ${error}`);
178
235
  });
179
236
  });
180
237
  this.webSocketServer.on('close', () => {
181
238
  this.log.debug(`WebSocketServer closed`);
182
239
  });
240
+ /* With { noServer: true } it never fires
241
+ this.webSocketServer.on('listening', () => {
242
+ this.log.info(`The WebSocketServer is listening`);
243
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
244
+ });
245
+ */
246
+ // istanbul ignore next
183
247
  this.webSocketServer.on('error', (ws, error) => {
184
248
  this.log.error(`WebSocketServer error: ${error}`);
185
249
  });
186
250
  if (!hasParameter('ssl')) {
251
+ // Create an HTTP server and attach the express app
187
252
  const http = await import('node:http');
188
253
  try {
189
254
  this.log.debug(`Creating HTTP server...`);
@@ -194,6 +259,7 @@ export class Frontend extends EventEmitter {
194
259
  this.emit('server_error', error);
195
260
  return;
196
261
  }
262
+ // Listen on the specified port
197
263
  if (hasParameter('ingress')) {
198
264
  this.httpServer.listen(this.port, '0.0.0.0', () => {
199
265
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -213,23 +279,29 @@ export class Frontend extends EventEmitter {
213
279
  }
214
280
  this.httpServer.on('upgrade', async (req, socket, head) => {
215
281
  try {
282
+ // Only proceed for real WebSocket upgrades
283
+ // istanbul ignore next cause is only a safety check
216
284
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
217
285
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
218
286
  return socket.destroy();
219
287
  }
288
+ // Build a URL so we can read ?password=...
220
289
  const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
290
+ // Validate WebSocket password
221
291
  const password = url.searchParams.get('password') ?? '';
222
292
  if (password !== this.storedPassword) {
223
293
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
224
294
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
225
295
  return socket.destroy();
226
296
  }
297
+ // Complete the WebSocket handshake
227
298
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
228
299
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
229
300
  this.webSocketServer?.emit('connection', ws, req);
230
301
  });
231
302
  }
232
303
  catch (err) {
304
+ /* istanbul ignore next: only triggered on unexpected internal error */
233
305
  {
234
306
  inspectError(this.log, 'WebSocket upgrade error:', err);
235
307
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -252,6 +324,7 @@ export class Frontend extends EventEmitter {
252
324
  });
253
325
  }
254
326
  else {
327
+ // SSL is enabled, load the certificate and the private key
255
328
  let cert;
256
329
  let key;
257
330
  let ca;
@@ -261,6 +334,7 @@ export class Frontend extends EventEmitter {
261
334
  let httpsServerOptions = {};
262
335
  const fs = await import('node:fs');
263
336
  if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
337
+ // Load the p12 certificate and the passphrase
264
338
  try {
265
339
  pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
266
340
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
@@ -272,7 +346,7 @@ export class Frontend extends EventEmitter {
272
346
  }
273
347
  try {
274
348
  passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
275
- passphrase = passphrase.trim();
349
+ passphrase = passphrase.trim(); // Ensure no extra characters
276
350
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
277
351
  }
278
352
  catch (error) {
@@ -283,6 +357,7 @@ export class Frontend extends EventEmitter {
283
357
  httpsServerOptions = { pfx, passphrase };
284
358
  }
285
359
  else {
360
+ // 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.
286
361
  try {
287
362
  cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
288
363
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
@@ -312,9 +387,10 @@ export class Frontend extends EventEmitter {
312
387
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
313
388
  }
314
389
  if (hasParameter('mtls')) {
315
- httpsServerOptions.requestCert = true;
316
- httpsServerOptions.rejectUnauthorized = true;
390
+ httpsServerOptions.requestCert = true; // Request client certificate
391
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
317
392
  }
393
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
318
394
  const https = await import('node:https');
319
395
  try {
320
396
  this.log.debug(`Creating HTTPS server...`);
@@ -325,6 +401,7 @@ export class Frontend extends EventEmitter {
325
401
  this.emit('server_error', error);
326
402
  return;
327
403
  }
404
+ // Listen on the specified port
328
405
  if (hasParameter('ingress')) {
329
406
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
330
407
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -344,23 +421,29 @@ export class Frontend extends EventEmitter {
344
421
  }
345
422
  this.httpsServer.on('upgrade', async (req, socket, head) => {
346
423
  try {
424
+ // Only proceed for real WebSocket upgrades
425
+ // istanbul ignore next cause is only a safety check
347
426
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
348
427
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
349
428
  return socket.destroy();
350
429
  }
430
+ // Build a URL so we can read ?password=...
351
431
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
432
+ // Validate WebSocket password
352
433
  const password = url.searchParams.get('password') ?? '';
353
434
  if (password !== this.storedPassword) {
354
435
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
355
436
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
356
437
  return socket.destroy();
357
438
  }
439
+ // Complete the WebSocket handshake
358
440
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
359
441
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
360
442
  this.webSocketServer?.emit('connection', ws, req);
361
443
  });
362
444
  }
363
445
  catch (err) {
446
+ /* istanbul ignore next: only triggered on unexpected internal error */
364
447
  {
365
448
  inspectError(this.log, 'WebSocket upgrade error:', err);
366
449
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -382,6 +465,7 @@ export class Frontend extends EventEmitter {
382
465
  return;
383
466
  });
384
467
  }
468
+ // Subscribe to cli events
385
469
  cliEmitter.removeAllListeners();
386
470
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
387
471
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -392,6 +476,8 @@ export class Frontend extends EventEmitter {
392
476
  cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
393
477
  this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
394
478
  });
479
+ // Endpoint to validate login code
480
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
395
481
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
396
482
  const { password } = req.body;
397
483
  this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
@@ -404,17 +490,20 @@ export class Frontend extends EventEmitter {
404
490
  res.json({ valid: false });
405
491
  }
406
492
  });
493
+ // Endpoint to provide health check for docker
407
494
  this.expressApp.get('/health', (req, res) => {
408
495
  this.log.debug('Express received /health');
409
496
  const healthStatus = {
410
- status: 'ok',
411
- uptime: process.uptime(),
412
- timestamp: new Date().toISOString(),
497
+ status: 'ok', // Indicate service is healthy
498
+ uptime: process.uptime(), // Server uptime in seconds
499
+ timestamp: new Date().toISOString(), // Current timestamp
413
500
  };
414
501
  res.status(200).json(healthStatus);
415
502
  });
503
+ // Endpoint to provide memory usage details
416
504
  this.expressApp.get('/memory', async (req, res) => {
417
505
  this.log.debug('Express received /memory');
506
+ // Memory usage from process
418
507
  const memoryUsageRaw = process.memoryUsage();
419
508
  const memoryUsage = {
420
509
  rss: formatBytes(memoryUsageRaw.rss),
@@ -423,10 +512,13 @@ export class Frontend extends EventEmitter {
423
512
  external: formatBytes(memoryUsageRaw.external),
424
513
  arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
425
514
  };
515
+ // V8 heap statistics
426
516
  const { default: v8 } = await import('node:v8');
427
517
  const heapStatsRaw = v8.getHeapStatistics();
428
518
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
519
+ // Format heapStats
429
520
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
521
+ // Format heapSpaces
430
522
  const heapSpaces = heapSpacesRaw.map((space) => ({
431
523
  ...space,
432
524
  space_size: formatBytes(space.space_size),
@@ -445,18 +537,22 @@ export class Frontend extends EventEmitter {
445
537
  };
446
538
  res.status(200).json(memoryReport);
447
539
  });
540
+ // Endpoint to provide settings
448
541
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
449
542
  this.log.debug('The frontend sent /api/settings');
450
543
  res.json(await this.getApiSettings());
451
544
  });
545
+ // Endpoint to provide plugins
452
546
  this.expressApp.get('/api/plugins', async (req, res) => {
453
547
  this.log.debug('The frontend sent /api/plugins');
454
548
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
455
549
  });
550
+ // Endpoint to provide devices
456
551
  this.expressApp.get('/api/devices', async (req, res) => {
457
552
  this.log.debug('The frontend sent /api/devices');
458
553
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
459
554
  });
555
+ // Endpoint to view the matterbridge log
460
556
  this.expressApp.get('/api/view-mblog', async (req, res) => {
461
557
  this.log.debug('The frontend sent /api/view-mblog');
462
558
  try {
@@ -470,6 +566,7 @@ export class Frontend extends EventEmitter {
470
566
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
471
567
  }
472
568
  });
569
+ // Endpoint to view the matter.js log
473
570
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
474
571
  this.log.debug('The frontend sent /api/view-mjlog');
475
572
  try {
@@ -483,6 +580,7 @@ export class Frontend extends EventEmitter {
483
580
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
484
581
  }
485
582
  });
583
+ // Endpoint to view the diagnostic.log
486
584
  this.expressApp.get('/api/view-diagnostic', async (req, res) => {
487
585
  this.log.debug('The frontend sent /api/view-diagnostic');
488
586
  await this.generateDiagnostic();
@@ -493,10 +591,13 @@ export class Frontend extends EventEmitter {
493
591
  res.send(data.slice(29));
494
592
  }
495
593
  catch (error) {
594
+ // istanbul ignore next
496
595
  this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
596
+ // istanbul ignore next
497
597
  res.status(500).send('Error reading diagnostic log file.');
498
598
  }
499
599
  });
600
+ // Endpoint to download the diagnostic.log
500
601
  this.expressApp.get('/api/download-diagnostic', async (req, res) => {
501
602
  this.log.debug(`The frontend sent /api/download-diagnostic`);
502
603
  await this.generateDiagnostic();
@@ -507,16 +608,19 @@ export class Frontend extends EventEmitter {
507
608
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
508
609
  }
509
610
  catch (error) {
611
+ // istanbul ignore next
510
612
  this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
511
613
  }
512
614
  res.type('text/plain');
513
615
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
616
+ /* istanbul ignore if */
514
617
  if (error) {
515
618
  this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
516
619
  res.status(500).send('Error downloading the diagnostic log file');
517
620
  }
518
621
  });
519
622
  });
623
+ // Endpoint to view the history.html
520
624
  this.expressApp.get('/api/viewhistory', async (req, res) => {
521
625
  this.log.debug('The frontend sent /api/viewhistory');
522
626
  try {
@@ -530,6 +634,7 @@ export class Frontend extends EventEmitter {
530
634
  res.status(500).send('Error reading history file.');
531
635
  }
532
636
  });
637
+ // Endpoint to download the history.html
533
638
  this.expressApp.get('/api/downloadhistory', async (req, res) => {
534
639
  this.log.debug(`The frontend sent /api/downloadhistory`);
535
640
  try {
@@ -539,6 +644,7 @@ export class Frontend extends EventEmitter {
539
644
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
540
645
  res.type('text/plain');
541
646
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
647
+ /* istanbul ignore if */
542
648
  if (error) {
543
649
  this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
544
650
  res.status(500).send('Error downloading history file');
@@ -550,6 +656,7 @@ export class Frontend extends EventEmitter {
550
656
  res.status(500).send('Error reading history file.');
551
657
  }
552
658
  });
659
+ // Endpoint to view the shelly log
553
660
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
554
661
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
555
662
  try {
@@ -563,6 +670,7 @@ export class Frontend extends EventEmitter {
563
670
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
564
671
  }
565
672
  });
673
+ // Endpoint to download the matterbridge log
566
674
  this.expressApp.get('/api/download-mblog', async (req, res) => {
567
675
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
568
676
  const fs = await import('node:fs');
@@ -577,12 +685,14 @@ export class Frontend extends EventEmitter {
577
685
  }
578
686
  res.type('text/plain');
579
687
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
688
+ /* istanbul ignore if */
580
689
  if (error) {
581
690
  this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
582
691
  res.status(500).send('Error downloading the matterbridge log file');
583
692
  }
584
693
  });
585
694
  });
695
+ // Endpoint to download the matter log
586
696
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
587
697
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
588
698
  const fs = await import('node:fs');
@@ -597,12 +707,14 @@ export class Frontend extends EventEmitter {
597
707
  }
598
708
  res.type('text/plain');
599
709
  res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
710
+ /* istanbul ignore if */
600
711
  if (error) {
601
712
  this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
602
713
  res.status(500).send('Error downloading the matter log file');
603
714
  }
604
715
  });
605
716
  });
717
+ // Endpoint to download the shelly log
606
718
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
607
719
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
608
720
  const fs = await import('node:fs');
@@ -617,75 +729,91 @@ export class Frontend extends EventEmitter {
617
729
  }
618
730
  res.type('text/plain');
619
731
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
732
+ /* istanbul ignore if */
620
733
  if (error) {
621
734
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
622
735
  res.status(500).send('Error downloading Shelly system log file');
623
736
  }
624
737
  });
625
738
  });
739
+ // Endpoint to download the matterbridge storage directory
626
740
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
627
741
  this.log.debug('The frontend sent /api/download-mbstorage');
628
742
  await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
629
743
  res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
744
+ /* istanbul ignore if */
630
745
  if (error) {
631
746
  this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
632
747
  res.status(500).send('Error downloading the matterbridge storage file');
633
748
  }
634
749
  });
635
750
  });
751
+ // Endpoint to download the matter storage file
636
752
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
637
753
  this.log.debug('The frontend sent /api/download-mjstorage');
638
754
  await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
639
755
  res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
756
+ /* istanbul ignore if */
640
757
  if (error) {
641
758
  this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
642
759
  res.status(500).send('Error downloading the matter storage zip file');
643
760
  }
644
761
  });
645
762
  });
763
+ // Endpoint to download the matterbridge plugin directory
646
764
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
647
765
  this.log.debug('The frontend sent /api/download-pluginstorage');
648
766
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
649
767
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
768
+ /* istanbul ignore if */
650
769
  if (error) {
651
770
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
652
771
  res.status(500).send('Error downloading the matterbridge plugin storage file');
653
772
  }
654
773
  });
655
774
  });
775
+ // Endpoint to download the matterbridge plugin config files
656
776
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
657
777
  this.log.debug('The frontend sent /api/download-pluginconfig');
658
778
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
659
779
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
780
+ /* istanbul ignore if */
660
781
  if (error) {
661
782
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
662
783
  res.status(500).send('Error downloading the matterbridge plugin config file');
663
784
  }
664
785
  });
665
786
  });
787
+ // Endpoint to download the matterbridge backup (created with the backup command)
666
788
  this.expressApp.get('/api/download-backup', async (req, res) => {
667
789
  this.log.debug('The frontend sent /api/download-backup');
668
790
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
791
+ /* istanbul ignore if */
669
792
  if (error) {
670
793
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
671
794
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
672
795
  }
673
796
  });
674
797
  });
798
+ // Endpoint to upload a package
675
799
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
676
800
  const { filename } = req.body;
677
801
  const file = req.file;
802
+ /* istanbul ignore if */
678
803
  if (!file || !filename) {
679
804
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
680
805
  res.status(400).send('Invalid request: file and filename are required');
681
806
  return;
682
807
  }
683
808
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
809
+ // Define the path where the plugin file will be saved
684
810
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
685
811
  try {
812
+ // Move the uploaded file to the specified path
686
813
  const fs = await import('node:fs');
687
814
  await fs.promises.rename(file.path, filePath);
688
815
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
816
+ // Install the plugin package
689
817
  if (filename.endsWith('.tgz')) {
690
818
  const { spawnCommand } = await import('./utils/spawn.js');
691
819
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename);
@@ -705,6 +833,7 @@ export class Frontend extends EventEmitter {
705
833
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
706
834
  }
707
835
  });
836
+ // Fallback for routing (must be the last route)
708
837
  this.expressApp.use((req, res) => {
709
838
  const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
710
839
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
@@ -715,13 +844,16 @@ export class Frontend extends EventEmitter {
715
844
  async stop() {
716
845
  this.log.debug('Stopping the frontend...');
717
846
  const ws = await import('ws');
847
+ // Remove listeners from the express app
718
848
  if (this.expressApp) {
719
849
  this.expressApp.removeAllListeners();
720
850
  this.expressApp = undefined;
721
851
  this.log.debug('Frontend app closed successfully');
722
852
  }
853
+ // Close the WebSocket server
723
854
  if (this.webSocketServer) {
724
855
  this.log.debug('Closing WebSocket server...');
856
+ // Close all active connections
725
857
  this.webSocketServer.clients.forEach((client) => {
726
858
  if (client.readyState === ws.WebSocket.OPEN) {
727
859
  client.close();
@@ -730,6 +862,7 @@ export class Frontend extends EventEmitter {
730
862
  await withTimeout(new Promise((resolve) => {
731
863
  this.webSocketServer?.close((error) => {
732
864
  if (error) {
865
+ // istanbul ignore next
733
866
  this.log.error(`Error closing WebSocket server: ${error}`);
734
867
  }
735
868
  else {
@@ -742,8 +875,27 @@ export class Frontend extends EventEmitter {
742
875
  this.webSocketServer.removeAllListeners();
743
876
  this.webSocketServer = undefined;
744
877
  }
878
+ // Close the http server
745
879
  if (this.httpServer) {
746
880
  this.log.debug('Closing http server...');
881
+ /*
882
+ await withTimeout(
883
+ new Promise<void>((resolve) => {
884
+ this.httpServer?.close((error) => {
885
+ if (error) {
886
+ // istanbul ignore next
887
+ this.log.error(`Error closing http server: ${error}`);
888
+ } else {
889
+ this.log.debug('Http server closed successfully');
890
+ this.emit('server_stopped');
891
+ }
892
+ resolve();
893
+ });
894
+ }),
895
+ 5000,
896
+ false,
897
+ );
898
+ */
747
899
  this.httpServer.close();
748
900
  this.log.debug('Http server closed successfully');
749
901
  this.listening = false;
@@ -752,8 +904,27 @@ export class Frontend extends EventEmitter {
752
904
  this.httpServer = undefined;
753
905
  this.log.debug('Frontend http server closed successfully');
754
906
  }
907
+ // Close the https server
755
908
  if (this.httpsServer) {
756
909
  this.log.debug('Closing https server...');
910
+ /*
911
+ await withTimeout(
912
+ new Promise<void>((resolve) => {
913
+ this.httpsServer?.close((error) => {
914
+ if (error) {
915
+ // istanbul ignore next
916
+ this.log.error(`Error closing https server: ${error}`);
917
+ } else {
918
+ this.log.debug('Https server closed successfully');
919
+ this.emit('server_stopped');
920
+ }
921
+ resolve();
922
+ });
923
+ }),
924
+ 5000,
925
+ false,
926
+ );
927
+ */
757
928
  this.httpsServer.close();
758
929
  this.log.debug('Https server closed successfully');
759
930
  this.listening = false;
@@ -764,7 +935,13 @@ export class Frontend extends EventEmitter {
764
935
  }
765
936
  this.log.debug('Frontend stopped successfully');
766
937
  }
938
+ /**
939
+ * Retrieves the api settings data.
940
+ *
941
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
942
+ */
767
943
  async getApiSettings() {
944
+ // Update the variable system information properties
768
945
  this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
769
946
  this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
770
947
  this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
@@ -774,6 +951,7 @@ export class Frontend extends EventEmitter {
774
951
  this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
775
952
  this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
776
953
  this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
954
+ // Create the matterbridge information
777
955
  const info = {
778
956
  homeDirectory: this.matterbridge.homeDirectory,
779
957
  rootDirectory: this.matterbridge.rootDirectory,
@@ -809,9 +987,15 @@ export class Frontend extends EventEmitter {
809
987
  };
810
988
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
811
989
  }
990
+ /**
991
+ * Retrieves the reachable attribute.
992
+ *
993
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
994
+ * @returns {boolean} The reachable attribute.
995
+ */
812
996
  getReachability(device) {
813
997
  if (this.matterbridge.hasCleanupStarted)
814
- return false;
998
+ return false; // Skip if cleanup has started
815
999
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
816
1000
  return false;
817
1001
  if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
@@ -822,9 +1006,15 @@ export class Frontend extends EventEmitter {
822
1006
  return true;
823
1007
  return false;
824
1008
  }
1009
+ /**
1010
+ * Retrieves the power source attribute.
1011
+ *
1012
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1013
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1014
+ */
825
1015
  getPowerSource(endpoint) {
826
1016
  if (this.matterbridge.hasCleanupStarted)
827
- return;
1017
+ return; // Skip if cleanup has started
828
1018
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
829
1019
  return undefined;
830
1020
  const powerSource = (device) => {
@@ -839,16 +1029,25 @@ export class Frontend extends EventEmitter {
839
1029
  }
840
1030
  return;
841
1031
  };
1032
+ // Root endpoint
842
1033
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
843
1034
  return powerSource(endpoint);
1035
+ // Child endpoints
844
1036
  for (const child of endpoint.getChildEndpoints()) {
845
1037
  if (child.hasClusterServer(PowerSource.Cluster.id))
846
1038
  return powerSource(child);
847
1039
  }
848
1040
  }
1041
+ /**
1042
+ * Retrieves the cluster text description from a given device.
1043
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1044
+ *
1045
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1046
+ * @returns {string} The attributes description of the cluster servers in the device.
1047
+ */
849
1048
  getClusterTextFromDevice(device) {
850
1049
  if (this.matterbridge.hasCleanupStarted)
851
- return '';
1050
+ return ''; // Skip if cleanup has started
852
1051
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
853
1052
  return '';
854
1053
  const getUserLabel = (device) => {
@@ -858,6 +1057,7 @@ export class Frontend extends EventEmitter {
858
1057
  if (composed)
859
1058
  return 'Composed: ' + composed.value;
860
1059
  }
1060
+ // istanbul ignore next cause is not reachable
861
1061
  return '';
862
1062
  };
863
1063
  const getFixedLabel = (device) => {
@@ -867,11 +1067,13 @@ export class Frontend extends EventEmitter {
867
1067
  if (composed)
868
1068
  return 'Composed: ' + composed.value;
869
1069
  }
1070
+ // istanbul ignore next cause is not reacheable
870
1071
  return '';
871
1072
  };
872
1073
  let attributes = '';
873
1074
  let supportedModes = [];
874
1075
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1076
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
875
1077
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
876
1078
  return;
877
1079
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -961,11 +1163,17 @@ export class Frontend extends EventEmitter {
961
1163
  if (clusterName === 'userLabel' && attributeName === 'labelList')
962
1164
  attributes += `${getUserLabel(device)} `;
963
1165
  });
1166
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
964
1167
  return attributes.trimStart().trimEnd();
965
1168
  }
1169
+ /**
1170
+ * Retrieves the registered plugins sanitized for res.json().
1171
+ *
1172
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1173
+ */
966
1174
  getPlugins() {
967
1175
  if (this.matterbridge.hasCleanupStarted)
968
- return [];
1176
+ return []; // Skip if cleanup has started
969
1177
  const plugins = [];
970
1178
  for (const plugin of this.matterbridge.plugins.array()) {
971
1179
  plugins.push({
@@ -993,18 +1201,27 @@ export class Frontend extends EventEmitter {
993
1201
  schemaJson: plugin.schemaJson,
994
1202
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
995
1203
  hasBlackList: plugin.configJson?.blackList !== undefined,
1204
+ // Childbridge mode specific data
996
1205
  matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
997
1206
  });
998
1207
  }
999
1208
  return plugins;
1000
1209
  }
1210
+ /**
1211
+ * Retrieves the devices from Matterbridge.
1212
+ *
1213
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1214
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1215
+ */
1001
1216
  getDevices(pluginName) {
1002
1217
  if (this.matterbridge.hasCleanupStarted)
1003
- return [];
1218
+ return []; // Skip if cleanup has started
1004
1219
  const devices = [];
1005
1220
  for (const device of this.matterbridge.devices.array()) {
1221
+ // Filter by pluginName if provided
1006
1222
  if (pluginName && pluginName !== device.plugin)
1007
1223
  continue;
1224
+ // Check if the device has the required properties
1008
1225
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1009
1226
  continue;
1010
1227
  devices.push({
@@ -1024,24 +1241,39 @@ export class Frontend extends EventEmitter {
1024
1241
  }
1025
1242
  return devices;
1026
1243
  }
1244
+ /**
1245
+ * Retrieves the clusters from a given plugin and endpoint number.
1246
+ *
1247
+ * Response for /api/clusters
1248
+ *
1249
+ * @param {string} pluginName - The name of the plugin.
1250
+ * @param {number} endpointNumber - The endpoint number.
1251
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1252
+ */
1027
1253
  getClusters(pluginName, endpointNumber) {
1028
1254
  if (this.matterbridge.hasCleanupStarted)
1029
- return;
1255
+ return; // Skip if cleanup has started
1030
1256
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1031
1257
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1032
1258
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1033
1259
  return;
1034
1260
  }
1261
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1262
+ // Get the device types from the main endpoint
1035
1263
  const deviceTypes = [];
1036
1264
  const clusters = [];
1037
1265
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1038
1266
  deviceTypes.push(d.deviceType);
1039
1267
  });
1268
+ // Get the clusters from the main endpoint
1040
1269
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1041
1270
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1042
1271
  return;
1043
1272
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1044
1273
  return;
1274
+ // console.log(
1275
+ // `${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}`,
1276
+ // );
1045
1277
  clusters.push({
1046
1278
  endpoint: endpoint.number.toString(),
1047
1279
  number: endpoint.number,
@@ -1055,12 +1287,19 @@ export class Frontend extends EventEmitter {
1055
1287
  attributeLocalValue: attributeValue,
1056
1288
  });
1057
1289
  });
1290
+ // Get the child endpoints
1058
1291
  const childEndpoints = endpoint.getChildEndpoints();
1292
+ // if (childEndpoints.length === 0) {
1293
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1294
+ // }
1059
1295
  childEndpoints.forEach((childEndpoint) => {
1296
+ // istanbul ignore if cause is not reachable: should never happen but ...
1060
1297
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1061
1298
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1062
1299
  return;
1063
1300
  }
1301
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1302
+ // Get the device types of the child endpoint
1064
1303
  const deviceTypes = [];
1065
1304
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1066
1305
  deviceTypes.push(d.deviceType);
@@ -1070,6 +1309,9 @@ export class Frontend extends EventEmitter {
1070
1309
  return;
1071
1310
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1072
1311
  return;
1312
+ // console.log(
1313
+ // `${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}`,
1314
+ // );
1073
1315
  clusters.push({
1074
1316
  endpoint: childEndpoint.number.toString(),
1075
1317
  number: childEndpoint.number,
@@ -1089,6 +1331,7 @@ export class Frontend extends EventEmitter {
1089
1331
  async generateDiagnostic() {
1090
1332
  this.log.debug('Generating diagnostic...');
1091
1333
  const serverNodes = [];
1334
+ // istanbul ignore else
1092
1335
  if (this.matterbridge.bridgeMode === 'bridge') {
1093
1336
  if (this.matterbridge.serverNode)
1094
1337
  serverNodes.push(this.matterbridge.serverNode);
@@ -1099,6 +1342,7 @@ export class Frontend extends EventEmitter {
1099
1342
  serverNodes.push(plugin.serverNode);
1100
1343
  }
1101
1344
  }
1345
+ // istanbul ignore next
1102
1346
  for (const device of this.matterbridge.devices.array()) {
1103
1347
  if (device.serverNode)
1104
1348
  serverNodes.push(device.serverNode);
@@ -1122,8 +1366,15 @@ export class Frontend extends EventEmitter {
1122
1366
  values: [...serverNodes],
1123
1367
  })));
1124
1368
  delete Logger.destinations.diagnostic;
1125
- await wait(500);
1369
+ await wait(500); // Wait for the log to be written
1126
1370
  }
1371
+ /**
1372
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1373
+ *
1374
+ * @param {WebSocket} client - The websocket client that sent the message.
1375
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1376
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1377
+ */
1127
1378
  async wsMessageHandler(client, message) {
1128
1379
  let data;
1129
1380
  const sendResponse = (data) => {
@@ -1143,7 +1394,7 @@ export class Frontend extends EventEmitter {
1143
1394
  };
1144
1395
  try {
1145
1396
  data = JSON.parse(message.toString());
1146
- if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
1397
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
1147
1398
  this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1148
1399
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1149
1400
  return;
@@ -1200,7 +1451,22 @@ export class Frontend extends EventEmitter {
1200
1451
  }
1201
1452
  this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
1202
1453
  this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
1203
- data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
1454
+ data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
1455
+ /*
1456
+ const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
1457
+ if (plugin) {
1458
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1459
+ await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
1460
+ this.wssSendRestartRequired();
1461
+ this.wssSendRefreshRequired('plugins');
1462
+ this.wssSendRefreshRequired('devices');
1463
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1464
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1465
+ } else {
1466
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1467
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1468
+ }
1469
+ */
1204
1470
  const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
1205
1471
  if (plugin) {
1206
1472
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1214,6 +1480,7 @@ export class Frontend extends EventEmitter {
1214
1480
  return;
1215
1481
  })
1216
1482
  .catch((_error) => {
1483
+ //
1217
1484
  });
1218
1485
  }
1219
1486
  else {
@@ -1228,6 +1495,10 @@ export class Frontend extends EventEmitter {
1228
1495
  }
1229
1496
  this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
1230
1497
  this.log.debug(`Removing plugin ${data.params.pluginName}...`);
1498
+ /*
1499
+ await this.server.fetch({ type: 'plugins_shutdown', src: this.server.name, dst: 'plugins', params: { plugin: data.params.pluginName, reason: 'The plugin has been removed.', removeAllDevices: true } }, 5000);
1500
+ await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
1501
+ */
1231
1502
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1232
1503
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1233
1504
  await this.matterbridge.plugins.remove(data.params.pluginName);
@@ -1263,6 +1534,7 @@ export class Frontend extends EventEmitter {
1263
1534
  return;
1264
1535
  })
1265
1536
  .catch((_error) => {
1537
+ //
1266
1538
  });
1267
1539
  }
1268
1540
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1288,6 +1560,7 @@ export class Frontend extends EventEmitter {
1288
1560
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1289
1561
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1290
1562
  if (plugin.serverNode) {
1563
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1291
1564
  await this.matterbridge.stopServerNode(plugin.serverNode);
1292
1565
  plugin.serverNode = undefined;
1293
1566
  }
@@ -1297,18 +1570,20 @@ export class Frontend extends EventEmitter {
1297
1570
  this.matterbridge.devices.remove(device);
1298
1571
  }
1299
1572
  }
1573
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1300
1574
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1301
1575
  await this.matterbridge.createDynamicPlugin(plugin);
1302
1576
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1303
- plugin.restartRequired = false;
1577
+ plugin.restartRequired = false; // Reset plugin restartRequired
1304
1578
  let needRestart = 0;
1305
1579
  for (const plugin of this.matterbridge.plugins) {
1306
1580
  if (plugin.restartRequired)
1307
1581
  needRestart++;
1308
1582
  }
1309
1583
  if (needRestart === 0) {
1310
- this.wssSendRestartNotRequired(true);
1584
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1311
1585
  }
1586
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1312
1587
  if (plugin.serverNode)
1313
1588
  await this.matterbridge.startServerNode(plugin.serverNode);
1314
1589
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1574,22 +1849,22 @@ export class Frontend extends EventEmitter {
1574
1849
  if (isValidString(data.params.value, 4)) {
1575
1850
  this.log.debug('Matterbridge logger level:', data.params.value);
1576
1851
  if (data.params.value === 'Debug') {
1577
- await this.matterbridge.setLogLevel("debug");
1852
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1578
1853
  }
1579
1854
  else if (data.params.value === 'Info') {
1580
- await this.matterbridge.setLogLevel("info");
1855
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1581
1856
  }
1582
1857
  else if (data.params.value === 'Notice') {
1583
- await this.matterbridge.setLogLevel("notice");
1858
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1584
1859
  }
1585
1860
  else if (data.params.value === 'Warn') {
1586
- await this.matterbridge.setLogLevel("warn");
1861
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1587
1862
  }
1588
1863
  else if (data.params.value === 'Error') {
1589
- await this.matterbridge.setLogLevel("error");
1864
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1590
1865
  }
1591
1866
  else if (data.params.value === 'Fatal') {
1592
- await this.matterbridge.setLogLevel("fatal");
1867
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1593
1868
  }
1594
1869
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1595
1870
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1600,6 +1875,7 @@ export class Frontend extends EventEmitter {
1600
1875
  this.log.debug('Matterbridge file log:', data.params.value);
1601
1876
  this.matterbridge.fileLogger = data.params.value;
1602
1877
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1878
+ // Create the file logger for matterbridge
1603
1879
  if (data.params.value)
1604
1880
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1605
1881
  else
@@ -1628,11 +1904,12 @@ export class Frontend extends EventEmitter {
1628
1904
  else if (data.params.value === 'Fatal') {
1629
1905
  Logger.level = MatterLogLevel.FATAL;
1630
1906
  }
1631
- let callbackLogLevel = "notice";
1632
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1633
- callbackLogLevel = "info";
1634
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1635
- callbackLogLevel = "debug";
1907
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
1908
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
1909
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
1910
+ callbackLogLevel = "info" /* LogLevel.INFO */;
1911
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
1912
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1636
1913
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1637
1914
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1638
1915
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1684,6 +1961,7 @@ export class Frontend extends EventEmitter {
1684
1961
  }
1685
1962
  break;
1686
1963
  case 'setmatterport':
1964
+ // eslint-disable-next-line no-case-declarations
1687
1965
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1688
1966
  if (isValidNumber(port, 5540, 5600)) {
1689
1967
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1703,6 +1981,7 @@ export class Frontend extends EventEmitter {
1703
1981
  }
1704
1982
  break;
1705
1983
  case 'setmatterdiscriminator':
1984
+ // eslint-disable-next-line no-case-declarations
1706
1985
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1707
1986
  if (isValidNumber(discriminator, 0, 4095)) {
1708
1987
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1722,6 +2001,7 @@ export class Frontend extends EventEmitter {
1722
2001
  }
1723
2002
  break;
1724
2003
  case 'setmatterpasscode':
2004
+ // eslint-disable-next-line no-case-declarations
1725
2005
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1726
2006
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1727
2007
  this.matterbridge.passcode = passcode;
@@ -1767,15 +2047,19 @@ export class Frontend extends EventEmitter {
1767
2047
  return;
1768
2048
  }
1769
2049
  const config = plugin.configJson;
2050
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1770
2051
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2052
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1771
2053
  if (select === 'serial')
1772
2054
  this.log.info(`Selected device serial ${data.params.serial}`);
1773
2055
  if (select === 'name')
1774
2056
  this.log.info(`Selected device name ${data.params.name}`);
1775
2057
  if (config && select && (select === 'serial' || select === 'name')) {
2058
+ // Remove postfix from the serial if it exists
1776
2059
  if (config.postfix) {
1777
2060
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1778
2061
  }
2062
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1779
2063
  if (isValidArray(config.whiteList, 1)) {
1780
2064
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1781
2065
  config.whiteList.push(data.params.serial);
@@ -1784,6 +2068,7 @@ export class Frontend extends EventEmitter {
1784
2068
  config.whiteList.push(data.params.name);
1785
2069
  }
1786
2070
  }
2071
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1787
2072
  if (isValidArray(config.blackList, 1)) {
1788
2073
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1789
2074
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1811,7 +2096,9 @@ export class Frontend extends EventEmitter {
1811
2096
  return;
1812
2097
  }
1813
2098
  const config = plugin.configJson;
2099
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1814
2100
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2101
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1815
2102
  if (select === 'serial')
1816
2103
  this.log.info(`Unselected device serial ${data.params.serial}`);
1817
2104
  if (select === 'name')
@@ -1820,6 +2107,7 @@ export class Frontend extends EventEmitter {
1820
2107
  if (config.postfix) {
1821
2108
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1822
2109
  }
2110
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1823
2111
  if (isValidArray(config.whiteList, 1)) {
1824
2112
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1825
2113
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1828,6 +2116,7 @@ export class Frontend extends EventEmitter {
1828
2116
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1829
2117
  }
1830
2118
  }
2119
+ // Add the serial to the blackList
1831
2120
  if (isValidArray(config.blackList)) {
1832
2121
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1833
2122
  config.blackList.push(data.params.serial);
@@ -1850,6 +2139,7 @@ export class Frontend extends EventEmitter {
1850
2139
  }
1851
2140
  }
1852
2141
  else {
2142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1853
2143
  const localData = data;
1854
2144
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1855
2145
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1859,23 +2149,46 @@ export class Frontend extends EventEmitter {
1859
2149
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1860
2150
  }
1861
2151
  }
2152
+ /**
2153
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2154
+ *
2155
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2156
+ * @param {string} time - The time string of the message
2157
+ * @param {string} name - The logger name of the message
2158
+ * @param {string} message - The content of the message.
2159
+ *
2160
+ * @remarks
2161
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2162
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2163
+ * The function sends the message to all connected clients.
2164
+ */
1862
2165
  wssSendLogMessage(level, time, name, message) {
1863
2166
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1864
2167
  return;
1865
2168
  if (!level || !time || !name || !message)
1866
2169
  return;
2170
+ // Remove ANSI escape codes from the message
2171
+ // eslint-disable-next-line no-control-regex
1867
2172
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2173
+ // Remove leading asterisks from the message
1868
2174
  message = message.replace(/^\*+/, '');
2175
+ // Replace all occurrences of \t and \n
1869
2176
  message = message.replace(/[\t\n]/g, '');
2177
+ // Remove non-printable characters
2178
+ // eslint-disable-next-line no-control-regex
1870
2179
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2180
+ // Replace all occurrences of \" with "
1871
2181
  message = message.replace(/\\"/g, '"');
2182
+ // Define the maximum allowed length for continuous characters without a space
1872
2183
  const maxContinuousLength = 100;
1873
2184
  const keepStartLength = 20;
1874
2185
  const keepEndLength = 20;
2186
+ // Split the message into words
1875
2187
  if (level !== 'spawn') {
1876
2188
  message = message
1877
2189
  .split(' ')
1878
2190
  .map((word) => {
2191
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1879
2192
  if (word.length > maxContinuousLength) {
1880
2193
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1881
2194
  }
@@ -1883,14 +2196,34 @@ export class Frontend extends EventEmitter {
1883
2196
  })
1884
2197
  .join(' ');
1885
2198
  }
2199
+ // Send the message to all connected clients
1886
2200
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1887
2201
  }
2202
+ /**
2203
+ * Sends a need to refresh WebSocket message to all connected clients.
2204
+ *
2205
+ * @param {string} changed - The changed value.
2206
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2207
+ * possible values for changed:
2208
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2209
+ * - 'plugins'
2210
+ * - 'devices'
2211
+ * - 'matter' with param 'matter' (QRDiv component)
2212
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2213
+ */
1888
2214
  wssSendRefreshRequired(changed, params) {
1889
2215
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1890
2216
  return;
1891
2217
  this.log.debug('Sending a refresh required message to all connected clients');
2218
+ // Send the message to all connected clients
1892
2219
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1893
2220
  }
2221
+ /**
2222
+ * Sends a need to restart WebSocket message to all connected clients.
2223
+ *
2224
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2225
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2226
+ */
1894
2227
  wssSendRestartRequired(snackbar = true, fixed = false) {
1895
2228
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1896
2229
  return;
@@ -1899,8 +2232,14 @@ export class Frontend extends EventEmitter {
1899
2232
  this.matterbridge.fixedRestartRequired = fixed;
1900
2233
  if (snackbar === true)
1901
2234
  this.wssSendSnackbarMessage(`Restart required`, 0);
2235
+ // Send the message to all connected clients
1902
2236
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1903
2237
  }
2238
+ /**
2239
+ * Sends a no need to restart WebSocket message to all connected clients.
2240
+ *
2241
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2242
+ */
1904
2243
  wssSendRestartNotRequired(snackbar = true) {
1905
2244
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1906
2245
  return;
@@ -1908,57 +2247,133 @@ export class Frontend extends EventEmitter {
1908
2247
  this.matterbridge.restartRequired = false;
1909
2248
  if (snackbar === true)
1910
2249
  this.wssSendCloseSnackbarMessage(`Restart required`);
2250
+ // Send the message to all connected clients
1911
2251
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1912
2252
  }
2253
+ /**
2254
+ * Sends a need to update WebSocket message to all connected clients.
2255
+ *
2256
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2257
+ */
1913
2258
  wssSendUpdateRequired(devVersion = false) {
1914
2259
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1915
2260
  return;
1916
2261
  this.log.debug('Sending an update required message to all connected clients');
1917
2262
  this.matterbridge.updateRequired = true;
2263
+ // Send the message to all connected clients
1918
2264
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1919
2265
  }
2266
+ /**
2267
+ * Sends a cpu update message to all connected clients.
2268
+ *
2269
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2270
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2271
+ */
1920
2272
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1921
2273
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1922
2274
  return;
1923
2275
  if (hasParameter('debug'))
1924
2276
  this.log.debug('Sending a cpu update message to all connected clients');
2277
+ // Send the message to all connected clients
1925
2278
  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 } });
1926
2279
  }
2280
+ /**
2281
+ * Sends a memory update message to all connected clients.
2282
+ *
2283
+ * @param {string} totalMemory - The total memory in bytes.
2284
+ * @param {string} freeMemory - The free memory in bytes.
2285
+ * @param {string} rss - The resident set size in bytes.
2286
+ * @param {string} heapTotal - The total heap memory in bytes.
2287
+ * @param {string} heapUsed - The used heap memory in bytes.
2288
+ * @param {string} external - The external memory in bytes.
2289
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2290
+ */
1927
2291
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1928
2292
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1929
2293
  return;
1930
2294
  if (hasParameter('debug'))
1931
2295
  this.log.debug('Sending a memory update message to all connected clients');
2296
+ // Send the message to all connected clients
1932
2297
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1933
2298
  }
2299
+ /**
2300
+ * Sends an uptime update message to all connected clients.
2301
+ *
2302
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2303
+ * @param {string} processUptime - The process uptime in a human-readable format.
2304
+ */
1934
2305
  wssSendUptimeUpdate(systemUptime, processUptime) {
1935
2306
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1936
2307
  return;
1937
2308
  if (hasParameter('debug'))
1938
2309
  this.log.debug('Sending a uptime update message to all connected clients');
2310
+ // Send the message to all connected clients
1939
2311
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1940
2312
  }
2313
+ /**
2314
+ * Sends an open snackbar message to all connected clients.
2315
+ *
2316
+ * @param {string} message - The message to send.
2317
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2318
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2319
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2320
+ *
2321
+ * @remarks
2322
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2323
+ */
1941
2324
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1942
2325
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1943
2326
  return;
1944
2327
  this.log.debug('Sending a snackbar message to all connected clients');
2328
+ // Send the message to all connected clients
1945
2329
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
1946
2330
  }
2331
+ /**
2332
+ * Sends a close snackbar message to all connected clients.
2333
+ * It will close the snackbar message with the same message and timeout = 0.
2334
+ *
2335
+ * @param {string} message - The message to send.
2336
+ */
1947
2337
  wssSendCloseSnackbarMessage(message) {
1948
2338
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1949
2339
  return;
1950
2340
  this.log.debug('Sending a close snackbar message to all connected clients');
2341
+ // Send the message to all connected clients
1951
2342
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
1952
2343
  }
2344
+ /**
2345
+ * Sends an attribute update message to all connected WebSocket clients.
2346
+ *
2347
+ * @param {string | undefined} plugin - The name of the plugin.
2348
+ * @param {string | undefined} serialNumber - The serial number of the device.
2349
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2350
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2351
+ * @param {string} id - The endpoint id where the attribute belongs.
2352
+ * @param {string} cluster - The cluster name where the attribute belongs.
2353
+ * @param {string} attribute - The name of the attribute that changed.
2354
+ * @param {number | string | boolean} value - The new value of the attribute.
2355
+ *
2356
+ * @remarks
2357
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2358
+ * with the updated attribute information.
2359
+ */
1953
2360
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
1954
2361
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1955
2362
  return;
1956
2363
  this.log.debug('Sending an attribute update message to all connected clients');
2364
+ // Send the message to all connected clients
1957
2365
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
1958
2366
  }
2367
+ /**
2368
+ * Sends a message to all connected clients.
2369
+ * This is an helper function to send a broadcast message to all connected clients.
2370
+ *
2371
+ * @param {WsMessageBroadcast} msg - The message to send.
2372
+ */
1959
2373
  wssBroadcastMessage(msg) {
1960
2374
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1961
2375
  return;
2376
+ // Send the message to all connected clients
1962
2377
  const stringifiedMsg = JSON.stringify(msg);
1963
2378
  if (msg.method !== 'log')
1964
2379
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
@@ -1969,3 +2384,4 @@ export class Frontend extends EventEmitter {
1969
2384
  });
1970
2385
  }
1971
2386
  }
2387
+ //# sourceMappingURL=frontend.js.map