matterbridge 3.4.4-dev-20251217-8b8b1cd → 3.4.4

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 (323) hide show
  1. package/dist/broadcastServer.d.ts +144 -0
  2. package/dist/broadcastServer.d.ts.map +1 -0
  3. package/dist/broadcastServer.js +117 -0
  4. package/dist/broadcastServer.js.map +1 -0
  5. package/dist/broadcastServerTypes.d.ts +841 -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/deviceManager.d.ts +135 -0
  26. package/dist/deviceManager.d.ts.map +1 -0
  27. package/dist/deviceManager.js +113 -1
  28. package/dist/deviceManager.js.map +1 -0
  29. package/dist/devices/airConditioner.d.ts +98 -0
  30. package/dist/devices/airConditioner.d.ts.map +1 -0
  31. package/dist/devices/airConditioner.js +57 -0
  32. package/dist/devices/airConditioner.js.map +1 -0
  33. package/dist/devices/batteryStorage.d.ts +48 -0
  34. package/dist/devices/batteryStorage.d.ts.map +1 -0
  35. package/dist/devices/batteryStorage.js +48 -1
  36. package/dist/devices/batteryStorage.js.map +1 -0
  37. package/dist/devices/cooktop.d.ts +61 -0
  38. package/dist/devices/cooktop.d.ts.map +1 -0
  39. package/dist/devices/cooktop.js +56 -0
  40. package/dist/devices/cooktop.js.map +1 -0
  41. package/dist/devices/dishwasher.d.ts +71 -0
  42. package/dist/devices/dishwasher.d.ts.map +1 -0
  43. package/dist/devices/dishwasher.js +57 -0
  44. package/dist/devices/dishwasher.js.map +1 -0
  45. package/dist/devices/evse.d.ts +76 -0
  46. package/dist/devices/evse.d.ts.map +1 -0
  47. package/dist/devices/evse.js +74 -10
  48. package/dist/devices/evse.js.map +1 -0
  49. package/dist/devices/export.d.ts +17 -0
  50. package/dist/devices/export.d.ts.map +1 -0
  51. package/dist/devices/export.js +5 -0
  52. package/dist/devices/export.js.map +1 -0
  53. package/dist/devices/extractorHood.d.ts +46 -0
  54. package/dist/devices/extractorHood.d.ts.map +1 -0
  55. package/dist/devices/extractorHood.js +43 -0
  56. package/dist/devices/extractorHood.js.map +1 -0
  57. package/dist/devices/heatPump.d.ts +47 -0
  58. package/dist/devices/heatPump.d.ts.map +1 -0
  59. package/dist/devices/heatPump.js +50 -2
  60. package/dist/devices/heatPump.js.map +1 -0
  61. package/dist/devices/laundryDryer.d.ts +67 -0
  62. package/dist/devices/laundryDryer.d.ts.map +1 -0
  63. package/dist/devices/laundryDryer.js +62 -3
  64. package/dist/devices/laundryDryer.js.map +1 -0
  65. package/dist/devices/laundryWasher.d.ts +81 -0
  66. package/dist/devices/laundryWasher.d.ts.map +1 -0
  67. package/dist/devices/laundryWasher.js +70 -4
  68. package/dist/devices/laundryWasher.js.map +1 -0
  69. package/dist/devices/microwaveOven.d.ts +168 -0
  70. package/dist/devices/microwaveOven.d.ts.map +1 -0
  71. package/dist/devices/microwaveOven.js +88 -5
  72. package/dist/devices/microwaveOven.js.map +1 -0
  73. package/dist/devices/oven.d.ts +105 -0
  74. package/dist/devices/oven.d.ts.map +1 -0
  75. package/dist/devices/oven.js +85 -0
  76. package/dist/devices/oven.js.map +1 -0
  77. package/dist/devices/refrigerator.d.ts +118 -0
  78. package/dist/devices/refrigerator.d.ts.map +1 -0
  79. package/dist/devices/refrigerator.js +102 -0
  80. package/dist/devices/refrigerator.js.map +1 -0
  81. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  82. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  83. package/dist/devices/roboticVacuumCleaner.js +100 -9
  84. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  85. package/dist/devices/solarPower.d.ts +40 -0
  86. package/dist/devices/solarPower.d.ts.map +1 -0
  87. package/dist/devices/solarPower.js +38 -0
  88. package/dist/devices/solarPower.js.map +1 -0
  89. package/dist/devices/speaker.d.ts +87 -0
  90. package/dist/devices/speaker.d.ts.map +1 -0
  91. package/dist/devices/speaker.js +84 -0
  92. package/dist/devices/speaker.js.map +1 -0
  93. package/dist/devices/temperatureControl.d.ts +166 -0
  94. package/dist/devices/temperatureControl.d.ts.map +1 -0
  95. package/dist/devices/temperatureControl.js +24 -3
  96. package/dist/devices/temperatureControl.js.map +1 -0
  97. package/dist/devices/waterHeater.d.ts +111 -0
  98. package/dist/devices/waterHeater.d.ts.map +1 -0
  99. package/dist/devices/waterHeater.js +82 -2
  100. package/dist/devices/waterHeater.js.map +1 -0
  101. package/dist/dgram/coap.d.ts +205 -0
  102. package/dist/dgram/coap.d.ts.map +1 -0
  103. package/dist/dgram/coap.js +126 -13
  104. package/dist/dgram/coap.js.map +1 -0
  105. package/dist/dgram/dgram.d.ts +141 -0
  106. package/dist/dgram/dgram.d.ts.map +1 -0
  107. package/dist/dgram/dgram.js +114 -2
  108. package/dist/dgram/dgram.js.map +1 -0
  109. package/dist/dgram/mb_coap.d.ts +24 -0
  110. package/dist/dgram/mb_coap.d.ts.map +1 -0
  111. package/dist/dgram/mb_coap.js +41 -3
  112. package/dist/dgram/mb_coap.js.map +1 -0
  113. package/dist/dgram/mb_mdns.d.ts +24 -0
  114. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  115. package/dist/dgram/mb_mdns.js +80 -15
  116. package/dist/dgram/mb_mdns.js.map +1 -0
  117. package/dist/dgram/mdns.d.ts +290 -0
  118. package/dist/dgram/mdns.d.ts.map +1 -0
  119. package/dist/dgram/mdns.js +299 -137
  120. package/dist/dgram/mdns.js.map +1 -0
  121. package/dist/dgram/multicast.d.ts +67 -0
  122. package/dist/dgram/multicast.d.ts.map +1 -0
  123. package/dist/dgram/multicast.js +62 -1
  124. package/dist/dgram/multicast.js.map +1 -0
  125. package/dist/dgram/unicast.d.ts +56 -0
  126. package/dist/dgram/unicast.d.ts.map +1 -0
  127. package/dist/dgram/unicast.js +54 -0
  128. package/dist/dgram/unicast.js.map +1 -0
  129. package/dist/frontend.d.ts +245 -0
  130. package/dist/frontend.d.ts.map +1 -0
  131. package/dist/frontend.js +485 -38
  132. package/dist/frontend.js.map +1 -0
  133. package/dist/frontendTypes.d.ts +529 -0
  134. package/dist/frontendTypes.d.ts.map +1 -0
  135. package/dist/frontendTypes.js +45 -0
  136. package/dist/frontendTypes.js.map +1 -0
  137. package/dist/helpers.d.ts +48 -0
  138. package/dist/helpers.d.ts.map +1 -0
  139. package/dist/helpers.js +53 -0
  140. package/dist/helpers.js.map +1 -0
  141. package/dist/index.d.ts +34 -0
  142. package/dist/index.d.ts.map +1 -0
  143. package/dist/index.js +25 -0
  144. package/dist/index.js.map +1 -0
  145. package/dist/jestutils/export.d.ts +2 -0
  146. package/dist/jestutils/export.d.ts.map +1 -0
  147. package/dist/jestutils/export.js +1 -0
  148. package/dist/jestutils/export.js.map +1 -0
  149. package/dist/jestutils/jestHelpers.d.ts +345 -0
  150. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  151. package/dist/jestutils/jestHelpers.js +371 -14
  152. package/dist/jestutils/jestHelpers.js.map +1 -0
  153. package/dist/logger/export.d.ts +2 -0
  154. package/dist/logger/export.d.ts.map +1 -0
  155. package/dist/logger/export.js +1 -0
  156. package/dist/logger/export.js.map +1 -0
  157. package/dist/matter/behaviors.d.ts +2 -0
  158. package/dist/matter/behaviors.d.ts.map +1 -0
  159. package/dist/matter/behaviors.js +2 -0
  160. package/dist/matter/behaviors.js.map +1 -0
  161. package/dist/matter/clusters.d.ts +2 -0
  162. package/dist/matter/clusters.d.ts.map +1 -0
  163. package/dist/matter/clusters.js +2 -0
  164. package/dist/matter/clusters.js.map +1 -0
  165. package/dist/matter/devices.d.ts +2 -0
  166. package/dist/matter/devices.d.ts.map +1 -0
  167. package/dist/matter/devices.js +2 -0
  168. package/dist/matter/devices.js.map +1 -0
  169. package/dist/matter/endpoints.d.ts +2 -0
  170. package/dist/matter/endpoints.d.ts.map +1 -0
  171. package/dist/matter/endpoints.js +2 -0
  172. package/dist/matter/endpoints.js.map +1 -0
  173. package/dist/matter/export.d.ts +5 -0
  174. package/dist/matter/export.d.ts.map +1 -0
  175. package/dist/matter/export.js +3 -0
  176. package/dist/matter/export.js.map +1 -0
  177. package/dist/matter/types.d.ts +3 -0
  178. package/dist/matter/types.d.ts.map +1 -0
  179. package/dist/matter/types.js +3 -0
  180. package/dist/matter/types.js.map +1 -0
  181. package/dist/matterNode.d.ts +342 -0
  182. package/dist/matterNode.d.ts.map +1 -0
  183. package/dist/matterNode.js +369 -8
  184. package/dist/matterNode.js.map +1 -0
  185. package/dist/matterbridge.d.ts +505 -0
  186. package/dist/matterbridge.d.ts.map +1 -0
  187. package/dist/matterbridge.js +829 -46
  188. package/dist/matterbridge.js.map +1 -0
  189. package/dist/matterbridgeAccessoryPlatform.d.ts +41 -0
  190. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  191. package/dist/matterbridgeAccessoryPlatform.js +38 -0
  192. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  193. package/dist/matterbridgeBehaviors.d.ts +2404 -0
  194. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  195. package/dist/matterbridgeBehaviors.js +68 -5
  196. package/dist/matterbridgeBehaviors.js.map +1 -0
  197. package/dist/matterbridgeDeviceTypes.d.ts +698 -0
  198. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  199. package/dist/matterbridgeDeviceTypes.js +635 -14
  200. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  201. package/dist/matterbridgeDynamicPlatform.d.ts +41 -0
  202. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  203. package/dist/matterbridgeDynamicPlatform.js +38 -0
  204. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  205. package/dist/matterbridgeEndpoint.d.ts +1507 -0
  206. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  207. package/dist/matterbridgeEndpoint.js +1457 -53
  208. package/dist/matterbridgeEndpoint.js.map +1 -0
  209. package/dist/matterbridgeEndpointHelpers.d.ts +787 -0
  210. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  211. package/dist/matterbridgeEndpointHelpers.js +483 -20
  212. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  213. package/dist/matterbridgeEndpointTypes.d.ts +166 -0
  214. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  215. package/dist/matterbridgeEndpointTypes.js +25 -0
  216. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  217. package/dist/matterbridgePlatform.d.ts +539 -0
  218. package/dist/matterbridgePlatform.d.ts.map +1 -0
  219. package/dist/matterbridgePlatform.js +451 -1
  220. package/dist/matterbridgePlatform.js.map +1 -0
  221. package/dist/matterbridgeTypes.d.ts +252 -0
  222. package/dist/matterbridgeTypes.d.ts.map +1 -0
  223. package/dist/matterbridgeTypes.js +26 -0
  224. package/dist/matterbridgeTypes.js.map +1 -0
  225. package/dist/pluginManager.d.ts +372 -0
  226. package/dist/pluginManager.d.ts.map +1 -0
  227. package/dist/pluginManager.js +341 -5
  228. package/dist/pluginManager.js.map +1 -0
  229. package/dist/shelly.d.ts +181 -0
  230. package/dist/shelly.d.ts.map +1 -0
  231. package/dist/shelly.js +178 -7
  232. package/dist/shelly.js.map +1 -0
  233. package/dist/storage/export.d.ts +2 -0
  234. package/dist/storage/export.d.ts.map +1 -0
  235. package/dist/storage/export.js +1 -0
  236. package/dist/storage/export.js.map +1 -0
  237. package/dist/update.d.ts +84 -0
  238. package/dist/update.d.ts.map +1 -0
  239. package/dist/update.js +93 -1
  240. package/dist/update.js.map +1 -0
  241. package/dist/utils/colorUtils.d.ts +101 -0
  242. package/dist/utils/colorUtils.d.ts.map +1 -0
  243. package/dist/utils/colorUtils.js +97 -2
  244. package/dist/utils/colorUtils.js.map +1 -0
  245. package/dist/utils/commandLine.d.ts +66 -0
  246. package/dist/utils/commandLine.d.ts.map +1 -0
  247. package/dist/utils/commandLine.js +60 -0
  248. package/dist/utils/commandLine.js.map +1 -0
  249. package/dist/utils/copyDirectory.d.ts +35 -0
  250. package/dist/utils/copyDirectory.d.ts.map +1 -0
  251. package/dist/utils/copyDirectory.js +37 -0
  252. package/dist/utils/copyDirectory.js.map +1 -0
  253. package/dist/utils/createDirectory.d.ts +34 -0
  254. package/dist/utils/createDirectory.d.ts.map +1 -0
  255. package/dist/utils/createDirectory.js +33 -0
  256. package/dist/utils/createDirectory.js.map +1 -0
  257. package/dist/utils/createZip.d.ts +39 -0
  258. package/dist/utils/createZip.d.ts.map +1 -0
  259. package/dist/utils/createZip.js +47 -2
  260. package/dist/utils/createZip.js.map +1 -0
  261. package/dist/utils/deepCopy.d.ts +32 -0
  262. package/dist/utils/deepCopy.d.ts.map +1 -0
  263. package/dist/utils/deepCopy.js +39 -0
  264. package/dist/utils/deepCopy.js.map +1 -0
  265. package/dist/utils/deepEqual.d.ts +54 -0
  266. package/dist/utils/deepEqual.d.ts.map +1 -0
  267. package/dist/utils/deepEqual.js +72 -1
  268. package/dist/utils/deepEqual.js.map +1 -0
  269. package/dist/utils/error.d.ts +45 -0
  270. package/dist/utils/error.d.ts.map +1 -0
  271. package/dist/utils/error.js +42 -0
  272. package/dist/utils/error.js.map +1 -0
  273. package/dist/utils/export.d.ts +13 -0
  274. package/dist/utils/export.d.ts.map +1 -0
  275. package/dist/utils/export.js +1 -0
  276. package/dist/utils/export.js.map +1 -0
  277. package/dist/utils/format.d.ts +53 -0
  278. package/dist/utils/format.d.ts.map +1 -0
  279. package/dist/utils/format.js +49 -0
  280. package/dist/utils/format.js.map +1 -0
  281. package/dist/utils/hex.d.ts +89 -0
  282. package/dist/utils/hex.d.ts.map +1 -0
  283. package/dist/utils/hex.js +124 -0
  284. package/dist/utils/hex.js.map +1 -0
  285. package/dist/utils/inspector.d.ts +87 -0
  286. package/dist/utils/inspector.d.ts.map +1 -0
  287. package/dist/utils/inspector.js +69 -1
  288. package/dist/utils/inspector.js.map +1 -0
  289. package/dist/utils/isvalid.d.ts +103 -0
  290. package/dist/utils/isvalid.d.ts.map +1 -0
  291. package/dist/utils/isvalid.js +101 -0
  292. package/dist/utils/isvalid.js.map +1 -0
  293. package/dist/utils/network.d.ts +111 -0
  294. package/dist/utils/network.d.ts.map +1 -0
  295. package/dist/utils/network.js +96 -5
  296. package/dist/utils/network.js.map +1 -0
  297. package/dist/utils/spawn.d.ts +33 -0
  298. package/dist/utils/spawn.d.ts.map +1 -0
  299. package/dist/utils/spawn.js +71 -1
  300. package/dist/utils/spawn.js.map +1 -0
  301. package/dist/utils/tracker.d.ts +108 -0
  302. package/dist/utils/tracker.d.ts.map +1 -0
  303. package/dist/utils/tracker.js +64 -1
  304. package/dist/utils/tracker.js.map +1 -0
  305. package/dist/utils/wait.d.ts +54 -0
  306. package/dist/utils/wait.d.ts.map +1 -0
  307. package/dist/utils/wait.js +60 -8
  308. package/dist/utils/wait.js.map +1 -0
  309. package/dist/workerGlobalPrefix.d.ts +25 -0
  310. package/dist/workerGlobalPrefix.d.ts.map +1 -0
  311. package/dist/workerGlobalPrefix.js +37 -5
  312. package/dist/workerGlobalPrefix.js.map +1 -0
  313. package/dist/workerTypes.d.ts +52 -0
  314. package/dist/workerTypes.d.ts.map +1 -0
  315. package/dist/workerTypes.js +24 -0
  316. package/dist/workerTypes.js.map +1 -0
  317. package/dist/workers.d.ts +69 -0
  318. package/dist/workers.d.ts.map +1 -0
  319. package/dist/workers.js +68 -4
  320. package/dist/workers.js.map +1 -0
  321. package/frontend/package-lock.json +13 -31
  322. package/npm-shrinkwrap.json +2 -2
  323. 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.3
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 */ /* istanbul ignore next */
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));
@@ -48,6 +74,7 @@ export class Frontend extends EventEmitter {
48
74
  }
49
75
  async msgHandler(msg) {
50
76
  if (this.server.isWorkerRequest(msg)) {
77
+ // istanbul ignore else
51
78
  if (this.verbose)
52
79
  this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
53
80
  switch (msg.type) {
@@ -99,11 +126,13 @@ export class Frontend extends EventEmitter {
99
126
  this.server.respond({ ...msg, result: { success: true } });
100
127
  break;
101
128
  default:
129
+ // istanbul ignore next
102
130
  if (this.verbose)
103
131
  this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
104
132
  }
105
133
  }
106
134
  if (this.server.isWorkerResponse(msg) && msg.result) {
135
+ // istanbul ignore next
107
136
  if (this.verbose)
108
137
  this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
109
138
  switch (msg.type) {
@@ -139,23 +168,55 @@ export class Frontend extends EventEmitter {
139
168
  this.port = port;
140
169
  this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
141
170
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
171
+ // Initialize multer with the upload directory
142
172
  const multer = await import('multer');
143
- const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
173
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
144
174
  const upload = multer.default({ dest: uploadDir });
175
+ // Create the express app that serves the frontend
145
176
  const express = await import('express');
146
177
  this.expressApp = express.default();
178
+ // Inject logging/debug wrapper for route/middleware registration
179
+ /*
180
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
181
+ for (const method of methods) {
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
186
+ try {
187
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
188
+ return original(path, ...rest);
189
+ } catch (err) {
190
+ console.error(`[ERROR] Failed to register route: ${path}`);
191
+ throw err;
192
+ }
193
+ };
194
+ }
195
+ */
196
+ // Log all requests to the server for debugging
197
+ /*
198
+ this.expressApp.use((req, res, next) => {
199
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
200
+ next();
201
+ });
202
+ */
203
+ // Serve static files from 'frontend/build' directory
147
204
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend', 'build')));
205
+ // Create a WebSocket server and attach it to the http or https server
148
206
  this.log.debug(`Creating WebSocketServer...`);
149
207
  const ws = await import('ws');
150
208
  this.webSocketServer = new ws.WebSocketServer({ noServer: true });
151
209
  this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
152
210
  this.webSocketServer.on('connection', (ws, request) => {
153
211
  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";
212
+ // Set the global logger callback for the WebSocketServer
213
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
214
+ // istanbul ignore else
215
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
216
+ callbackLogLevel = "info" /* LogLevel.INFO */;
217
+ // istanbul ignore else
218
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
219
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
159
220
  AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
160
221
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
161
222
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -171,22 +232,33 @@ export class Frontend extends EventEmitter {
171
232
  });
172
233
  ws.on('close', () => {
173
234
  this.log.info('WebSocket client disconnected');
235
+ // istanbul ignore else
174
236
  if (this.webSocketServer?.clients.size === 0) {
175
237
  AnsiLogger.setGlobalCallback(undefined);
176
238
  this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
177
239
  }
178
240
  });
241
+ // istanbul ignore next
179
242
  ws.on('error', (error) => {
243
+ // istanbul ignore next
180
244
  this.log.error(`WebSocket client error: ${error}`);
181
245
  });
182
246
  });
183
247
  this.webSocketServer.on('close', () => {
184
248
  this.log.debug(`WebSocketServer closed`);
185
249
  });
250
+ /* With { noServer: true } it never fires
251
+ this.webSocketServer.on('listening', () => {
252
+ this.log.info(`The WebSocketServer is listening`);
253
+ this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
254
+ });
255
+ */
256
+ // istanbul ignore next
186
257
  this.webSocketServer.on('error', (ws, error) => {
187
258
  this.log.error(`WebSocketServer error: ${error}`);
188
259
  });
189
260
  if (!hasParameter('ssl')) {
261
+ // Create an HTTP server and attach the express app
190
262
  const http = await import('node:http');
191
263
  try {
192
264
  this.log.debug(`Creating HTTP server...`);
@@ -197,6 +269,7 @@ export class Frontend extends EventEmitter {
197
269
  this.emit('server_error', error);
198
270
  return;
199
271
  }
272
+ // Listen on the specified port
200
273
  if (hasParameter('ingress')) {
201
274
  this.httpServer.listen(this.port, '0.0.0.0', () => {
202
275
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -206,8 +279,10 @@ export class Frontend extends EventEmitter {
206
279
  }
207
280
  else {
208
281
  this.httpServer.listen(this.port, () => {
282
+ // istanbul ignore else
209
283
  if (this.matterbridge.systemInformation.ipv4Address !== '')
210
284
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
285
+ // istanbul ignore else
211
286
  if (this.matterbridge.systemInformation.ipv6Address !== '')
212
287
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
213
288
  this.listening = true;
@@ -216,24 +291,30 @@ export class Frontend extends EventEmitter {
216
291
  }
217
292
  this.httpServer.on('upgrade', async (req, socket, head) => {
218
293
  try {
294
+ // Only proceed for real WebSocket upgrades
295
+ // istanbul ignore next cause is only a safety check
219
296
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
220
297
  this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
221
298
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
222
299
  return socket.destroy();
223
300
  }
301
+ // Build a URL so we can read ?password=...
224
302
  const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
303
+ // Validate WebSocket password
225
304
  const password = url.searchParams.get('password') ?? '';
226
305
  if (password !== this.storedPassword) {
227
306
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
228
307
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
229
308
  return socket.destroy();
230
309
  }
310
+ // Complete the WebSocket handshake
231
311
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
232
312
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
233
313
  this.webSocketServer?.emit('connection', ws, req);
234
314
  });
235
315
  }
236
316
  catch (err) {
317
+ /* istanbul ignore next: only triggered on unexpected internal error */
237
318
  {
238
319
  inspectError(this.log, 'WebSocket upgrade error:', err);
239
320
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -256,6 +337,7 @@ export class Frontend extends EventEmitter {
256
337
  });
257
338
  }
258
339
  else {
340
+ // SSL is enabled, load the certificate and the private key
259
341
  let cert;
260
342
  let key;
261
343
  let ca;
@@ -265,6 +347,7 @@ export class Frontend extends EventEmitter {
265
347
  let httpsServerOptions = {};
266
348
  const fs = await import('node:fs');
267
349
  if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
350
+ // Load the p12 certificate and the passphrase
268
351
  try {
269
352
  pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
270
353
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
@@ -276,7 +359,7 @@ export class Frontend extends EventEmitter {
276
359
  }
277
360
  try {
278
361
  passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
279
- passphrase = passphrase.trim();
362
+ passphrase = passphrase.trim(); // Ensure no extra characters
280
363
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
281
364
  }
282
365
  catch (error) {
@@ -287,6 +370,7 @@ export class Frontend extends EventEmitter {
287
370
  httpsServerOptions = { pfx, passphrase };
288
371
  }
289
372
  else {
373
+ // 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
374
  try {
291
375
  cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
292
376
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
@@ -316,9 +400,10 @@ export class Frontend extends EventEmitter {
316
400
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
317
401
  }
318
402
  if (hasParameter('mtls')) {
319
- httpsServerOptions.requestCert = true;
320
- httpsServerOptions.rejectUnauthorized = true;
403
+ httpsServerOptions.requestCert = true; // Request client certificate
404
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
321
405
  }
406
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
322
407
  const https = await import('node:https');
323
408
  try {
324
409
  this.log.debug(`Creating HTTPS server...`);
@@ -329,6 +414,7 @@ export class Frontend extends EventEmitter {
329
414
  this.emit('server_error', error);
330
415
  return;
331
416
  }
417
+ // Listen on the specified port
332
418
  if (hasParameter('ingress')) {
333
419
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
334
420
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -338,8 +424,10 @@ export class Frontend extends EventEmitter {
338
424
  }
339
425
  else {
340
426
  this.httpsServer.listen(this.port, () => {
427
+ // istanbul ignore else
341
428
  if (this.matterbridge.systemInformation.ipv4Address !== '')
342
429
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
430
+ // istanbul ignore else
343
431
  if (this.matterbridge.systemInformation.ipv6Address !== '')
344
432
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
345
433
  this.listening = true;
@@ -348,23 +436,29 @@ export class Frontend extends EventEmitter {
348
436
  }
349
437
  this.httpsServer.on('upgrade', async (req, socket, head) => {
350
438
  try {
439
+ // Only proceed for real WebSocket upgrades
440
+ // istanbul ignore next cause is only a safety check
351
441
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
352
442
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
353
443
  return socket.destroy();
354
444
  }
445
+ // Build a URL so we can read ?password=...
355
446
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
447
+ // Validate WebSocket password
356
448
  const password = url.searchParams.get('password') ?? '';
357
449
  if (password !== this.storedPassword) {
358
450
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
359
451
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
360
452
  return socket.destroy();
361
453
  }
454
+ // Complete the WebSocket handshake
362
455
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
363
456
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
364
457
  this.webSocketServer?.emit('connection', ws, req);
365
458
  });
366
459
  }
367
460
  catch (err) {
461
+ /* istanbul ignore next: only triggered on unexpected internal error */
368
462
  {
369
463
  inspectError(this.log, 'WebSocket upgrade error:', err);
370
464
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -386,6 +480,7 @@ export class Frontend extends EventEmitter {
386
480
  return;
387
481
  });
388
482
  }
483
+ // Subscribe to cli events
389
484
  cliEmitter.removeAllListeners();
390
485
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
391
486
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -396,6 +491,8 @@ export class Frontend extends EventEmitter {
396
491
  cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
397
492
  this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
398
493
  });
494
+ // Endpoint to validate login code
495
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
399
496
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
400
497
  const { password } = req.body;
401
498
  this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
@@ -408,17 +505,20 @@ export class Frontend extends EventEmitter {
408
505
  res.json({ valid: false });
409
506
  }
410
507
  });
508
+ // Endpoint to provide health check for docker
411
509
  this.expressApp.get('/health', (req, res) => {
412
510
  this.log.debug('Express received /health');
413
511
  const healthStatus = {
414
- status: 'ok',
415
- uptime: process.uptime(),
416
- timestamp: new Date().toISOString(),
512
+ status: 'ok', // Indicate service is healthy
513
+ uptime: process.uptime(), // Server uptime in seconds
514
+ timestamp: new Date().toISOString(), // Current timestamp
417
515
  };
418
516
  res.status(200).json(healthStatus);
419
517
  });
518
+ // Endpoint to provide memory usage details
420
519
  this.expressApp.get('/memory', async (req, res) => {
421
520
  this.log.debug('Express received /memory');
521
+ // Memory usage from process
422
522
  const memoryUsageRaw = process.memoryUsage();
423
523
  const memoryUsage = {
424
524
  rss: formatBytes(memoryUsageRaw.rss),
@@ -427,10 +527,13 @@ export class Frontend extends EventEmitter {
427
527
  external: formatBytes(memoryUsageRaw.external),
428
528
  arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
429
529
  };
530
+ // V8 heap statistics
430
531
  const { default: v8 } = await import('node:v8');
431
532
  const heapStatsRaw = v8.getHeapStatistics();
432
533
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
534
+ // Format heapStats
433
535
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
536
+ // Format heapSpaces
434
537
  const heapSpaces = heapSpacesRaw.map((space) => ({
435
538
  ...space,
436
539
  space_size: formatBytes(space.space_size),
@@ -449,18 +552,22 @@ export class Frontend extends EventEmitter {
449
552
  };
450
553
  res.status(200).json(memoryReport);
451
554
  });
555
+ // Endpoint to provide settings
452
556
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
453
557
  this.log.debug('The frontend sent /api/settings');
454
558
  res.json(await this.getApiSettings());
455
559
  });
560
+ // Endpoint to provide plugins
456
561
  this.expressApp.get('/api/plugins', async (req, res) => {
457
562
  this.log.debug('The frontend sent /api/plugins');
458
563
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
459
564
  });
565
+ // Endpoint to provide devices
460
566
  this.expressApp.get('/api/devices', async (req, res) => {
461
567
  this.log.debug('The frontend sent /api/devices');
462
568
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
463
569
  });
570
+ // Endpoint to view the matterbridge log
464
571
  this.expressApp.get('/api/view-mblog', async (req, res) => {
465
572
  this.log.debug('The frontend sent /api/view-mblog');
466
573
  try {
@@ -474,6 +581,7 @@ export class Frontend extends EventEmitter {
474
581
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
475
582
  }
476
583
  });
584
+ // Endpoint to view the matter.js log
477
585
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
478
586
  this.log.debug('The frontend sent /api/view-mjlog');
479
587
  try {
@@ -487,6 +595,7 @@ export class Frontend extends EventEmitter {
487
595
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
488
596
  }
489
597
  });
598
+ // Endpoint to view the diagnostic.log
490
599
  this.expressApp.get('/api/view-diagnostic', async (req, res) => {
491
600
  this.log.debug('The frontend sent /api/view-diagnostic');
492
601
  await this.generateDiagnostic();
@@ -497,10 +606,13 @@ export class Frontend extends EventEmitter {
497
606
  res.send(data.slice(29));
498
607
  }
499
608
  catch (error) {
609
+ // istanbul ignore next
500
610
  this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
611
+ // istanbul ignore next
501
612
  res.status(500).send('Error reading diagnostic log file.');
502
613
  }
503
614
  });
615
+ // Endpoint to download the diagnostic.log
504
616
  this.expressApp.get('/api/download-diagnostic', async (req, res) => {
505
617
  this.log.debug(`The frontend sent /api/download-diagnostic`);
506
618
  await this.generateDiagnostic();
@@ -511,16 +623,19 @@ export class Frontend extends EventEmitter {
511
623
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
512
624
  }
513
625
  catch (error) {
626
+ // istanbul ignore next
514
627
  this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
515
628
  }
516
629
  res.type('text/plain');
517
630
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
631
+ /* istanbul ignore if */
518
632
  if (error) {
519
633
  this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
520
634
  res.status(500).send('Error downloading the diagnostic log file');
521
635
  }
522
636
  });
523
637
  });
638
+ // Endpoint to view the history.html
524
639
  this.expressApp.get('/api/viewhistory', async (req, res) => {
525
640
  this.log.debug('The frontend sent /api/viewhistory');
526
641
  try {
@@ -534,6 +649,7 @@ export class Frontend extends EventEmitter {
534
649
  res.status(500).send('Error reading history file.');
535
650
  }
536
651
  });
652
+ // Endpoint to download the history.html
537
653
  this.expressApp.get('/api/downloadhistory', async (req, res) => {
538
654
  this.log.debug(`The frontend sent /api/downloadhistory`);
539
655
  try {
@@ -543,6 +659,7 @@ export class Frontend extends EventEmitter {
543
659
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
544
660
  res.type('text/plain');
545
661
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
662
+ /* istanbul ignore if */
546
663
  if (error) {
547
664
  this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
548
665
  res.status(500).send('Error downloading history file');
@@ -554,6 +671,7 @@ export class Frontend extends EventEmitter {
554
671
  res.status(500).send('Error reading history file.');
555
672
  }
556
673
  });
674
+ // Endpoint to view the shelly log
557
675
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
558
676
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
559
677
  try {
@@ -567,6 +685,7 @@ export class Frontend extends EventEmitter {
567
685
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
568
686
  }
569
687
  });
688
+ // Endpoint to download the matterbridge log
570
689
  this.expressApp.get('/api/download-mblog', async (req, res) => {
571
690
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
572
691
  const fs = await import('node:fs');
@@ -581,12 +700,14 @@ export class Frontend extends EventEmitter {
581
700
  }
582
701
  res.type('text/plain');
583
702
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
703
+ /* istanbul ignore if */
584
704
  if (error) {
585
705
  this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
586
706
  res.status(500).send('Error downloading the matterbridge log file');
587
707
  }
588
708
  });
589
709
  });
710
+ // Endpoint to download the matter log
590
711
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
591
712
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
592
713
  const fs = await import('node:fs');
@@ -601,12 +722,14 @@ export class Frontend extends EventEmitter {
601
722
  }
602
723
  res.type('text/plain');
603
724
  res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
725
+ /* istanbul ignore if */
604
726
  if (error) {
605
727
  this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
606
728
  res.status(500).send('Error downloading the matter log file');
607
729
  }
608
730
  });
609
731
  });
732
+ // Endpoint to download the shelly log
610
733
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
611
734
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
612
735
  const fs = await import('node:fs');
@@ -621,75 +744,91 @@ export class Frontend extends EventEmitter {
621
744
  }
622
745
  res.type('text/plain');
623
746
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
747
+ /* istanbul ignore if */
624
748
  if (error) {
625
749
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
626
750
  res.status(500).send('Error downloading Shelly system log file');
627
751
  }
628
752
  });
629
753
  });
754
+ // Endpoint to download the matterbridge storage directory
630
755
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
631
756
  this.log.debug('The frontend sent /api/download-mbstorage');
632
757
  await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
633
758
  res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
759
+ /* istanbul ignore if */
634
760
  if (error) {
635
761
  this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
636
762
  res.status(500).send('Error downloading the matterbridge storage file');
637
763
  }
638
764
  });
639
765
  });
766
+ // Endpoint to download the matter storage file
640
767
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
641
768
  this.log.debug('The frontend sent /api/download-mjstorage');
642
769
  await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
643
770
  res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
771
+ /* istanbul ignore if */
644
772
  if (error) {
645
773
  this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
646
774
  res.status(500).send('Error downloading the matter storage zip file');
647
775
  }
648
776
  });
649
777
  });
778
+ // Endpoint to download the matterbridge plugin directory
650
779
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
651
780
  this.log.debug('The frontend sent /api/download-pluginstorage');
652
781
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
653
782
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
783
+ /* istanbul ignore if */
654
784
  if (error) {
655
785
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
656
786
  res.status(500).send('Error downloading the matterbridge plugin storage file');
657
787
  }
658
788
  });
659
789
  });
790
+ // Endpoint to download the matterbridge plugin config files
660
791
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
661
792
  this.log.debug('The frontend sent /api/download-pluginconfig');
662
793
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
663
794
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
795
+ /* istanbul ignore if */
664
796
  if (error) {
665
797
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
666
798
  res.status(500).send('Error downloading the matterbridge plugin config file');
667
799
  }
668
800
  });
669
801
  });
802
+ // Endpoint to download the matterbridge backup (created with the backup command)
670
803
  this.expressApp.get('/api/download-backup', async (req, res) => {
671
804
  this.log.debug('The frontend sent /api/download-backup');
672
805
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
806
+ /* istanbul ignore if */
673
807
  if (error) {
674
808
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
675
809
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
676
810
  }
677
811
  });
678
812
  });
813
+ // Endpoint to upload a package
679
814
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
680
815
  const { filename } = req.body;
681
816
  const file = req.file;
817
+ /* istanbul ignore if */
682
818
  if (!file || !filename) {
683
819
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
684
820
  res.status(400).send('Invalid request: file and filename are required');
685
821
  return;
686
822
  }
687
823
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
824
+ // Define the path where the plugin file will be saved
688
825
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
689
826
  try {
827
+ // Move the uploaded file to the specified path
690
828
  const fs = await import('node:fs');
691
829
  await fs.promises.rename(file.path, filePath);
692
830
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
831
+ // Install the plugin package
693
832
  if (filename.endsWith('.tgz')) {
694
833
  const { spawnCommand } = await import('./utils/spawn.js');
695
834
  if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
@@ -717,6 +856,7 @@ export class Frontend extends EventEmitter {
717
856
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
718
857
  }
719
858
  });
859
+ // Fallback for routing (must be the last route)
720
860
  this.expressApp.use((req, res) => {
721
861
  const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
722
862
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
@@ -727,13 +867,16 @@ export class Frontend extends EventEmitter {
727
867
  async stop() {
728
868
  this.log.debug('Stopping the frontend...');
729
869
  const ws = await import('ws');
870
+ // Remove listeners from the express app
730
871
  if (this.expressApp) {
731
872
  this.expressApp.removeAllListeners();
732
873
  this.expressApp = undefined;
733
874
  this.log.debug('Frontend app closed successfully');
734
875
  }
876
+ // Close the WebSocket server
735
877
  if (this.webSocketServer) {
736
878
  this.log.debug('Closing WebSocket server...');
879
+ // Close all active connections
737
880
  this.webSocketServer.clients.forEach((client) => {
738
881
  if (client.readyState === ws.WebSocket.OPEN) {
739
882
  client.close();
@@ -741,7 +884,9 @@ export class Frontend extends EventEmitter {
741
884
  });
742
885
  await withTimeout(new Promise((resolve) => {
743
886
  this.webSocketServer?.close((error) => {
887
+ // istanbul ignore if
744
888
  if (error) {
889
+ // istanbul ignore next
745
890
  this.log.error(`Error closing WebSocket server: ${error}`);
746
891
  }
747
892
  else {
@@ -754,8 +899,27 @@ export class Frontend extends EventEmitter {
754
899
  this.webSocketServer.removeAllListeners();
755
900
  this.webSocketServer = undefined;
756
901
  }
902
+ // Close the http server
757
903
  if (this.httpServer) {
758
904
  this.log.debug('Closing http server...');
905
+ /*
906
+ await withTimeout(
907
+ new Promise<void>((resolve) => {
908
+ this.httpServer?.close((error) => {
909
+ if (error) {
910
+ // istanbul ignore next
911
+ this.log.error(`Error closing http server: ${error}`);
912
+ } else {
913
+ this.log.debug('Http server closed successfully');
914
+ this.emit('server_stopped');
915
+ }
916
+ resolve();
917
+ });
918
+ }),
919
+ 5000,
920
+ false,
921
+ );
922
+ */
759
923
  this.httpServer.close();
760
924
  this.log.debug('Http server closed successfully');
761
925
  this.listening = false;
@@ -764,8 +928,27 @@ export class Frontend extends EventEmitter {
764
928
  this.httpServer = undefined;
765
929
  this.log.debug('Frontend http server closed successfully');
766
930
  }
931
+ // Close the https server
767
932
  if (this.httpsServer) {
768
933
  this.log.debug('Closing https server...');
934
+ /*
935
+ await withTimeout(
936
+ new Promise<void>((resolve) => {
937
+ this.httpsServer?.close((error) => {
938
+ if (error) {
939
+ // istanbul ignore next
940
+ this.log.error(`Error closing https server: ${error}`);
941
+ } else {
942
+ this.log.debug('Https server closed successfully');
943
+ this.emit('server_stopped');
944
+ }
945
+ resolve();
946
+ });
947
+ }),
948
+ 5000,
949
+ false,
950
+ );
951
+ */
769
952
  this.httpsServer.close();
770
953
  this.log.debug('Https server closed successfully');
771
954
  this.listening = false;
@@ -776,7 +959,13 @@ export class Frontend extends EventEmitter {
776
959
  }
777
960
  this.log.debug('Frontend stopped successfully');
778
961
  }
962
+ /**
963
+ * Retrieves the api settings data.
964
+ *
965
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
966
+ */
779
967
  async getApiSettings() {
968
+ // Update the variable system information properties
780
969
  this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
781
970
  this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
782
971
  this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
@@ -786,6 +975,7 @@ export class Frontend extends EventEmitter {
786
975
  this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
787
976
  this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
788
977
  this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
978
+ // Create the matterbridge information
789
979
  const info = {
790
980
  homeDirectory: this.matterbridge.homeDirectory,
791
981
  rootDirectory: this.matterbridge.rootDirectory,
@@ -821,9 +1011,15 @@ export class Frontend extends EventEmitter {
821
1011
  };
822
1012
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
823
1013
  }
1014
+ /**
1015
+ * Retrieves the reachable attribute.
1016
+ *
1017
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
1018
+ * @returns {boolean} The reachable attribute.
1019
+ */
824
1020
  getReachability(device) {
825
1021
  if (this.matterbridge.hasCleanupStarted)
826
- return false;
1022
+ return false; // Skip if cleanup has started
827
1023
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
828
1024
  return false;
829
1025
  if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
@@ -834,9 +1030,15 @@ export class Frontend extends EventEmitter {
834
1030
  return true;
835
1031
  return false;
836
1032
  }
1033
+ /**
1034
+ * Retrieves the power source attribute.
1035
+ *
1036
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1037
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1038
+ */
837
1039
  getPowerSource(endpoint) {
838
1040
  if (this.matterbridge.hasCleanupStarted)
839
- return undefined;
1041
+ return undefined; // Skip if cleanup has started
840
1042
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
841
1043
  return undefined;
842
1044
  const powerSource = (device) => {
@@ -851,16 +1053,25 @@ export class Frontend extends EventEmitter {
851
1053
  }
852
1054
  return;
853
1055
  };
1056
+ // Root endpoint
854
1057
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
855
1058
  return powerSource(endpoint);
1059
+ // Child endpoints
856
1060
  for (const child of endpoint.getChildEndpoints()) {
1061
+ // istanbul ignore else
857
1062
  if (child.hasClusterServer(PowerSource.Cluster.id))
858
1063
  return powerSource(child);
859
1064
  }
860
1065
  }
1066
+ /**
1067
+ * Retrieves the battery level attribute.
1068
+ *
1069
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1070
+ * @returns {number | undefined} The battery level attribute.
1071
+ */
861
1072
  getBatteryLevel(endpoint) {
862
1073
  if (this.matterbridge.hasCleanupStarted)
863
- return undefined;
1074
+ return undefined; // Skip if cleanup has started
864
1075
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
865
1076
  return undefined;
866
1077
  const batteryLevel = (device) => {
@@ -871,16 +1082,27 @@ export class Frontend extends EventEmitter {
871
1082
  }
872
1083
  return undefined;
873
1084
  };
1085
+ // Root endpoint
874
1086
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
875
1087
  return batteryLevel(endpoint);
1088
+ // Child endpoints
876
1089
  for (const child of endpoint.getChildEndpoints()) {
1090
+ // istanbul ignore else
877
1091
  if (child.hasClusterServer(PowerSource.Cluster.id))
878
1092
  return batteryLevel(child);
879
1093
  }
880
1094
  }
1095
+ /**
1096
+ * Retrieves the cluster text description from a given device.
1097
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1098
+ *
1099
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1100
+ * @returns {string} The attributes description of the cluster servers in the device.
1101
+ */
881
1102
  getClusterTextFromDevice(device) {
882
1103
  if (this.matterbridge.hasCleanupStarted)
883
- return '';
1104
+ return ''; // Skip if cleanup has started
1105
+ // istanbul ignore else
884
1106
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
885
1107
  return '';
886
1108
  const getUserLabel = (device) => {
@@ -890,6 +1112,7 @@ export class Frontend extends EventEmitter {
890
1112
  if (composed)
891
1113
  return 'Composed: ' + composed.value;
892
1114
  }
1115
+ // istanbul ignore next cause is not reachable
893
1116
  return '';
894
1117
  };
895
1118
  const getFixedLabel = (device) => {
@@ -899,11 +1122,13 @@ export class Frontend extends EventEmitter {
899
1122
  if (composed)
900
1123
  return 'Composed: ' + composed.value;
901
1124
  }
1125
+ // istanbul ignore next cause is not reacheable
902
1126
  return '';
903
1127
  };
904
1128
  let attributes = '';
905
1129
  let supportedModes = [];
906
1130
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1131
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
907
1132
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
908
1133
  return;
909
1134
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -993,11 +1218,17 @@ export class Frontend extends EventEmitter {
993
1218
  if (clusterName === 'userLabel' && attributeName === 'labelList')
994
1219
  attributes += `${getUserLabel(device)} `;
995
1220
  });
1221
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
996
1222
  return attributes.trimStart().trimEnd();
997
1223
  }
1224
+ /**
1225
+ * Retrieves the registered plugins sanitized for res.json().
1226
+ *
1227
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1228
+ */
998
1229
  getPlugins() {
999
1230
  if (this.matterbridge.hasCleanupStarted)
1000
- return [];
1231
+ return []; // Skip if cleanup has started
1001
1232
  const plugins = [];
1002
1233
  for (const plugin of this.matterbridge.plugins.array()) {
1003
1234
  plugins.push({
@@ -1025,18 +1256,27 @@ export class Frontend extends EventEmitter {
1025
1256
  schemaJson: plugin.schemaJson,
1026
1257
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
1027
1258
  hasBlackList: plugin.configJson?.blackList !== undefined,
1259
+ // Childbridge mode specific data
1028
1260
  matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
1029
1261
  });
1030
1262
  }
1031
1263
  return plugins;
1032
1264
  }
1265
+ /**
1266
+ * Retrieves the devices from Matterbridge.
1267
+ *
1268
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1269
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1270
+ */
1033
1271
  getDevices(pluginName) {
1034
1272
  if (this.matterbridge.hasCleanupStarted)
1035
- return [];
1273
+ return []; // Skip if cleanup has started
1036
1274
  const devices = [];
1037
1275
  for (const device of this.matterbridge.devices.array()) {
1276
+ // Filter by pluginName if provided
1038
1277
  if (pluginName && pluginName !== device.plugin)
1039
1278
  continue;
1279
+ // Check if the device has the required properties
1040
1280
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1041
1281
  continue;
1042
1282
  devices.push({
@@ -1057,24 +1297,39 @@ export class Frontend extends EventEmitter {
1057
1297
  }
1058
1298
  return devices;
1059
1299
  }
1300
+ /**
1301
+ * Retrieves the clusters from a given plugin and endpoint number.
1302
+ *
1303
+ * Response for /api/clusters
1304
+ *
1305
+ * @param {string} pluginName - The name of the plugin.
1306
+ * @param {number} endpointNumber - The endpoint number.
1307
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1308
+ */
1060
1309
  getClusters(pluginName, endpointNumber) {
1061
1310
  if (this.matterbridge.hasCleanupStarted)
1062
- return;
1311
+ return; // Skip if cleanup has started
1063
1312
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1064
1313
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1065
1314
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1066
1315
  return;
1067
1316
  }
1317
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1318
+ // Get the device types from the main endpoint
1068
1319
  const deviceTypes = [];
1069
1320
  const clusters = [];
1070
1321
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1071
1322
  deviceTypes.push(d.deviceType);
1072
1323
  });
1324
+ // Get the clusters from the main endpoint
1073
1325
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1074
1326
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1075
1327
  return;
1076
1328
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1077
1329
  return;
1330
+ // console.log(
1331
+ // `${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}`,
1332
+ // );
1078
1333
  clusters.push({
1079
1334
  endpoint: endpoint.number.toString(),
1080
1335
  number: endpoint.number,
@@ -1088,12 +1343,19 @@ export class Frontend extends EventEmitter {
1088
1343
  attributeLocalValue: attributeValue,
1089
1344
  });
1090
1345
  });
1346
+ // Get the child endpoints
1091
1347
  const childEndpoints = endpoint.getChildEndpoints();
1348
+ // if (childEndpoints.length === 0) {
1349
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1350
+ // }
1092
1351
  childEndpoints.forEach((childEndpoint) => {
1352
+ // istanbul ignore if cause is not reachable: should never happen but ...
1093
1353
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1094
1354
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1095
1355
  return;
1096
1356
  }
1357
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1358
+ // Get the device types of the child endpoint
1097
1359
  const deviceTypes = [];
1098
1360
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1099
1361
  deviceTypes.push(d.deviceType);
@@ -1103,6 +1365,9 @@ export class Frontend extends EventEmitter {
1103
1365
  return;
1104
1366
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1105
1367
  return;
1368
+ // console.log(
1369
+ // `${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}`,
1370
+ // );
1106
1371
  clusters.push({
1107
1372
  endpoint: childEndpoint.number.toString(),
1108
1373
  number: childEndpoint.number,
@@ -1122,6 +1387,7 @@ export class Frontend extends EventEmitter {
1122
1387
  async generateDiagnostic() {
1123
1388
  this.log.debug('Generating diagnostic...');
1124
1389
  const serverNodes = [];
1390
+ // istanbul ignore else
1125
1391
  if (this.matterbridge.bridgeMode === 'bridge') {
1126
1392
  if (this.matterbridge.serverNode)
1127
1393
  serverNodes.push(this.matterbridge.serverNode);
@@ -1132,6 +1398,7 @@ export class Frontend extends EventEmitter {
1132
1398
  serverNodes.push(plugin.serverNode);
1133
1399
  }
1134
1400
  }
1401
+ // istanbul ignore next
1135
1402
  for (const device of this.matterbridge.devices.array()) {
1136
1403
  if (device.serverNode)
1137
1404
  serverNodes.push(device.serverNode);
@@ -1155,8 +1422,15 @@ export class Frontend extends EventEmitter {
1155
1422
  values: [...serverNodes],
1156
1423
  })));
1157
1424
  delete Logger.destinations.diagnostic;
1158
- await wait(500);
1425
+ await wait(500); // Wait for the log to be written
1159
1426
  }
1427
+ /**
1428
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1429
+ *
1430
+ * @param {WebSocket} client - The websocket client that sent the message.
1431
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1432
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1433
+ */
1160
1434
  async wsMessageHandler(client, message) {
1161
1435
  let data;
1162
1436
  const sendResponse = (data) => {
@@ -1174,12 +1448,13 @@ export class Frontend extends EventEmitter {
1174
1448
  client.send(JSON.stringify(data));
1175
1449
  }
1176
1450
  else {
1451
+ // istanbul ignore next cause is only a safety check
1177
1452
  this.log.error('Cannot send api response, client not connected');
1178
1453
  }
1179
1454
  };
1180
1455
  try {
1181
1456
  data = JSON.parse(message.toString());
1182
- if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
1457
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
1183
1458
  this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1184
1459
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1185
1460
  return;
@@ -1236,7 +1511,22 @@ export class Frontend extends EventEmitter {
1236
1511
  }
1237
1512
  this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
1238
1513
  this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
1239
- data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
1514
+ data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
1515
+ /*
1516
+ const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
1517
+ if (plugin) {
1518
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1519
+ await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
1520
+ this.wssSendRestartRequired();
1521
+ this.wssSendRefreshRequired('plugins');
1522
+ this.wssSendRefreshRequired('devices');
1523
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1524
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1525
+ } else {
1526
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1527
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1528
+ }
1529
+ */
1240
1530
  const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
1241
1531
  if (plugin) {
1242
1532
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1249,7 +1539,7 @@ export class Frontend extends EventEmitter {
1249
1539
  this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1250
1540
  return;
1251
1541
  })
1252
- .catch((_error) => { });
1542
+ .catch(/* istanbul ignore next */ (_error) => { });
1253
1543
  }
1254
1544
  else {
1255
1545
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
@@ -1263,6 +1553,10 @@ export class Frontend extends EventEmitter {
1263
1553
  }
1264
1554
  this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
1265
1555
  this.log.debug(`Removing plugin ${data.params.pluginName}...`);
1556
+ /*
1557
+ 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);
1558
+ await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
1559
+ */
1266
1560
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1267
1561
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1268
1562
  await this.matterbridge.plugins.remove(data.params.pluginName);
@@ -1297,7 +1591,7 @@ export class Frontend extends EventEmitter {
1297
1591
  this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
1298
1592
  return;
1299
1593
  })
1300
- .catch((_error) => { });
1594
+ .catch(/* istanbul ignore next */ (_error) => { });
1301
1595
  }
1302
1596
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1303
1597
  }
@@ -1322,6 +1616,7 @@ export class Frontend extends EventEmitter {
1322
1616
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1323
1617
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1324
1618
  if (plugin.serverNode) {
1619
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1325
1620
  await this.matterbridge.stopServerNode(plugin.serverNode);
1326
1621
  plugin.serverNode = undefined;
1327
1622
  }
@@ -1331,18 +1626,20 @@ export class Frontend extends EventEmitter {
1331
1626
  this.matterbridge.devices.remove(device);
1332
1627
  }
1333
1628
  }
1629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1334
1630
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1335
1631
  await this.matterbridge.createDynamicPlugin(plugin);
1336
1632
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1337
- plugin.restartRequired = false;
1633
+ plugin.restartRequired = false; // Reset plugin restartRequired
1338
1634
  let needRestart = 0;
1339
1635
  for (const plugin of this.matterbridge.plugins) {
1340
1636
  if (plugin.restartRequired)
1341
1637
  needRestart++;
1342
1638
  }
1343
1639
  if (needRestart === 0) {
1344
- this.wssSendRestartNotRequired(true);
1640
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1345
1641
  }
1642
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1346
1643
  if (plugin.serverNode)
1347
1644
  await this.matterbridge.startServerNode(plugin.serverNode);
1348
1645
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1487,6 +1784,9 @@ export class Frontend extends EventEmitter {
1487
1784
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1488
1785
  }
1489
1786
  if (data.params.advertise) {
1787
+ // TODO: matter.js 0.16.0
1788
+ // await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
1789
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1490
1790
  const advertiser = serverNode.env.get(DeviceAdvertiser);
1491
1791
  if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
1492
1792
  await advertiser.advertise(true);
@@ -1548,6 +1848,7 @@ export class Frontend extends EventEmitter {
1548
1848
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
1549
1849
  return;
1550
1850
  }
1851
+ // istanbul ignore next
1551
1852
  const selectDeviceValues = !plugin.platform ? [] : plugin.platform.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1552
1853
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDeviceValues });
1553
1854
  }
@@ -1561,6 +1862,7 @@ export class Frontend extends EventEmitter {
1561
1862
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
1562
1863
  return;
1563
1864
  }
1865
+ // istanbul ignore next
1564
1866
  const selectEntityValues = !plugin.platform ? [] : plugin.platform.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1565
1867
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntityValues });
1566
1868
  }
@@ -1612,22 +1914,22 @@ export class Frontend extends EventEmitter {
1612
1914
  if (isValidString(data.params.value, 4)) {
1613
1915
  this.log.debug('Matterbridge logger level:', data.params.value);
1614
1916
  if (data.params.value === 'Debug') {
1615
- await this.matterbridge.setLogLevel("debug");
1917
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1616
1918
  }
1617
1919
  else if (data.params.value === 'Info') {
1618
- await this.matterbridge.setLogLevel("info");
1920
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1619
1921
  }
1620
1922
  else if (data.params.value === 'Notice') {
1621
- await this.matterbridge.setLogLevel("notice");
1923
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1622
1924
  }
1623
1925
  else if (data.params.value === 'Warn') {
1624
- await this.matterbridge.setLogLevel("warn");
1926
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1625
1927
  }
1626
1928
  else if (data.params.value === 'Error') {
1627
- await this.matterbridge.setLogLevel("error");
1929
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1628
1930
  }
1629
1931
  else if (data.params.value === 'Fatal') {
1630
- await this.matterbridge.setLogLevel("fatal");
1932
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1631
1933
  }
1632
1934
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1633
1935
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1638,6 +1940,7 @@ export class Frontend extends EventEmitter {
1638
1940
  this.log.debug('Matterbridge file log:', data.params.value);
1639
1941
  this.matterbridge.fileLogger = data.params.value;
1640
1942
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1943
+ // Create the file logger for matterbridge
1641
1944
  if (data.params.value)
1642
1945
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1643
1946
  else
@@ -1667,11 +1970,12 @@ export class Frontend extends EventEmitter {
1667
1970
  Logger.level = MatterLogLevel.FATAL;
1668
1971
  }
1669
1972
  this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
1670
- let callbackLogLevel = "notice";
1671
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1672
- callbackLogLevel = "info";
1673
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1674
- callbackLogLevel = "debug";
1973
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
1974
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
1975
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
1976
+ callbackLogLevel = "info" /* LogLevel.INFO */;
1977
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
1978
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1675
1979
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1676
1980
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1677
1981
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1723,6 +2027,7 @@ export class Frontend extends EventEmitter {
1723
2027
  }
1724
2028
  break;
1725
2029
  case 'setmatterport':
2030
+ // eslint-disable-next-line no-case-declarations
1726
2031
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1727
2032
  if (isValidNumber(port, 5540, 5600)) {
1728
2033
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1742,6 +2047,7 @@ export class Frontend extends EventEmitter {
1742
2047
  }
1743
2048
  break;
1744
2049
  case 'setmatterdiscriminator':
2050
+ // eslint-disable-next-line no-case-declarations
1745
2051
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1746
2052
  if (isValidNumber(discriminator, 0, 4095)) {
1747
2053
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1761,6 +2067,7 @@ export class Frontend extends EventEmitter {
1761
2067
  }
1762
2068
  break;
1763
2069
  case 'setmatterpasscode':
2070
+ // eslint-disable-next-line no-case-declarations
1764
2071
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1765
2072
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1766
2073
  this.matterbridge.passcode = passcode;
@@ -1806,15 +2113,19 @@ export class Frontend extends EventEmitter {
1806
2113
  return;
1807
2114
  }
1808
2115
  const config = plugin.configJson;
2116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1809
2117
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2118
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1810
2119
  if (select === 'serial')
1811
2120
  this.log.info(`Selected device serial ${data.params.serial}`);
1812
2121
  if (select === 'name')
1813
2122
  this.log.info(`Selected device name ${data.params.name}`);
1814
2123
  if (config && select && (select === 'serial' || select === 'name')) {
2124
+ // Remove postfix from the serial if it exists
1815
2125
  if (config.postfix) {
1816
2126
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1817
2127
  }
2128
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1818
2129
  if (isValidArray(config.whiteList, 1)) {
1819
2130
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1820
2131
  config.whiteList.push(data.params.serial);
@@ -1823,6 +2134,7 @@ export class Frontend extends EventEmitter {
1823
2134
  config.whiteList.push(data.params.name);
1824
2135
  }
1825
2136
  }
2137
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1826
2138
  if (isValidArray(config.blackList, 1)) {
1827
2139
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1828
2140
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1850,7 +2162,9 @@ export class Frontend extends EventEmitter {
1850
2162
  return;
1851
2163
  }
1852
2164
  const config = plugin.configJson;
2165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1853
2166
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2167
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1854
2168
  if (select === 'serial')
1855
2169
  this.log.info(`Unselected device serial ${data.params.serial}`);
1856
2170
  if (select === 'name')
@@ -1859,6 +2173,7 @@ export class Frontend extends EventEmitter {
1859
2173
  if (config.postfix) {
1860
2174
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1861
2175
  }
2176
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1862
2177
  if (isValidArray(config.whiteList, 1)) {
1863
2178
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1864
2179
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1867,6 +2182,7 @@ export class Frontend extends EventEmitter {
1867
2182
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1868
2183
  }
1869
2184
  }
2185
+ // Add the serial to the blackList
1870
2186
  if (isValidArray(config.blackList)) {
1871
2187
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1872
2188
  config.blackList.push(data.params.serial);
@@ -1889,6 +2205,7 @@ export class Frontend extends EventEmitter {
1889
2205
  }
1890
2206
  }
1891
2207
  else {
2208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1892
2209
  const localData = data;
1893
2210
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1894
2211
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1898,23 +2215,46 @@ export class Frontend extends EventEmitter {
1898
2215
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1899
2216
  }
1900
2217
  }
2218
+ /**
2219
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2220
+ *
2221
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2222
+ * @param {string} time - The time string of the message
2223
+ * @param {string} name - The logger name of the message
2224
+ * @param {string} message - The content of the message.
2225
+ *
2226
+ * @remarks
2227
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2228
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2229
+ * The function sends the message to all connected clients.
2230
+ */
1901
2231
  wssSendLogMessage(level, time, name, message) {
1902
2232
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1903
2233
  return;
1904
2234
  if (!level || !time || !name || !message)
1905
2235
  return;
2236
+ // Remove ANSI escape codes from the message
2237
+ // eslint-disable-next-line no-control-regex
1906
2238
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2239
+ // Remove leading asterisks from the message
1907
2240
  message = message.replace(/^\*+/, '');
2241
+ // Replace all occurrences of \t and \n
1908
2242
  message = message.replace(/[\t\n]/g, '');
2243
+ // Remove non-printable characters
2244
+ // eslint-disable-next-line no-control-regex
1909
2245
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2246
+ // Replace all occurrences of \" with "
1910
2247
  message = message.replace(/\\"/g, '"');
2248
+ // Define the maximum allowed length for continuous characters without a space
1911
2249
  const maxContinuousLength = 100;
1912
2250
  const keepStartLength = 20;
1913
2251
  const keepEndLength = 20;
2252
+ // Split the message into words
1914
2253
  if (level !== 'spawn') {
1915
2254
  message = message
1916
2255
  .split(' ')
1917
2256
  .map((word) => {
2257
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1918
2258
  if (word.length > maxContinuousLength) {
1919
2259
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1920
2260
  }
@@ -1922,14 +2262,34 @@ export class Frontend extends EventEmitter {
1922
2262
  })
1923
2263
  .join(' ');
1924
2264
  }
2265
+ // Send the message to all connected clients
1925
2266
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1926
2267
  }
2268
+ /**
2269
+ * Sends a need to refresh WebSocket message to all connected clients.
2270
+ *
2271
+ * @param {string} changed - The changed value.
2272
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2273
+ * possible values for changed:
2274
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2275
+ * - 'plugins'
2276
+ * - 'devices'
2277
+ * - 'matter' with param 'matter' (QRDiv component)
2278
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2279
+ */
1927
2280
  wssSendRefreshRequired(changed, params) {
1928
2281
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1929
2282
  return;
1930
2283
  this.log.debug('Sending a refresh required message to all connected clients');
2284
+ // Send the message to all connected clients
1931
2285
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1932
2286
  }
2287
+ /**
2288
+ * Sends a need to restart WebSocket message to all connected clients.
2289
+ *
2290
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2291
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2292
+ */
1933
2293
  wssSendRestartRequired(snackbar = true, fixed = false) {
1934
2294
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1935
2295
  return;
@@ -1938,8 +2298,14 @@ export class Frontend extends EventEmitter {
1938
2298
  this.matterbridge.fixedRestartRequired = fixed;
1939
2299
  if (snackbar === true)
1940
2300
  this.wssSendSnackbarMessage(`Restart required`, 0);
2301
+ // Send the message to all connected clients
1941
2302
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1942
2303
  }
2304
+ /**
2305
+ * Sends a no need to restart WebSocket message to all connected clients.
2306
+ *
2307
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2308
+ */
1943
2309
  wssSendRestartNotRequired(snackbar = true) {
1944
2310
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1945
2311
  return;
@@ -1947,64 +2313,145 @@ export class Frontend extends EventEmitter {
1947
2313
  this.matterbridge.restartRequired = false;
1948
2314
  if (snackbar === true)
1949
2315
  this.wssSendCloseSnackbarMessage(`Restart required`);
2316
+ // Send the message to all connected clients
1950
2317
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1951
2318
  }
2319
+ /**
2320
+ * Sends a need to update WebSocket message to all connected clients.
2321
+ *
2322
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2323
+ */
1952
2324
  wssSendUpdateRequired(devVersion = false) {
1953
2325
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1954
2326
  return;
1955
2327
  this.log.debug('Sending an update required message to all connected clients');
1956
2328
  this.matterbridge.updateRequired = true;
2329
+ // Send the message to all connected clients
1957
2330
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1958
2331
  }
2332
+ /**
2333
+ * Sends a cpu update message to all connected clients.
2334
+ *
2335
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2336
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2337
+ */
1959
2338
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1960
2339
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1961
2340
  return;
2341
+ // istanbul ignore else
1962
2342
  if (hasParameter('debug'))
1963
2343
  this.log.debug('Sending a cpu update message to all connected clients');
2344
+ // Send the message to all connected clients
1964
2345
  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 } });
1965
2346
  }
2347
+ /**
2348
+ * Sends a memory update message to all connected clients.
2349
+ *
2350
+ * @param {string} totalMemory - The total memory in bytes.
2351
+ * @param {string} freeMemory - The free memory in bytes.
2352
+ * @param {string} rss - The resident set size in bytes.
2353
+ * @param {string} heapTotal - The total heap memory in bytes.
2354
+ * @param {string} heapUsed - The used heap memory in bytes.
2355
+ * @param {string} external - The external memory in bytes.
2356
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2357
+ */
1966
2358
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1967
2359
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1968
2360
  return;
2361
+ // istanbul ignore else
1969
2362
  if (hasParameter('debug'))
1970
2363
  this.log.debug('Sending a memory update message to all connected clients');
2364
+ // Send the message to all connected clients
1971
2365
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1972
2366
  }
2367
+ /**
2368
+ * Sends an uptime update message to all connected clients.
2369
+ *
2370
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2371
+ * @param {string} processUptime - The process uptime in a human-readable format.
2372
+ */
1973
2373
  wssSendUptimeUpdate(systemUptime, processUptime) {
1974
2374
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1975
2375
  return;
2376
+ // istanbul ignore else
1976
2377
  if (hasParameter('debug'))
1977
2378
  this.log.debug('Sending a uptime update message to all connected clients');
2379
+ // Send the message to all connected clients
1978
2380
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1979
2381
  }
2382
+ /**
2383
+ * Sends an open snackbar message to all connected clients.
2384
+ *
2385
+ * @param {string} message - The message to send.
2386
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2387
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2388
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2389
+ *
2390
+ * @remarks
2391
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2392
+ */
1980
2393
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1981
2394
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1982
2395
  return;
1983
2396
  this.log.debug('Sending a snackbar message to all connected clients');
2397
+ // Send the message to all connected clients
1984
2398
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
1985
2399
  }
2400
+ /**
2401
+ * Sends a close snackbar message to all connected clients.
2402
+ * It will close the snackbar message with the same message and timeout = 0.
2403
+ *
2404
+ * @param {string} message - The message to send.
2405
+ */
1986
2406
  wssSendCloseSnackbarMessage(message) {
1987
2407
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1988
2408
  return;
1989
2409
  this.log.debug('Sending a close snackbar message to all connected clients');
2410
+ // Send the message to all connected clients
1990
2411
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
1991
2412
  }
2413
+ /**
2414
+ * Sends an attribute update message to all connected WebSocket clients.
2415
+ *
2416
+ * @param {string | undefined} plugin - The name of the plugin.
2417
+ * @param {string | undefined} serialNumber - The serial number of the device.
2418
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2419
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2420
+ * @param {string} id - The endpoint id where the attribute belongs.
2421
+ * @param {string} cluster - The cluster name where the attribute belongs.
2422
+ * @param {string} attribute - The name of the attribute that changed.
2423
+ * @param {number | string | boolean} value - The new value of the attribute.
2424
+ *
2425
+ * @remarks
2426
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2427
+ * with the updated attribute information.
2428
+ */
1992
2429
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
1993
2430
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1994
2431
  return;
1995
2432
  this.log.debug('Sending an attribute update message to all connected clients');
2433
+ // Send the message to all connected clients
1996
2434
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
1997
2435
  }
2436
+ /**
2437
+ * Sends a message to all connected clients.
2438
+ * This is an helper function to send a broadcast message to all connected clients.
2439
+ *
2440
+ * @param {WsMessageBroadcast} msg - The message to send.
2441
+ */
1998
2442
  wssBroadcastMessage(msg) {
1999
2443
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2000
2444
  return;
2445
+ // Send the message to all connected clients
2001
2446
  const stringifiedMsg = JSON.stringify(msg);
2002
2447
  if (msg.method !== 'log')
2003
2448
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
2004
2449
  this.webSocketServer?.clients.forEach((client) => {
2450
+ // istanbul ignore else
2005
2451
  if (client.readyState === client.OPEN) {
2006
2452
  client.send(stringifiedMsg);
2007
2453
  }
2008
2454
  });
2009
2455
  }
2010
2456
  }
2457
+ //# sourceMappingURL=frontend.js.map