matterbridge 3.4.3-dev-20251209-e6cb85f → 3.4.3

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