matterbridge 3.4.0-dev-20251126-5087664 → 3.4.0

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 (314) 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 +838 -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 +135 -0
  30. package/dist/deviceManager.d.ts.map +1 -0
  31. package/dist/deviceManager.js +113 -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 +61 -0
  42. package/dist/devices/cooktop.d.ts.map +1 -0
  43. package/dist/devices/cooktop.js +56 -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 +43 -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 +455 -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 +34 -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/jestutils/export.d.ts +2 -0
  150. package/dist/jestutils/export.d.ts.map +1 -0
  151. package/dist/jestutils/export.js +1 -0
  152. package/dist/jestutils/export.js.map +1 -0
  153. package/dist/jestutils/jestHelpers.d.ts +303 -0
  154. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  155. package/dist/jestutils/jestHelpers.js +352 -13
  156. package/dist/jestutils/jestHelpers.js.map +1 -0
  157. package/dist/logger/export.d.ts +2 -0
  158. package/dist/logger/export.d.ts.map +1 -0
  159. package/dist/logger/export.js +1 -0
  160. package/dist/logger/export.js.map +1 -0
  161. package/dist/matter/behaviors.d.ts +2 -0
  162. package/dist/matter/behaviors.d.ts.map +1 -0
  163. package/dist/matter/behaviors.js +2 -0
  164. package/dist/matter/behaviors.js.map +1 -0
  165. package/dist/matter/clusters.d.ts +2 -0
  166. package/dist/matter/clusters.d.ts.map +1 -0
  167. package/dist/matter/clusters.js +2 -0
  168. package/dist/matter/clusters.js.map +1 -0
  169. package/dist/matter/devices.d.ts +2 -0
  170. package/dist/matter/devices.d.ts.map +1 -0
  171. package/dist/matter/devices.js +2 -0
  172. package/dist/matter/devices.js.map +1 -0
  173. package/dist/matter/endpoints.d.ts +2 -0
  174. package/dist/matter/endpoints.d.ts.map +1 -0
  175. package/dist/matter/endpoints.js +2 -0
  176. package/dist/matter/endpoints.js.map +1 -0
  177. package/dist/matter/export.d.ts +5 -0
  178. package/dist/matter/export.d.ts.map +1 -0
  179. package/dist/matter/export.js +3 -0
  180. package/dist/matter/export.js.map +1 -0
  181. package/dist/matter/types.d.ts +3 -0
  182. package/dist/matter/types.d.ts.map +1 -0
  183. package/dist/matter/types.js +3 -0
  184. package/dist/matter/types.js.map +1 -0
  185. package/dist/matterNode.d.ts +342 -0
  186. package/dist/matterNode.d.ts.map +1 -0
  187. package/dist/matterNode.js +369 -8
  188. package/dist/matterNode.js.map +1 -0
  189. package/dist/matterbridge.d.ts +473 -0
  190. package/dist/matterbridge.d.ts.map +1 -0
  191. package/dist/matterbridge.js +787 -46
  192. package/dist/matterbridge.js.map +1 -0
  193. package/dist/matterbridgeAccessoryPlatform.d.ts +41 -0
  194. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  195. package/dist/matterbridgeAccessoryPlatform.js +38 -0
  196. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  197. package/dist/matterbridgeBehaviors.d.ts +2404 -0
  198. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  199. package/dist/matterbridgeBehaviors.js +68 -5
  200. package/dist/matterbridgeBehaviors.js.map +1 -0
  201. package/dist/matterbridgeDeviceTypes.d.ts +698 -0
  202. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  203. package/dist/matterbridgeDeviceTypes.js +635 -14
  204. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  205. package/dist/matterbridgeDynamicPlatform.d.ts +41 -0
  206. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  207. package/dist/matterbridgeDynamicPlatform.js +38 -0
  208. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  209. package/dist/matterbridgeEndpoint.d.ts +1507 -0
  210. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  211. package/dist/matterbridgeEndpoint.js +1444 -53
  212. package/dist/matterbridgeEndpoint.js.map +1 -0
  213. package/dist/matterbridgeEndpointHelpers.d.ts +787 -0
  214. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  215. package/dist/matterbridgeEndpointHelpers.js +483 -20
  216. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  217. package/dist/matterbridgeEndpointTypes.d.ts +166 -0
  218. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  219. package/dist/matterbridgeEndpointTypes.js +25 -0
  220. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  221. package/dist/matterbridgePlatform.d.ts +524 -0
  222. package/dist/matterbridgePlatform.d.ts.map +1 -0
  223. package/dist/matterbridgePlatform.js +439 -1
  224. package/dist/matterbridgePlatform.js.map +1 -0
  225. package/dist/matterbridgeTypes.d.ts +251 -0
  226. package/dist/matterbridgeTypes.d.ts.map +1 -0
  227. package/dist/matterbridgeTypes.js +26 -0
  228. package/dist/matterbridgeTypes.js.map +1 -0
  229. package/dist/pluginManager.d.ts +371 -0
  230. package/dist/pluginManager.d.ts.map +1 -0
  231. package/dist/pluginManager.js +340 -5
  232. package/dist/pluginManager.js.map +1 -0
  233. package/dist/shelly.d.ts +174 -0
  234. package/dist/shelly.d.ts.map +1 -0
  235. package/dist/shelly.js +168 -7
  236. package/dist/shelly.js.map +1 -0
  237. package/dist/storage/export.d.ts +2 -0
  238. package/dist/storage/export.d.ts.map +1 -0
  239. package/dist/storage/export.js +1 -0
  240. package/dist/storage/export.js.map +1 -0
  241. package/dist/update.d.ts +75 -0
  242. package/dist/update.d.ts.map +1 -0
  243. package/dist/update.js +69 -0
  244. package/dist/update.js.map +1 -0
  245. package/dist/utils/colorUtils.d.ts +101 -0
  246. package/dist/utils/colorUtils.d.ts.map +1 -0
  247. package/dist/utils/colorUtils.js +97 -2
  248. package/dist/utils/colorUtils.js.map +1 -0
  249. package/dist/utils/commandLine.d.ts +66 -0
  250. package/dist/utils/commandLine.d.ts.map +1 -0
  251. package/dist/utils/commandLine.js +60 -0
  252. package/dist/utils/commandLine.js.map +1 -0
  253. package/dist/utils/copyDirectory.d.ts +35 -0
  254. package/dist/utils/copyDirectory.d.ts.map +1 -0
  255. package/dist/utils/copyDirectory.js +37 -0
  256. package/dist/utils/copyDirectory.js.map +1 -0
  257. package/dist/utils/createDirectory.d.ts +34 -0
  258. package/dist/utils/createDirectory.d.ts.map +1 -0
  259. package/dist/utils/createDirectory.js +33 -0
  260. package/dist/utils/createDirectory.js.map +1 -0
  261. package/dist/utils/createZip.d.ts +39 -0
  262. package/dist/utils/createZip.d.ts.map +1 -0
  263. package/dist/utils/createZip.js +47 -2
  264. package/dist/utils/createZip.js.map +1 -0
  265. package/dist/utils/deepCopy.d.ts +32 -0
  266. package/dist/utils/deepCopy.d.ts.map +1 -0
  267. package/dist/utils/deepCopy.js +39 -0
  268. package/dist/utils/deepCopy.js.map +1 -0
  269. package/dist/utils/deepEqual.d.ts +54 -0
  270. package/dist/utils/deepEqual.d.ts.map +1 -0
  271. package/dist/utils/deepEqual.js +72 -1
  272. package/dist/utils/deepEqual.js.map +1 -0
  273. package/dist/utils/error.d.ts +44 -0
  274. package/dist/utils/error.d.ts.map +1 -0
  275. package/dist/utils/error.js +41 -0
  276. package/dist/utils/error.js.map +1 -0
  277. package/dist/utils/export.d.ts +13 -0
  278. package/dist/utils/export.d.ts.map +1 -0
  279. package/dist/utils/export.js +1 -0
  280. package/dist/utils/export.js.map +1 -0
  281. package/dist/utils/format.d.ts +53 -0
  282. package/dist/utils/format.d.ts.map +1 -0
  283. package/dist/utils/format.js +49 -0
  284. package/dist/utils/format.js.map +1 -0
  285. package/dist/utils/hex.d.ts +89 -0
  286. package/dist/utils/hex.d.ts.map +1 -0
  287. package/dist/utils/hex.js +124 -0
  288. package/dist/utils/hex.js.map +1 -0
  289. package/dist/utils/inspector.d.ts +87 -0
  290. package/dist/utils/inspector.d.ts.map +1 -0
  291. package/dist/utils/inspector.js +69 -1
  292. package/dist/utils/inspector.js.map +1 -0
  293. package/dist/utils/isvalid.d.ts +103 -0
  294. package/dist/utils/isvalid.d.ts.map +1 -0
  295. package/dist/utils/isvalid.js +101 -0
  296. package/dist/utils/isvalid.js.map +1 -0
  297. package/dist/utils/network.d.ts +111 -0
  298. package/dist/utils/network.d.ts.map +1 -0
  299. package/dist/utils/network.js +96 -5
  300. package/dist/utils/network.js.map +1 -0
  301. package/dist/utils/spawn.d.ts +33 -0
  302. package/dist/utils/spawn.d.ts.map +1 -0
  303. package/dist/utils/spawn.js +71 -1
  304. package/dist/utils/spawn.js.map +1 -0
  305. package/dist/utils/tracker.d.ts +108 -0
  306. package/dist/utils/tracker.d.ts.map +1 -0
  307. package/dist/utils/tracker.js +64 -1
  308. package/dist/utils/tracker.js.map +1 -0
  309. package/dist/utils/wait.d.ts +54 -0
  310. package/dist/utils/wait.d.ts.map +1 -0
  311. package/dist/utils/wait.js +60 -8
  312. package/dist/utils/wait.js.map +1 -0
  313. package/npm-shrinkwrap.json +2 -2
  314. 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));
@@ -140,23 +166,53 @@ export class Frontend extends EventEmitter {
140
166
  this.port = port;
141
167
  this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
142
168
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
169
+ // Initialize multer with the upload directory
143
170
  const multer = await import('multer');
144
- const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
171
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
145
172
  const upload = multer.default({ dest: uploadDir });
173
+ // Create the express app that serves the frontend
146
174
  const express = await import('express');
147
175
  this.expressApp = express.default();
176
+ // Inject logging/debug wrapper for route/middleware registration
177
+ /*
178
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
179
+ for (const method of methods) {
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
184
+ try {
185
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
186
+ return original(path, ...rest);
187
+ } catch (err) {
188
+ console.error(`[ERROR] Failed to register route: ${path}`);
189
+ throw err;
190
+ }
191
+ };
192
+ }
193
+ */
194
+ // Log all requests to the server for debugging
195
+ /*
196
+ this.expressApp.use((req, res, next) => {
197
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
198
+ next();
199
+ });
200
+ */
201
+ // Serve static files from 'frontend/build' directory
148
202
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend', 'build')));
203
+ // Create a WebSocket server and attach it to the http or https server
149
204
  this.log.debug(`Creating WebSocketServer...`);
150
205
  const ws = await import('ws');
151
206
  this.webSocketServer = new ws.WebSocketServer({ noServer: true });
152
207
  this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
153
208
  this.webSocketServer.on('connection', (ws, request) => {
154
209
  const clientIp = request.socket.remoteAddress;
155
- let callbackLogLevel = "notice";
156
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
157
- callbackLogLevel = "info";
158
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
159
- callbackLogLevel = "debug";
210
+ // Set the global logger callback for the WebSocketServer
211
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
212
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
213
+ callbackLogLevel = "info" /* LogLevel.INFO */;
214
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
215
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
160
216
  AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
161
217
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
162
218
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -178,16 +234,25 @@ export class Frontend extends EventEmitter {
178
234
  }
179
235
  });
180
236
  ws.on('error', (error) => {
237
+ // istanbul ignore next
181
238
  this.log.error(`WebSocket client error: ${error}`);
182
239
  });
183
240
  });
184
241
  this.webSocketServer.on('close', () => {
185
242
  this.log.debug(`WebSocketServer closed`);
186
243
  });
244
+ /* With { noServer: true } it never fires
245
+ this.webSocketServer.on('listening', () => {
246
+ this.log.info(`The WebSocketServer is listening`);
247
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
248
+ });
249
+ */
250
+ // istanbul ignore next
187
251
  this.webSocketServer.on('error', (ws, error) => {
188
252
  this.log.error(`WebSocketServer error: ${error}`);
189
253
  });
190
254
  if (!hasParameter('ssl')) {
255
+ // Create an HTTP server and attach the express app
191
256
  const http = await import('node:http');
192
257
  try {
193
258
  this.log.debug(`Creating HTTP server...`);
@@ -198,6 +263,7 @@ export class Frontend extends EventEmitter {
198
263
  this.emit('server_error', error);
199
264
  return;
200
265
  }
266
+ // Listen on the specified port
201
267
  if (hasParameter('ingress')) {
202
268
  this.httpServer.listen(this.port, '0.0.0.0', () => {
203
269
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -217,24 +283,30 @@ export class Frontend extends EventEmitter {
217
283
  }
218
284
  this.httpServer.on('upgrade', async (req, socket, head) => {
219
285
  try {
286
+ // Only proceed for real WebSocket upgrades
287
+ // istanbul ignore next cause is only a safety check
220
288
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
221
289
  this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
222
290
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
223
291
  return socket.destroy();
224
292
  }
293
+ // Build a URL so we can read ?password=...
225
294
  const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
295
+ // Validate WebSocket password
226
296
  const password = url.searchParams.get('password') ?? '';
227
297
  if (password !== this.storedPassword) {
228
298
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
229
299
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
230
300
  return socket.destroy();
231
301
  }
302
+ // Complete the WebSocket handshake
232
303
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
233
304
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
234
305
  this.webSocketServer?.emit('connection', ws, req);
235
306
  });
236
307
  }
237
308
  catch (err) {
309
+ /* istanbul ignore next: only triggered on unexpected internal error */
238
310
  {
239
311
  inspectError(this.log, 'WebSocket upgrade error:', err);
240
312
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -257,6 +329,7 @@ export class Frontend extends EventEmitter {
257
329
  });
258
330
  }
259
331
  else {
332
+ // SSL is enabled, load the certificate and the private key
260
333
  let cert;
261
334
  let key;
262
335
  let ca;
@@ -266,6 +339,7 @@ export class Frontend extends EventEmitter {
266
339
  let httpsServerOptions = {};
267
340
  const fs = await import('node:fs');
268
341
  if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
342
+ // Load the p12 certificate and the passphrase
269
343
  try {
270
344
  pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
271
345
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
@@ -277,7 +351,7 @@ export class Frontend extends EventEmitter {
277
351
  }
278
352
  try {
279
353
  passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
280
- passphrase = passphrase.trim();
354
+ passphrase = passphrase.trim(); // Ensure no extra characters
281
355
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
282
356
  }
283
357
  catch (error) {
@@ -288,6 +362,7 @@ export class Frontend extends EventEmitter {
288
362
  httpsServerOptions = { pfx, passphrase };
289
363
  }
290
364
  else {
365
+ // 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.
291
366
  try {
292
367
  cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
293
368
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
@@ -317,9 +392,10 @@ export class Frontend extends EventEmitter {
317
392
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
318
393
  }
319
394
  if (hasParameter('mtls')) {
320
- httpsServerOptions.requestCert = true;
321
- httpsServerOptions.rejectUnauthorized = true;
395
+ httpsServerOptions.requestCert = true; // Request client certificate
396
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
322
397
  }
398
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
323
399
  const https = await import('node:https');
324
400
  try {
325
401
  this.log.debug(`Creating HTTPS server...`);
@@ -330,6 +406,7 @@ export class Frontend extends EventEmitter {
330
406
  this.emit('server_error', error);
331
407
  return;
332
408
  }
409
+ // Listen on the specified port
333
410
  if (hasParameter('ingress')) {
334
411
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
335
412
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -349,23 +426,29 @@ export class Frontend extends EventEmitter {
349
426
  }
350
427
  this.httpsServer.on('upgrade', async (req, socket, head) => {
351
428
  try {
429
+ // Only proceed for real WebSocket upgrades
430
+ // istanbul ignore next cause is only a safety check
352
431
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
353
432
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
354
433
  return socket.destroy();
355
434
  }
435
+ // Build a URL so we can read ?password=...
356
436
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
437
+ // Validate WebSocket password
357
438
  const password = url.searchParams.get('password') ?? '';
358
439
  if (password !== this.storedPassword) {
359
440
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
360
441
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
361
442
  return socket.destroy();
362
443
  }
444
+ // Complete the WebSocket handshake
363
445
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
364
446
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
365
447
  this.webSocketServer?.emit('connection', ws, req);
366
448
  });
367
449
  }
368
450
  catch (err) {
451
+ /* istanbul ignore next: only triggered on unexpected internal error */
369
452
  {
370
453
  inspectError(this.log, 'WebSocket upgrade error:', err);
371
454
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -387,6 +470,7 @@ export class Frontend extends EventEmitter {
387
470
  return;
388
471
  });
389
472
  }
473
+ // Subscribe to cli events
390
474
  cliEmitter.removeAllListeners();
391
475
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
392
476
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -397,6 +481,8 @@ export class Frontend extends EventEmitter {
397
481
  cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
398
482
  this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
399
483
  });
484
+ // Endpoint to validate login code
485
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
400
486
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
401
487
  const { password } = req.body;
402
488
  this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
@@ -409,17 +495,20 @@ export class Frontend extends EventEmitter {
409
495
  res.json({ valid: false });
410
496
  }
411
497
  });
498
+ // Endpoint to provide health check for docker
412
499
  this.expressApp.get('/health', (req, res) => {
413
500
  this.log.debug('Express received /health');
414
501
  const healthStatus = {
415
- status: 'ok',
416
- uptime: process.uptime(),
417
- timestamp: new Date().toISOString(),
502
+ status: 'ok', // Indicate service is healthy
503
+ uptime: process.uptime(), // Server uptime in seconds
504
+ timestamp: new Date().toISOString(), // Current timestamp
418
505
  };
419
506
  res.status(200).json(healthStatus);
420
507
  });
508
+ // Endpoint to provide memory usage details
421
509
  this.expressApp.get('/memory', async (req, res) => {
422
510
  this.log.debug('Express received /memory');
511
+ // Memory usage from process
423
512
  const memoryUsageRaw = process.memoryUsage();
424
513
  const memoryUsage = {
425
514
  rss: formatBytes(memoryUsageRaw.rss),
@@ -428,10 +517,13 @@ export class Frontend extends EventEmitter {
428
517
  external: formatBytes(memoryUsageRaw.external),
429
518
  arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
430
519
  };
520
+ // V8 heap statistics
431
521
  const { default: v8 } = await import('node:v8');
432
522
  const heapStatsRaw = v8.getHeapStatistics();
433
523
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
524
+ // Format heapStats
434
525
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
526
+ // Format heapSpaces
435
527
  const heapSpaces = heapSpacesRaw.map((space) => ({
436
528
  ...space,
437
529
  space_size: formatBytes(space.space_size),
@@ -450,18 +542,22 @@ export class Frontend extends EventEmitter {
450
542
  };
451
543
  res.status(200).json(memoryReport);
452
544
  });
545
+ // Endpoint to provide settings
453
546
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
454
547
  this.log.debug('The frontend sent /api/settings');
455
548
  res.json(await this.getApiSettings());
456
549
  });
550
+ // Endpoint to provide plugins
457
551
  this.expressApp.get('/api/plugins', async (req, res) => {
458
552
  this.log.debug('The frontend sent /api/plugins');
459
553
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
460
554
  });
555
+ // Endpoint to provide devices
461
556
  this.expressApp.get('/api/devices', async (req, res) => {
462
557
  this.log.debug('The frontend sent /api/devices');
463
558
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
464
559
  });
560
+ // Endpoint to view the matterbridge log
465
561
  this.expressApp.get('/api/view-mblog', async (req, res) => {
466
562
  this.log.debug('The frontend sent /api/view-mblog');
467
563
  try {
@@ -475,6 +571,7 @@ export class Frontend extends EventEmitter {
475
571
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
476
572
  }
477
573
  });
574
+ // Endpoint to view the matter.js log
478
575
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
479
576
  this.log.debug('The frontend sent /api/view-mjlog');
480
577
  try {
@@ -488,6 +585,7 @@ export class Frontend extends EventEmitter {
488
585
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
489
586
  }
490
587
  });
588
+ // Endpoint to view the diagnostic.log
491
589
  this.expressApp.get('/api/view-diagnostic', async (req, res) => {
492
590
  this.log.debug('The frontend sent /api/view-diagnostic');
493
591
  await this.generateDiagnostic();
@@ -498,10 +596,13 @@ export class Frontend extends EventEmitter {
498
596
  res.send(data.slice(29));
499
597
  }
500
598
  catch (error) {
599
+ // istanbul ignore next
501
600
  this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
601
+ // istanbul ignore next
502
602
  res.status(500).send('Error reading diagnostic log file.');
503
603
  }
504
604
  });
605
+ // Endpoint to download the diagnostic.log
505
606
  this.expressApp.get('/api/download-diagnostic', async (req, res) => {
506
607
  this.log.debug(`The frontend sent /api/download-diagnostic`);
507
608
  await this.generateDiagnostic();
@@ -512,16 +613,19 @@ export class Frontend extends EventEmitter {
512
613
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
513
614
  }
514
615
  catch (error) {
616
+ // istanbul ignore next
515
617
  this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
516
618
  }
517
619
  res.type('text/plain');
518
620
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
621
+ /* istanbul ignore if */
519
622
  if (error) {
520
623
  this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
521
624
  res.status(500).send('Error downloading the diagnostic log file');
522
625
  }
523
626
  });
524
627
  });
628
+ // Endpoint to view the history.html
525
629
  this.expressApp.get('/api/viewhistory', async (req, res) => {
526
630
  this.log.debug('The frontend sent /api/viewhistory');
527
631
  try {
@@ -535,6 +639,7 @@ export class Frontend extends EventEmitter {
535
639
  res.status(500).send('Error reading history file.');
536
640
  }
537
641
  });
642
+ // Endpoint to download the history.html
538
643
  this.expressApp.get('/api/downloadhistory', async (req, res) => {
539
644
  this.log.debug(`The frontend sent /api/downloadhistory`);
540
645
  try {
@@ -544,6 +649,7 @@ export class Frontend extends EventEmitter {
544
649
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
545
650
  res.type('text/plain');
546
651
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
652
+ /* istanbul ignore if */
547
653
  if (error) {
548
654
  this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
549
655
  res.status(500).send('Error downloading history file');
@@ -555,6 +661,7 @@ export class Frontend extends EventEmitter {
555
661
  res.status(500).send('Error reading history file.');
556
662
  }
557
663
  });
664
+ // Endpoint to view the shelly log
558
665
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
559
666
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
560
667
  try {
@@ -568,6 +675,7 @@ export class Frontend extends EventEmitter {
568
675
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
569
676
  }
570
677
  });
678
+ // Endpoint to download the matterbridge log
571
679
  this.expressApp.get('/api/download-mblog', async (req, res) => {
572
680
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
573
681
  const fs = await import('node:fs');
@@ -582,12 +690,14 @@ export class Frontend extends EventEmitter {
582
690
  }
583
691
  res.type('text/plain');
584
692
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
693
+ /* istanbul ignore if */
585
694
  if (error) {
586
695
  this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
587
696
  res.status(500).send('Error downloading the matterbridge log file');
588
697
  }
589
698
  });
590
699
  });
700
+ // Endpoint to download the matter log
591
701
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
592
702
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
593
703
  const fs = await import('node:fs');
@@ -602,12 +712,14 @@ export class Frontend extends EventEmitter {
602
712
  }
603
713
  res.type('text/plain');
604
714
  res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
715
+ /* istanbul ignore if */
605
716
  if (error) {
606
717
  this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
607
718
  res.status(500).send('Error downloading the matter log file');
608
719
  }
609
720
  });
610
721
  });
722
+ // Endpoint to download the shelly log
611
723
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
612
724
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
613
725
  const fs = await import('node:fs');
@@ -622,75 +734,91 @@ export class Frontend extends EventEmitter {
622
734
  }
623
735
  res.type('text/plain');
624
736
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
737
+ /* istanbul ignore if */
625
738
  if (error) {
626
739
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
627
740
  res.status(500).send('Error downloading Shelly system log file');
628
741
  }
629
742
  });
630
743
  });
744
+ // Endpoint to download the matterbridge storage directory
631
745
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
632
746
  this.log.debug('The frontend sent /api/download-mbstorage');
633
747
  await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
634
748
  res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
749
+ /* istanbul ignore if */
635
750
  if (error) {
636
751
  this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
637
752
  res.status(500).send('Error downloading the matterbridge storage file');
638
753
  }
639
754
  });
640
755
  });
756
+ // Endpoint to download the matter storage file
641
757
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
642
758
  this.log.debug('The frontend sent /api/download-mjstorage');
643
759
  await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
644
760
  res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
761
+ /* istanbul ignore if */
645
762
  if (error) {
646
763
  this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
647
764
  res.status(500).send('Error downloading the matter storage zip file');
648
765
  }
649
766
  });
650
767
  });
768
+ // Endpoint to download the matterbridge plugin directory
651
769
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
652
770
  this.log.debug('The frontend sent /api/download-pluginstorage');
653
771
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
654
772
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
773
+ /* istanbul ignore if */
655
774
  if (error) {
656
775
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
657
776
  res.status(500).send('Error downloading the matterbridge plugin storage file');
658
777
  }
659
778
  });
660
779
  });
780
+ // Endpoint to download the matterbridge plugin config files
661
781
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
662
782
  this.log.debug('The frontend sent /api/download-pluginconfig');
663
783
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
664
784
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
785
+ /* istanbul ignore if */
665
786
  if (error) {
666
787
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
667
788
  res.status(500).send('Error downloading the matterbridge plugin config file');
668
789
  }
669
790
  });
670
791
  });
792
+ // Endpoint to download the matterbridge backup (created with the backup command)
671
793
  this.expressApp.get('/api/download-backup', async (req, res) => {
672
794
  this.log.debug('The frontend sent /api/download-backup');
673
795
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
796
+ /* istanbul ignore if */
674
797
  if (error) {
675
798
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
676
799
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
677
800
  }
678
801
  });
679
802
  });
803
+ // Endpoint to upload a package
680
804
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
681
805
  const { filename } = req.body;
682
806
  const file = req.file;
807
+ /* istanbul ignore if */
683
808
  if (!file || !filename) {
684
809
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
685
810
  res.status(400).send('Invalid request: file and filename are required');
686
811
  return;
687
812
  }
688
813
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
814
+ // Define the path where the plugin file will be saved
689
815
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
690
816
  try {
817
+ // Move the uploaded file to the specified path
691
818
  const fs = await import('node:fs');
692
819
  await fs.promises.rename(file.path, filePath);
693
820
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
821
+ // Install the plugin package
694
822
  if (filename.endsWith('.tgz')) {
695
823
  const { spawnCommand } = await import('./utils/spawn.js');
696
824
  if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
@@ -718,6 +846,7 @@ export class Frontend extends EventEmitter {
718
846
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
719
847
  }
720
848
  });
849
+ // Fallback for routing (must be the last route)
721
850
  this.expressApp.use((req, res) => {
722
851
  const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
723
852
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
@@ -728,13 +857,16 @@ export class Frontend extends EventEmitter {
728
857
  async stop() {
729
858
  this.log.debug('Stopping the frontend...');
730
859
  const ws = await import('ws');
860
+ // Remove listeners from the express app
731
861
  if (this.expressApp) {
732
862
  this.expressApp.removeAllListeners();
733
863
  this.expressApp = undefined;
734
864
  this.log.debug('Frontend app closed successfully');
735
865
  }
866
+ // Close the WebSocket server
736
867
  if (this.webSocketServer) {
737
868
  this.log.debug('Closing WebSocket server...');
869
+ // Close all active connections
738
870
  this.webSocketServer.clients.forEach((client) => {
739
871
  if (client.readyState === ws.WebSocket.OPEN) {
740
872
  client.close();
@@ -743,6 +875,7 @@ export class Frontend extends EventEmitter {
743
875
  await withTimeout(new Promise((resolve) => {
744
876
  this.webSocketServer?.close((error) => {
745
877
  if (error) {
878
+ // istanbul ignore next
746
879
  this.log.error(`Error closing WebSocket server: ${error}`);
747
880
  }
748
881
  else {
@@ -755,8 +888,27 @@ export class Frontend extends EventEmitter {
755
888
  this.webSocketServer.removeAllListeners();
756
889
  this.webSocketServer = undefined;
757
890
  }
891
+ // Close the http server
758
892
  if (this.httpServer) {
759
893
  this.log.debug('Closing http server...');
894
+ /*
895
+ await withTimeout(
896
+ new Promise<void>((resolve) => {
897
+ this.httpServer?.close((error) => {
898
+ if (error) {
899
+ // istanbul ignore next
900
+ this.log.error(`Error closing http server: ${error}`);
901
+ } else {
902
+ this.log.debug('Http server closed successfully');
903
+ this.emit('server_stopped');
904
+ }
905
+ resolve();
906
+ });
907
+ }),
908
+ 5000,
909
+ false,
910
+ );
911
+ */
760
912
  this.httpServer.close();
761
913
  this.log.debug('Http server closed successfully');
762
914
  this.listening = false;
@@ -765,8 +917,27 @@ export class Frontend extends EventEmitter {
765
917
  this.httpServer = undefined;
766
918
  this.log.debug('Frontend http server closed successfully');
767
919
  }
920
+ // Close the https server
768
921
  if (this.httpsServer) {
769
922
  this.log.debug('Closing https server...');
923
+ /*
924
+ await withTimeout(
925
+ new Promise<void>((resolve) => {
926
+ this.httpsServer?.close((error) => {
927
+ if (error) {
928
+ // istanbul ignore next
929
+ this.log.error(`Error closing https server: ${error}`);
930
+ } else {
931
+ this.log.debug('Https server closed successfully');
932
+ this.emit('server_stopped');
933
+ }
934
+ resolve();
935
+ });
936
+ }),
937
+ 5000,
938
+ false,
939
+ );
940
+ */
770
941
  this.httpsServer.close();
771
942
  this.log.debug('Https server closed successfully');
772
943
  this.listening = false;
@@ -777,7 +948,13 @@ export class Frontend extends EventEmitter {
777
948
  }
778
949
  this.log.debug('Frontend stopped successfully');
779
950
  }
951
+ /**
952
+ * Retrieves the api settings data.
953
+ *
954
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
955
+ */
780
956
  async getApiSettings() {
957
+ // Update the variable system information properties
781
958
  this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
782
959
  this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
783
960
  this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
@@ -787,6 +964,7 @@ export class Frontend extends EventEmitter {
787
964
  this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
788
965
  this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
789
966
  this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
967
+ // Create the matterbridge information
790
968
  const info = {
791
969
  homeDirectory: this.matterbridge.homeDirectory,
792
970
  rootDirectory: this.matterbridge.rootDirectory,
@@ -822,9 +1000,15 @@ export class Frontend extends EventEmitter {
822
1000
  };
823
1001
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
824
1002
  }
1003
+ /**
1004
+ * Retrieves the reachable attribute.
1005
+ *
1006
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
1007
+ * @returns {boolean} The reachable attribute.
1008
+ */
825
1009
  getReachability(device) {
826
1010
  if (this.matterbridge.hasCleanupStarted)
827
- return false;
1011
+ return false; // Skip if cleanup has started
828
1012
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
829
1013
  return false;
830
1014
  if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
@@ -835,9 +1019,15 @@ export class Frontend extends EventEmitter {
835
1019
  return true;
836
1020
  return false;
837
1021
  }
1022
+ /**
1023
+ * Retrieves the power source attribute.
1024
+ *
1025
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1026
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1027
+ */
838
1028
  getPowerSource(endpoint) {
839
1029
  if (this.matterbridge.hasCleanupStarted)
840
- return;
1030
+ return; // Skip if cleanup has started
841
1031
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
842
1032
  return undefined;
843
1033
  const powerSource = (device) => {
@@ -852,16 +1042,25 @@ export class Frontend extends EventEmitter {
852
1042
  }
853
1043
  return;
854
1044
  };
1045
+ // Root endpoint
855
1046
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
856
1047
  return powerSource(endpoint);
1048
+ // Child endpoints
857
1049
  for (const child of endpoint.getChildEndpoints()) {
858
1050
  if (child.hasClusterServer(PowerSource.Cluster.id))
859
1051
  return powerSource(child);
860
1052
  }
861
1053
  }
1054
+ /**
1055
+ * Retrieves the cluster text description from a given device.
1056
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1057
+ *
1058
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1059
+ * @returns {string} The attributes description of the cluster servers in the device.
1060
+ */
862
1061
  getClusterTextFromDevice(device) {
863
1062
  if (this.matterbridge.hasCleanupStarted)
864
- return '';
1063
+ return ''; // Skip if cleanup has started
865
1064
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
866
1065
  return '';
867
1066
  const getUserLabel = (device) => {
@@ -871,6 +1070,7 @@ export class Frontend extends EventEmitter {
871
1070
  if (composed)
872
1071
  return 'Composed: ' + composed.value;
873
1072
  }
1073
+ // istanbul ignore next cause is not reachable
874
1074
  return '';
875
1075
  };
876
1076
  const getFixedLabel = (device) => {
@@ -880,11 +1080,13 @@ export class Frontend extends EventEmitter {
880
1080
  if (composed)
881
1081
  return 'Composed: ' + composed.value;
882
1082
  }
1083
+ // istanbul ignore next cause is not reacheable
883
1084
  return '';
884
1085
  };
885
1086
  let attributes = '';
886
1087
  let supportedModes = [];
887
1088
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1089
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
888
1090
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
889
1091
  return;
890
1092
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -974,11 +1176,17 @@ export class Frontend extends EventEmitter {
974
1176
  if (clusterName === 'userLabel' && attributeName === 'labelList')
975
1177
  attributes += `${getUserLabel(device)} `;
976
1178
  });
1179
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
977
1180
  return attributes.trimStart().trimEnd();
978
1181
  }
1182
+ /**
1183
+ * Retrieves the registered plugins sanitized for res.json().
1184
+ *
1185
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1186
+ */
979
1187
  getPlugins() {
980
1188
  if (this.matterbridge.hasCleanupStarted)
981
- return [];
1189
+ return []; // Skip if cleanup has started
982
1190
  const plugins = [];
983
1191
  for (const plugin of this.matterbridge.plugins.array()) {
984
1192
  plugins.push({
@@ -1006,18 +1214,27 @@ export class Frontend extends EventEmitter {
1006
1214
  schemaJson: plugin.schemaJson,
1007
1215
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
1008
1216
  hasBlackList: plugin.configJson?.blackList !== undefined,
1217
+ // Childbridge mode specific data
1009
1218
  matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
1010
1219
  });
1011
1220
  }
1012
1221
  return plugins;
1013
1222
  }
1223
+ /**
1224
+ * Retrieves the devices from Matterbridge.
1225
+ *
1226
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1227
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1228
+ */
1014
1229
  getDevices(pluginName) {
1015
1230
  if (this.matterbridge.hasCleanupStarted)
1016
- return [];
1231
+ return []; // Skip if cleanup has started
1017
1232
  const devices = [];
1018
1233
  for (const device of this.matterbridge.devices.array()) {
1234
+ // Filter by pluginName if provided
1019
1235
  if (pluginName && pluginName !== device.plugin)
1020
1236
  continue;
1237
+ // Check if the device has the required properties
1021
1238
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1022
1239
  continue;
1023
1240
  devices.push({
@@ -1037,24 +1254,39 @@ export class Frontend extends EventEmitter {
1037
1254
  }
1038
1255
  return devices;
1039
1256
  }
1257
+ /**
1258
+ * Retrieves the clusters from a given plugin and endpoint number.
1259
+ *
1260
+ * Response for /api/clusters
1261
+ *
1262
+ * @param {string} pluginName - The name of the plugin.
1263
+ * @param {number} endpointNumber - The endpoint number.
1264
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1265
+ */
1040
1266
  getClusters(pluginName, endpointNumber) {
1041
1267
  if (this.matterbridge.hasCleanupStarted)
1042
- return;
1268
+ return; // Skip if cleanup has started
1043
1269
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1044
1270
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1045
1271
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1046
1272
  return;
1047
1273
  }
1274
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1275
+ // Get the device types from the main endpoint
1048
1276
  const deviceTypes = [];
1049
1277
  const clusters = [];
1050
1278
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1051
1279
  deviceTypes.push(d.deviceType);
1052
1280
  });
1281
+ // Get the clusters from the main endpoint
1053
1282
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1054
1283
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1055
1284
  return;
1056
1285
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1057
1286
  return;
1287
+ // console.log(
1288
+ // `${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}`,
1289
+ // );
1058
1290
  clusters.push({
1059
1291
  endpoint: endpoint.number.toString(),
1060
1292
  number: endpoint.number,
@@ -1068,12 +1300,19 @@ export class Frontend extends EventEmitter {
1068
1300
  attributeLocalValue: attributeValue,
1069
1301
  });
1070
1302
  });
1303
+ // Get the child endpoints
1071
1304
  const childEndpoints = endpoint.getChildEndpoints();
1305
+ // if (childEndpoints.length === 0) {
1306
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1307
+ // }
1072
1308
  childEndpoints.forEach((childEndpoint) => {
1309
+ // istanbul ignore if cause is not reachable: should never happen but ...
1073
1310
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1074
1311
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1075
1312
  return;
1076
1313
  }
1314
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1315
+ // Get the device types of the child endpoint
1077
1316
  const deviceTypes = [];
1078
1317
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1079
1318
  deviceTypes.push(d.deviceType);
@@ -1083,6 +1322,9 @@ export class Frontend extends EventEmitter {
1083
1322
  return;
1084
1323
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1085
1324
  return;
1325
+ // console.log(
1326
+ // `${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}`,
1327
+ // );
1086
1328
  clusters.push({
1087
1329
  endpoint: childEndpoint.number.toString(),
1088
1330
  number: childEndpoint.number,
@@ -1102,6 +1344,7 @@ export class Frontend extends EventEmitter {
1102
1344
  async generateDiagnostic() {
1103
1345
  this.log.debug('Generating diagnostic...');
1104
1346
  const serverNodes = [];
1347
+ // istanbul ignore else
1105
1348
  if (this.matterbridge.bridgeMode === 'bridge') {
1106
1349
  if (this.matterbridge.serverNode)
1107
1350
  serverNodes.push(this.matterbridge.serverNode);
@@ -1112,6 +1355,7 @@ export class Frontend extends EventEmitter {
1112
1355
  serverNodes.push(plugin.serverNode);
1113
1356
  }
1114
1357
  }
1358
+ // istanbul ignore next
1115
1359
  for (const device of this.matterbridge.devices.array()) {
1116
1360
  if (device.serverNode)
1117
1361
  serverNodes.push(device.serverNode);
@@ -1135,8 +1379,15 @@ export class Frontend extends EventEmitter {
1135
1379
  values: [...serverNodes],
1136
1380
  })));
1137
1381
  delete Logger.destinations.diagnostic;
1138
- await wait(500);
1382
+ await wait(500); // Wait for the log to be written
1139
1383
  }
1384
+ /**
1385
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1386
+ *
1387
+ * @param {WebSocket} client - The websocket client that sent the message.
1388
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1389
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1390
+ */
1140
1391
  async wsMessageHandler(client, message) {
1141
1392
  let data;
1142
1393
  const sendResponse = (data) => {
@@ -1154,12 +1405,13 @@ export class Frontend extends EventEmitter {
1154
1405
  client.send(JSON.stringify(data));
1155
1406
  }
1156
1407
  else {
1408
+ // istanbul ignore next cause is only a safety check
1157
1409
  this.log.error('Cannot send api response, client not connected');
1158
1410
  }
1159
1411
  };
1160
1412
  try {
1161
1413
  data = JSON.parse(message.toString());
1162
- if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
1414
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
1163
1415
  this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1164
1416
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1165
1417
  return;
@@ -1216,7 +1468,22 @@ export class Frontend extends EventEmitter {
1216
1468
  }
1217
1469
  this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
1218
1470
  this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
1219
- data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
1471
+ data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
1472
+ /*
1473
+ const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
1474
+ if (plugin) {
1475
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1476
+ await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
1477
+ this.wssSendRestartRequired();
1478
+ this.wssSendRefreshRequired('plugins');
1479
+ this.wssSendRefreshRequired('devices');
1480
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1481
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1482
+ } else {
1483
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1484
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1485
+ }
1486
+ */
1220
1487
  const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
1221
1488
  if (plugin) {
1222
1489
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1230,6 +1497,7 @@ export class Frontend extends EventEmitter {
1230
1497
  return;
1231
1498
  })
1232
1499
  .catch((_error) => {
1500
+ //
1233
1501
  });
1234
1502
  }
1235
1503
  else {
@@ -1244,6 +1512,10 @@ export class Frontend extends EventEmitter {
1244
1512
  }
1245
1513
  this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
1246
1514
  this.log.debug(`Removing plugin ${data.params.pluginName}...`);
1515
+ /*
1516
+ 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);
1517
+ await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
1518
+ */
1247
1519
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1248
1520
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1249
1521
  await this.matterbridge.plugins.remove(data.params.pluginName);
@@ -1279,6 +1551,7 @@ export class Frontend extends EventEmitter {
1279
1551
  return;
1280
1552
  })
1281
1553
  .catch((_error) => {
1554
+ //
1282
1555
  });
1283
1556
  }
1284
1557
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1304,6 +1577,7 @@ export class Frontend extends EventEmitter {
1304
1577
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1305
1578
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1306
1579
  if (plugin.serverNode) {
1580
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1307
1581
  await this.matterbridge.stopServerNode(plugin.serverNode);
1308
1582
  plugin.serverNode = undefined;
1309
1583
  }
@@ -1313,18 +1587,20 @@ export class Frontend extends EventEmitter {
1313
1587
  this.matterbridge.devices.remove(device);
1314
1588
  }
1315
1589
  }
1590
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1316
1591
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1317
1592
  await this.matterbridge.createDynamicPlugin(plugin);
1318
1593
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1319
- plugin.restartRequired = false;
1594
+ plugin.restartRequired = false; // Reset plugin restartRequired
1320
1595
  let needRestart = 0;
1321
1596
  for (const plugin of this.matterbridge.plugins) {
1322
1597
  if (plugin.restartRequired)
1323
1598
  needRestart++;
1324
1599
  }
1325
1600
  if (needRestart === 0) {
1326
- this.wssSendRestartNotRequired(true);
1601
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1327
1602
  }
1603
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1328
1604
  if (plugin.serverNode)
1329
1605
  await this.matterbridge.startServerNode(plugin.serverNode);
1330
1606
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1469,6 +1745,9 @@ export class Frontend extends EventEmitter {
1469
1745
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1470
1746
  }
1471
1747
  if (data.params.advertise) {
1748
+ // TODO: matter.js 0.16.0
1749
+ // await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
1750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1472
1751
  const advertiser = serverNode.env.get(DeviceAdvertiser);
1473
1752
  if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
1474
1753
  await advertiser.advertise(true);
@@ -1594,22 +1873,22 @@ export class Frontend extends EventEmitter {
1594
1873
  if (isValidString(data.params.value, 4)) {
1595
1874
  this.log.debug('Matterbridge logger level:', data.params.value);
1596
1875
  if (data.params.value === 'Debug') {
1597
- await this.matterbridge.setLogLevel("debug");
1876
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1598
1877
  }
1599
1878
  else if (data.params.value === 'Info') {
1600
- await this.matterbridge.setLogLevel("info");
1879
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1601
1880
  }
1602
1881
  else if (data.params.value === 'Notice') {
1603
- await this.matterbridge.setLogLevel("notice");
1882
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1604
1883
  }
1605
1884
  else if (data.params.value === 'Warn') {
1606
- await this.matterbridge.setLogLevel("warn");
1885
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1607
1886
  }
1608
1887
  else if (data.params.value === 'Error') {
1609
- await this.matterbridge.setLogLevel("error");
1888
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1610
1889
  }
1611
1890
  else if (data.params.value === 'Fatal') {
1612
- await this.matterbridge.setLogLevel("fatal");
1891
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1613
1892
  }
1614
1893
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1615
1894
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1620,6 +1899,7 @@ export class Frontend extends EventEmitter {
1620
1899
  this.log.debug('Matterbridge file log:', data.params.value);
1621
1900
  this.matterbridge.fileLogger = data.params.value;
1622
1901
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1902
+ // Create the file logger for matterbridge
1623
1903
  if (data.params.value)
1624
1904
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1625
1905
  else
@@ -1649,11 +1929,12 @@ export class Frontend extends EventEmitter {
1649
1929
  Logger.level = MatterLogLevel.FATAL;
1650
1930
  }
1651
1931
  this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
1652
- let callbackLogLevel = "notice";
1653
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1654
- callbackLogLevel = "info";
1655
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1656
- callbackLogLevel = "debug";
1932
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
1933
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
1934
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
1935
+ callbackLogLevel = "info" /* LogLevel.INFO */;
1936
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
1937
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1657
1938
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1658
1939
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1659
1940
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1705,6 +1986,7 @@ export class Frontend extends EventEmitter {
1705
1986
  }
1706
1987
  break;
1707
1988
  case 'setmatterport':
1989
+ // eslint-disable-next-line no-case-declarations
1708
1990
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1709
1991
  if (isValidNumber(port, 5540, 5600)) {
1710
1992
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1724,6 +2006,7 @@ export class Frontend extends EventEmitter {
1724
2006
  }
1725
2007
  break;
1726
2008
  case 'setmatterdiscriminator':
2009
+ // eslint-disable-next-line no-case-declarations
1727
2010
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1728
2011
  if (isValidNumber(discriminator, 0, 4095)) {
1729
2012
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1743,6 +2026,7 @@ export class Frontend extends EventEmitter {
1743
2026
  }
1744
2027
  break;
1745
2028
  case 'setmatterpasscode':
2029
+ // eslint-disable-next-line no-case-declarations
1746
2030
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1747
2031
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1748
2032
  this.matterbridge.passcode = passcode;
@@ -1788,15 +2072,19 @@ export class Frontend extends EventEmitter {
1788
2072
  return;
1789
2073
  }
1790
2074
  const config = plugin.configJson;
2075
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1791
2076
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2077
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1792
2078
  if (select === 'serial')
1793
2079
  this.log.info(`Selected device serial ${data.params.serial}`);
1794
2080
  if (select === 'name')
1795
2081
  this.log.info(`Selected device name ${data.params.name}`);
1796
2082
  if (config && select && (select === 'serial' || select === 'name')) {
2083
+ // Remove postfix from the serial if it exists
1797
2084
  if (config.postfix) {
1798
2085
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1799
2086
  }
2087
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1800
2088
  if (isValidArray(config.whiteList, 1)) {
1801
2089
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1802
2090
  config.whiteList.push(data.params.serial);
@@ -1805,6 +2093,7 @@ export class Frontend extends EventEmitter {
1805
2093
  config.whiteList.push(data.params.name);
1806
2094
  }
1807
2095
  }
2096
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1808
2097
  if (isValidArray(config.blackList, 1)) {
1809
2098
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1810
2099
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1832,7 +2121,9 @@ export class Frontend extends EventEmitter {
1832
2121
  return;
1833
2122
  }
1834
2123
  const config = plugin.configJson;
2124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1835
2125
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2126
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1836
2127
  if (select === 'serial')
1837
2128
  this.log.info(`Unselected device serial ${data.params.serial}`);
1838
2129
  if (select === 'name')
@@ -1841,6 +2132,7 @@ export class Frontend extends EventEmitter {
1841
2132
  if (config.postfix) {
1842
2133
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1843
2134
  }
2135
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1844
2136
  if (isValidArray(config.whiteList, 1)) {
1845
2137
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1846
2138
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1849,6 +2141,7 @@ export class Frontend extends EventEmitter {
1849
2141
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1850
2142
  }
1851
2143
  }
2144
+ // Add the serial to the blackList
1852
2145
  if (isValidArray(config.blackList)) {
1853
2146
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1854
2147
  config.blackList.push(data.params.serial);
@@ -1871,6 +2164,7 @@ export class Frontend extends EventEmitter {
1871
2164
  }
1872
2165
  }
1873
2166
  else {
2167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1874
2168
  const localData = data;
1875
2169
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1876
2170
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1880,23 +2174,46 @@ export class Frontend extends EventEmitter {
1880
2174
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1881
2175
  }
1882
2176
  }
2177
+ /**
2178
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2179
+ *
2180
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2181
+ * @param {string} time - The time string of the message
2182
+ * @param {string} name - The logger name of the message
2183
+ * @param {string} message - The content of the message.
2184
+ *
2185
+ * @remarks
2186
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2187
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2188
+ * The function sends the message to all connected clients.
2189
+ */
1883
2190
  wssSendLogMessage(level, time, name, message) {
1884
2191
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1885
2192
  return;
1886
2193
  if (!level || !time || !name || !message)
1887
2194
  return;
2195
+ // Remove ANSI escape codes from the message
2196
+ // eslint-disable-next-line no-control-regex
1888
2197
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2198
+ // Remove leading asterisks from the message
1889
2199
  message = message.replace(/^\*+/, '');
2200
+ // Replace all occurrences of \t and \n
1890
2201
  message = message.replace(/[\t\n]/g, '');
2202
+ // Remove non-printable characters
2203
+ // eslint-disable-next-line no-control-regex
1891
2204
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2205
+ // Replace all occurrences of \" with "
1892
2206
  message = message.replace(/\\"/g, '"');
2207
+ // Define the maximum allowed length for continuous characters without a space
1893
2208
  const maxContinuousLength = 100;
1894
2209
  const keepStartLength = 20;
1895
2210
  const keepEndLength = 20;
2211
+ // Split the message into words
1896
2212
  if (level !== 'spawn') {
1897
2213
  message = message
1898
2214
  .split(' ')
1899
2215
  .map((word) => {
2216
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1900
2217
  if (word.length > maxContinuousLength) {
1901
2218
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1902
2219
  }
@@ -1904,14 +2221,34 @@ export class Frontend extends EventEmitter {
1904
2221
  })
1905
2222
  .join(' ');
1906
2223
  }
2224
+ // Send the message to all connected clients
1907
2225
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1908
2226
  }
2227
+ /**
2228
+ * Sends a need to refresh WebSocket message to all connected clients.
2229
+ *
2230
+ * @param {string} changed - The changed value.
2231
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2232
+ * possible values for changed:
2233
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2234
+ * - 'plugins'
2235
+ * - 'devices'
2236
+ * - 'matter' with param 'matter' (QRDiv component)
2237
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2238
+ */
1909
2239
  wssSendRefreshRequired(changed, params) {
1910
2240
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1911
2241
  return;
1912
2242
  this.log.debug('Sending a refresh required message to all connected clients');
2243
+ // Send the message to all connected clients
1913
2244
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1914
2245
  }
2246
+ /**
2247
+ * Sends a need to restart WebSocket message to all connected clients.
2248
+ *
2249
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2250
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2251
+ */
1915
2252
  wssSendRestartRequired(snackbar = true, fixed = false) {
1916
2253
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1917
2254
  return;
@@ -1920,8 +2257,14 @@ export class Frontend extends EventEmitter {
1920
2257
  this.matterbridge.fixedRestartRequired = fixed;
1921
2258
  if (snackbar === true)
1922
2259
  this.wssSendSnackbarMessage(`Restart required`, 0);
2260
+ // Send the message to all connected clients
1923
2261
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1924
2262
  }
2263
+ /**
2264
+ * Sends a no need to restart WebSocket message to all connected clients.
2265
+ *
2266
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2267
+ */
1925
2268
  wssSendRestartNotRequired(snackbar = true) {
1926
2269
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1927
2270
  return;
@@ -1929,57 +2272,133 @@ export class Frontend extends EventEmitter {
1929
2272
  this.matterbridge.restartRequired = false;
1930
2273
  if (snackbar === true)
1931
2274
  this.wssSendCloseSnackbarMessage(`Restart required`);
2275
+ // Send the message to all connected clients
1932
2276
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1933
2277
  }
2278
+ /**
2279
+ * Sends a need to update WebSocket message to all connected clients.
2280
+ *
2281
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2282
+ */
1934
2283
  wssSendUpdateRequired(devVersion = false) {
1935
2284
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1936
2285
  return;
1937
2286
  this.log.debug('Sending an update required message to all connected clients');
1938
2287
  this.matterbridge.updateRequired = true;
2288
+ // Send the message to all connected clients
1939
2289
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1940
2290
  }
2291
+ /**
2292
+ * Sends a cpu update message to all connected clients.
2293
+ *
2294
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2295
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2296
+ */
1941
2297
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1942
2298
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1943
2299
  return;
1944
2300
  if (hasParameter('debug'))
1945
2301
  this.log.debug('Sending a cpu update message to all connected clients');
2302
+ // Send the message to all connected clients
1946
2303
  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 } });
1947
2304
  }
2305
+ /**
2306
+ * Sends a memory update message to all connected clients.
2307
+ *
2308
+ * @param {string} totalMemory - The total memory in bytes.
2309
+ * @param {string} freeMemory - The free memory in bytes.
2310
+ * @param {string} rss - The resident set size in bytes.
2311
+ * @param {string} heapTotal - The total heap memory in bytes.
2312
+ * @param {string} heapUsed - The used heap memory in bytes.
2313
+ * @param {string} external - The external memory in bytes.
2314
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2315
+ */
1948
2316
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1949
2317
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1950
2318
  return;
1951
2319
  if (hasParameter('debug'))
1952
2320
  this.log.debug('Sending a memory update message to all connected clients');
2321
+ // Send the message to all connected clients
1953
2322
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1954
2323
  }
2324
+ /**
2325
+ * Sends an uptime update message to all connected clients.
2326
+ *
2327
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2328
+ * @param {string} processUptime - The process uptime in a human-readable format.
2329
+ */
1955
2330
  wssSendUptimeUpdate(systemUptime, processUptime) {
1956
2331
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1957
2332
  return;
1958
2333
  if (hasParameter('debug'))
1959
2334
  this.log.debug('Sending a uptime update message to all connected clients');
2335
+ // Send the message to all connected clients
1960
2336
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1961
2337
  }
2338
+ /**
2339
+ * Sends an open snackbar message to all connected clients.
2340
+ *
2341
+ * @param {string} message - The message to send.
2342
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2343
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2344
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2345
+ *
2346
+ * @remarks
2347
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2348
+ */
1962
2349
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1963
2350
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1964
2351
  return;
1965
2352
  this.log.debug('Sending a snackbar message to all connected clients');
2353
+ // Send the message to all connected clients
1966
2354
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
1967
2355
  }
2356
+ /**
2357
+ * Sends a close snackbar message to all connected clients.
2358
+ * It will close the snackbar message with the same message and timeout = 0.
2359
+ *
2360
+ * @param {string} message - The message to send.
2361
+ */
1968
2362
  wssSendCloseSnackbarMessage(message) {
1969
2363
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1970
2364
  return;
1971
2365
  this.log.debug('Sending a close snackbar message to all connected clients');
2366
+ // Send the message to all connected clients
1972
2367
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
1973
2368
  }
2369
+ /**
2370
+ * Sends an attribute update message to all connected WebSocket clients.
2371
+ *
2372
+ * @param {string | undefined} plugin - The name of the plugin.
2373
+ * @param {string | undefined} serialNumber - The serial number of the device.
2374
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2375
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2376
+ * @param {string} id - The endpoint id where the attribute belongs.
2377
+ * @param {string} cluster - The cluster name where the attribute belongs.
2378
+ * @param {string} attribute - The name of the attribute that changed.
2379
+ * @param {number | string | boolean} value - The new value of the attribute.
2380
+ *
2381
+ * @remarks
2382
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2383
+ * with the updated attribute information.
2384
+ */
1974
2385
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
1975
2386
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1976
2387
  return;
1977
2388
  this.log.debug('Sending an attribute update message to all connected clients');
2389
+ // Send the message to all connected clients
1978
2390
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
1979
2391
  }
2392
+ /**
2393
+ * Sends a message to all connected clients.
2394
+ * This is an helper function to send a broadcast message to all connected clients.
2395
+ *
2396
+ * @param {WsMessageBroadcast} msg - The message to send.
2397
+ */
1980
2398
  wssBroadcastMessage(msg) {
1981
2399
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1982
2400
  return;
2401
+ // Send the message to all connected clients
1983
2402
  const stringifiedMsg = JSON.stringify(msg);
1984
2403
  if (msg.method !== 'log')
1985
2404
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
@@ -1990,3 +2409,4 @@ export class Frontend extends EventEmitter {
1990
2409
  });
1991
2410
  }
1992
2411
  }
2412
+ //# sourceMappingURL=frontend.js.map