matterbridge 3.5.0-dev-20260119-f9ea00e → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/CHANGELOG.md +119 -117
  2. package/dist/broadcastServer.d.ts +115 -0
  3. package/dist/broadcastServer.d.ts.map +1 -0
  4. package/dist/broadcastServer.js +117 -0
  5. package/dist/broadcastServer.js.map +1 -0
  6. package/dist/broadcastServerTypes.d.ts +43 -0
  7. package/dist/broadcastServerTypes.d.ts.map +1 -0
  8. package/dist/broadcastServerTypes.js +24 -0
  9. package/dist/broadcastServerTypes.js.map +1 -0
  10. package/dist/cli.d.ts +24 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +97 -1
  13. package/dist/cli.js.map +1 -0
  14. package/dist/cliEmitter.d.ts +36 -0
  15. package/dist/cliEmitter.d.ts.map +1 -0
  16. package/dist/cliEmitter.js +37 -0
  17. package/dist/cliEmitter.js.map +1 -0
  18. package/dist/cliHistory.d.ts +42 -0
  19. package/dist/cliHistory.d.ts.map +1 -0
  20. package/dist/cliHistory.js +38 -0
  21. package/dist/cliHistory.js.map +1 -0
  22. package/dist/clusters/export.d.ts +1 -0
  23. package/dist/clusters/export.d.ts.map +1 -0
  24. package/dist/clusters/export.js +2 -0
  25. package/dist/clusters/export.js.map +1 -0
  26. package/dist/deviceManager.d.ts +108 -0
  27. package/dist/deviceManager.d.ts.map +1 -0
  28. package/dist/deviceManager.js +113 -1
  29. package/dist/deviceManager.js.map +1 -0
  30. package/dist/devices/airConditioner.d.ts +75 -0
  31. package/dist/devices/airConditioner.d.ts.map +1 -0
  32. package/dist/devices/airConditioner.js +57 -0
  33. package/dist/devices/airConditioner.js.map +1 -0
  34. package/dist/devices/batteryStorage.d.ts +43 -0
  35. package/dist/devices/batteryStorage.d.ts.map +1 -0
  36. package/dist/devices/batteryStorage.js +48 -1
  37. package/dist/devices/batteryStorage.js.map +1 -0
  38. package/dist/devices/cooktop.d.ts +55 -0
  39. package/dist/devices/cooktop.d.ts.map +1 -0
  40. package/dist/devices/cooktop.js +56 -0
  41. package/dist/devices/cooktop.js.map +1 -0
  42. package/dist/devices/dishwasher.d.ts +55 -0
  43. package/dist/devices/dishwasher.d.ts.map +1 -0
  44. package/dist/devices/dishwasher.js +57 -0
  45. package/dist/devices/dishwasher.js.map +1 -0
  46. package/dist/devices/evse.d.ts +57 -0
  47. package/dist/devices/evse.d.ts.map +1 -0
  48. package/dist/devices/evse.js +74 -10
  49. package/dist/devices/evse.js.map +1 -0
  50. package/dist/devices/export.d.ts +1 -0
  51. package/dist/devices/export.d.ts.map +1 -0
  52. package/dist/devices/export.js +5 -0
  53. package/dist/devices/export.js.map +1 -0
  54. package/dist/devices/extractorHood.d.ts +41 -0
  55. package/dist/devices/extractorHood.d.ts.map +1 -0
  56. package/dist/devices/extractorHood.js +43 -0
  57. package/dist/devices/extractorHood.js.map +1 -0
  58. package/dist/devices/heatPump.d.ts +43 -0
  59. package/dist/devices/heatPump.d.ts.map +1 -0
  60. package/dist/devices/heatPump.js +50 -2
  61. package/dist/devices/heatPump.js.map +1 -0
  62. package/dist/devices/laundryDryer.d.ts +58 -0
  63. package/dist/devices/laundryDryer.d.ts.map +1 -0
  64. package/dist/devices/laundryDryer.js +62 -3
  65. package/dist/devices/laundryDryer.js.map +1 -0
  66. package/dist/devices/laundryWasher.d.ts +64 -0
  67. package/dist/devices/laundryWasher.d.ts.map +1 -0
  68. package/dist/devices/laundryWasher.js +70 -4
  69. package/dist/devices/laundryWasher.js.map +1 -0
  70. package/dist/devices/microwaveOven.d.ts +77 -1
  71. package/dist/devices/microwaveOven.d.ts.map +1 -0
  72. package/dist/devices/microwaveOven.js +88 -5
  73. package/dist/devices/microwaveOven.js.map +1 -0
  74. package/dist/devices/oven.d.ts +82 -0
  75. package/dist/devices/oven.d.ts.map +1 -0
  76. package/dist/devices/oven.js +85 -0
  77. package/dist/devices/oven.js.map +1 -0
  78. package/dist/devices/refrigerator.d.ts +100 -0
  79. package/dist/devices/refrigerator.d.ts.map +1 -0
  80. package/dist/devices/refrigerator.js +102 -0
  81. package/dist/devices/refrigerator.js.map +1 -0
  82. package/dist/devices/roboticVacuumCleaner.d.ts +83 -0
  83. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  84. package/dist/devices/roboticVacuumCleaner.js +100 -9
  85. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  86. package/dist/devices/solarPower.d.ts +36 -0
  87. package/dist/devices/solarPower.d.ts.map +1 -0
  88. package/dist/devices/solarPower.js +38 -0
  89. package/dist/devices/solarPower.js.map +1 -0
  90. package/dist/devices/speaker.d.ts +79 -0
  91. package/dist/devices/speaker.d.ts.map +1 -0
  92. package/dist/devices/speaker.js +84 -0
  93. package/dist/devices/speaker.js.map +1 -0
  94. package/dist/devices/temperatureControl.d.ts +21 -0
  95. package/dist/devices/temperatureControl.d.ts.map +1 -0
  96. package/dist/devices/temperatureControl.js +24 -3
  97. package/dist/devices/temperatureControl.js.map +1 -0
  98. package/dist/devices/waterHeater.d.ts +74 -0
  99. package/dist/devices/waterHeater.d.ts.map +1 -0
  100. package/dist/devices/waterHeater.js +82 -2
  101. package/dist/devices/waterHeater.js.map +1 -0
  102. package/dist/dgram/coap.d.ts +171 -0
  103. package/dist/dgram/coap.d.ts.map +1 -0
  104. package/dist/dgram/coap.js +126 -13
  105. package/dist/dgram/coap.js.map +1 -0
  106. package/dist/dgram/dgram.d.ts +99 -0
  107. package/dist/dgram/dgram.d.ts.map +1 -0
  108. package/dist/dgram/dgram.js +114 -2
  109. package/dist/dgram/dgram.js.map +1 -0
  110. package/dist/dgram/mb_coap.d.ts +23 -0
  111. package/dist/dgram/mb_coap.d.ts.map +1 -0
  112. package/dist/dgram/mb_coap.js +41 -3
  113. package/dist/dgram/mb_coap.js.map +1 -0
  114. package/dist/dgram/mb_mdns.d.ts +23 -0
  115. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  116. package/dist/dgram/mb_mdns.js +80 -24
  117. package/dist/dgram/mb_mdns.js.map +1 -0
  118. package/dist/dgram/mdns.d.ts +187 -4
  119. package/dist/dgram/mdns.d.ts.map +1 -0
  120. package/dist/dgram/mdns.js +371 -139
  121. package/dist/dgram/mdns.js.map +1 -0
  122. package/dist/dgram/multicast.d.ts +49 -0
  123. package/dist/dgram/multicast.d.ts.map +1 -0
  124. package/dist/dgram/multicast.js +62 -1
  125. package/dist/dgram/multicast.js.map +1 -0
  126. package/dist/dgram/unicast.d.ts +53 -0
  127. package/dist/dgram/unicast.d.ts.map +1 -0
  128. package/dist/dgram/unicast.js +60 -0
  129. package/dist/dgram/unicast.js.map +1 -0
  130. package/dist/frontend.d.ts +187 -0
  131. package/dist/frontend.d.ts.map +1 -0
  132. package/dist/frontend.js +498 -37
  133. package/dist/frontend.js.map +1 -0
  134. package/dist/frontendTypes.d.ts +57 -0
  135. package/dist/frontendTypes.d.ts.map +1 -0
  136. package/dist/frontendTypes.js +45 -0
  137. package/dist/frontendTypes.js.map +1 -0
  138. package/dist/helpers.d.ts +43 -0
  139. package/dist/helpers.d.ts.map +1 -0
  140. package/dist/helpers.js +53 -0
  141. package/dist/helpers.js.map +1 -0
  142. package/dist/index.d.ts +23 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +25 -0
  145. package/dist/index.js.map +1 -0
  146. package/dist/jestutils/export.d.ts +1 -0
  147. package/dist/jestutils/export.d.ts.map +1 -0
  148. package/dist/jestutils/export.js +1 -0
  149. package/dist/jestutils/export.js.map +1 -0
  150. package/dist/jestutils/jestHelpers.d.ts +255 -0
  151. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  152. package/dist/jestutils/jestHelpers.js +372 -14
  153. package/dist/jestutils/jestHelpers.js.map +1 -0
  154. package/dist/logger/export.d.ts +1 -0
  155. package/dist/logger/export.d.ts.map +1 -0
  156. package/dist/logger/export.js +1 -0
  157. package/dist/logger/export.js.map +1 -0
  158. package/dist/matter/behaviors.d.ts +1 -0
  159. package/dist/matter/behaviors.d.ts.map +1 -0
  160. package/dist/matter/behaviors.js +2 -0
  161. package/dist/matter/behaviors.js.map +1 -0
  162. package/dist/matter/clusters.d.ts +1 -0
  163. package/dist/matter/clusters.d.ts.map +1 -0
  164. package/dist/matter/clusters.js +2 -0
  165. package/dist/matter/clusters.js.map +1 -0
  166. package/dist/matter/devices.d.ts +1 -0
  167. package/dist/matter/devices.d.ts.map +1 -0
  168. package/dist/matter/devices.js +2 -0
  169. package/dist/matter/devices.js.map +1 -0
  170. package/dist/matter/endpoints.d.ts +1 -0
  171. package/dist/matter/endpoints.d.ts.map +1 -0
  172. package/dist/matter/endpoints.js +2 -0
  173. package/dist/matter/endpoints.js.map +1 -0
  174. package/dist/matter/export.d.ts +1 -0
  175. package/dist/matter/export.d.ts.map +1 -0
  176. package/dist/matter/export.js +2 -0
  177. package/dist/matter/export.js.map +1 -0
  178. package/dist/matter/types.d.ts +1 -0
  179. package/dist/matter/types.d.ts.map +1 -0
  180. package/dist/matter/types.js +2 -0
  181. package/dist/matter/types.js.map +1 -0
  182. package/dist/matterNode.d.ts +258 -0
  183. package/dist/matterNode.d.ts.map +1 -0
  184. package/dist/matterNode.js +359 -8
  185. package/dist/matterNode.js.map +1 -0
  186. package/dist/matterbridge.d.ts +362 -0
  187. package/dist/matterbridge.d.ts.map +1 -0
  188. package/dist/matterbridge.js +842 -46
  189. package/dist/matterbridge.js.map +1 -0
  190. package/dist/matterbridgeAccessoryPlatform.d.ts +36 -0
  191. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  192. package/dist/matterbridgeAccessoryPlatform.js +38 -0
  193. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  194. package/dist/matterbridgeBehaviors.d.ts +24 -0
  195. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  196. package/dist/matterbridgeBehaviors.js +68 -5
  197. package/dist/matterbridgeBehaviors.js.map +1 -0
  198. package/dist/matterbridgeDeviceTypes.d.ts +649 -0
  199. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  200. package/dist/matterbridgeDeviceTypes.js +673 -6
  201. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  202. package/dist/matterbridgeDynamicPlatform.d.ts +36 -0
  203. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  204. package/dist/matterbridgeDynamicPlatform.js +38 -0
  205. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  206. package/dist/matterbridgeEndpoint.d.ts +1332 -0
  207. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  208. package/dist/matterbridgeEndpoint.js +1457 -53
  209. package/dist/matterbridgeEndpoint.js.map +1 -0
  210. package/dist/matterbridgeEndpointHelpers.d.ts +425 -0
  211. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  212. package/dist/matterbridgeEndpointHelpers.js +483 -20
  213. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  214. package/dist/matterbridgeEndpointTypes.d.ts +70 -0
  215. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  216. package/dist/matterbridgeEndpointTypes.js +25 -0
  217. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  218. package/dist/matterbridgePlatform.d.ts +425 -0
  219. package/dist/matterbridgePlatform.d.ts.map +1 -0
  220. package/dist/matterbridgePlatform.js +451 -1
  221. package/dist/matterbridgePlatform.js.map +1 -0
  222. package/dist/matterbridgeTypes.d.ts +46 -0
  223. package/dist/matterbridgeTypes.d.ts.map +1 -0
  224. package/dist/matterbridgeTypes.js +26 -0
  225. package/dist/matterbridgeTypes.js.map +1 -0
  226. package/dist/pluginManager.d.ts +305 -0
  227. package/dist/pluginManager.d.ts.map +1 -0
  228. package/dist/pluginManager.js +341 -5
  229. package/dist/pluginManager.js.map +1 -0
  230. package/dist/shelly.d.ts +157 -0
  231. package/dist/shelly.d.ts.map +1 -0
  232. package/dist/shelly.js +178 -7
  233. package/dist/shelly.js.map +1 -0
  234. package/dist/storage/export.d.ts +1 -0
  235. package/dist/storage/export.d.ts.map +1 -0
  236. package/dist/storage/export.js +1 -0
  237. package/dist/storage/export.js.map +1 -0
  238. package/dist/update.d.ts +75 -0
  239. package/dist/update.d.ts.map +1 -0
  240. package/dist/update.js +93 -1
  241. package/dist/update.js.map +1 -0
  242. package/dist/utils/colorUtils.d.ts +77 -0
  243. package/dist/utils/colorUtils.d.ts.map +1 -0
  244. package/dist/utils/colorUtils.js +97 -2
  245. package/dist/utils/colorUtils.js.map +1 -0
  246. package/dist/utils/commandLine.d.ts +60 -0
  247. package/dist/utils/commandLine.d.ts.map +1 -0
  248. package/dist/utils/commandLine.js +60 -0
  249. package/dist/utils/commandLine.js.map +1 -0
  250. package/dist/utils/copyDirectory.d.ts +33 -0
  251. package/dist/utils/copyDirectory.d.ts.map +1 -0
  252. package/dist/utils/copyDirectory.js +37 -0
  253. package/dist/utils/copyDirectory.js.map +1 -0
  254. package/dist/utils/createDirectory.d.ts +32 -0
  255. package/dist/utils/createDirectory.d.ts.map +1 -0
  256. package/dist/utils/createDirectory.js +33 -0
  257. package/dist/utils/createDirectory.js.map +1 -0
  258. package/dist/utils/createZip.d.ts +38 -0
  259. package/dist/utils/createZip.d.ts.map +1 -0
  260. package/dist/utils/createZip.js +47 -2
  261. package/dist/utils/createZip.js.map +1 -0
  262. package/dist/utils/deepCopy.d.ts +31 -0
  263. package/dist/utils/deepCopy.d.ts.map +1 -0
  264. package/dist/utils/deepCopy.js +39 -0
  265. package/dist/utils/deepCopy.js.map +1 -0
  266. package/dist/utils/deepEqual.d.ts +53 -0
  267. package/dist/utils/deepEqual.d.ts.map +1 -0
  268. package/dist/utils/deepEqual.js +72 -1
  269. package/dist/utils/deepEqual.js.map +1 -0
  270. package/dist/utils/error.d.ts +42 -0
  271. package/dist/utils/error.d.ts.map +1 -0
  272. package/dist/utils/error.js +42 -0
  273. package/dist/utils/error.js.map +1 -0
  274. package/dist/utils/export.d.ts +1 -0
  275. package/dist/utils/export.d.ts.map +1 -0
  276. package/dist/utils/export.js +1 -0
  277. package/dist/utils/export.js.map +1 -0
  278. package/dist/utils/format.d.ts +49 -0
  279. package/dist/utils/format.d.ts.map +1 -0
  280. package/dist/utils/format.js +49 -0
  281. package/dist/utils/format.js.map +1 -0
  282. package/dist/utils/hex.d.ts +85 -0
  283. package/dist/utils/hex.d.ts.map +1 -0
  284. package/dist/utils/hex.js +124 -0
  285. package/dist/utils/hex.js.map +1 -0
  286. package/dist/utils/inspector.d.ts +63 -0
  287. package/dist/utils/inspector.d.ts.map +1 -0
  288. package/dist/utils/inspector.js +69 -1
  289. package/dist/utils/inspector.js.map +1 -0
  290. package/dist/utils/isValid.d.ts +93 -0
  291. package/dist/utils/isValid.d.ts.map +1 -0
  292. package/dist/utils/isValid.js +93 -0
  293. package/dist/utils/isValid.js.map +1 -0
  294. package/dist/utils/network.d.ts +116 -0
  295. package/dist/utils/network.d.ts.map +1 -0
  296. package/dist/utils/network.js +126 -5
  297. package/dist/utils/network.js.map +1 -0
  298. package/dist/utils/spawn.d.ts +32 -0
  299. package/dist/utils/spawn.d.ts.map +1 -0
  300. package/dist/utils/spawn.js +71 -1
  301. package/dist/utils/spawn.js.map +1 -0
  302. package/dist/utils/tracker.d.ts +56 -0
  303. package/dist/utils/tracker.d.ts.map +1 -0
  304. package/dist/utils/tracker.js +64 -1
  305. package/dist/utils/tracker.js.map +1 -0
  306. package/dist/utils/wait.d.ts +51 -0
  307. package/dist/utils/wait.d.ts.map +1 -0
  308. package/dist/utils/wait.js +60 -8
  309. package/dist/utils/wait.js.map +1 -0
  310. package/dist/workerGlobalPrefix.d.ts +24 -0
  311. package/dist/workerGlobalPrefix.d.ts.map +1 -0
  312. package/dist/workerGlobalPrefix.js +37 -5
  313. package/dist/workerGlobalPrefix.js.map +1 -0
  314. package/dist/workerTypes.d.ts +25 -0
  315. package/dist/workerTypes.d.ts.map +1 -0
  316. package/dist/workerTypes.js +24 -0
  317. package/dist/workerTypes.js.map +1 -0
  318. package/dist/workers.d.ts +61 -0
  319. package/dist/workers.d.ts.map +1 -0
  320. package/dist/workers.js +68 -4
  321. package/dist/workers.js.map +1 -0
  322. package/frontend/build/assets/index.js +4 -4
  323. package/frontend/package.json +1 -1
  324. package/npm-shrinkwrap.json +5 -35
  325. package/package.json +7 -7
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,7 +269,9 @@ 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')) {
274
+ // We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
201
275
  this.httpServer.listen(this.port, '0.0.0.0', () => {
202
276
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
203
277
  this.listening = true;
@@ -205,13 +279,17 @@ export class Frontend extends EventEmitter {
205
279
  });
206
280
  }
207
281
  else {
282
+ // We listen to all available addresses
208
283
  this.httpServer.listen(this.port, getParameter('bind'), () => {
209
284
  const addr = this.httpServer?.address();
285
+ // istanbul ignore else
210
286
  if (addr && typeof addr !== 'string') {
211
287
  this.log.info(`The frontend http server is bound to ${addr.family} ${addr.address}:${addr.port}`);
212
288
  }
289
+ // istanbul ignore else
213
290
  if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
214
291
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
292
+ // istanbul ignore else
215
293
  if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
216
294
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
217
295
  this.listening = true;
@@ -220,24 +298,30 @@ export class Frontend extends EventEmitter {
220
298
  }
221
299
  this.httpServer.on('upgrade', async (req, socket, head) => {
222
300
  try {
301
+ // Only proceed for real WebSocket upgrades
302
+ // istanbul ignore next cause is only a safety check
223
303
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
224
304
  this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
225
305
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
226
306
  return socket.destroy();
227
307
  }
308
+ // Build a URL so we can read ?password=...
228
309
  const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
310
+ // Validate WebSocket password
229
311
  const password = url.searchParams.get('password') ?? '';
230
312
  if (password !== this.storedPassword) {
231
313
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
232
314
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
233
315
  return socket.destroy();
234
316
  }
317
+ // Complete the WebSocket handshake
235
318
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
236
319
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
237
320
  this.webSocketServer?.emit('connection', ws, req);
238
321
  });
239
322
  }
240
323
  catch (err) {
324
+ /* istanbul ignore next: only triggered on unexpected internal error */
241
325
  {
242
326
  inspectError(this.log, 'WebSocket upgrade error:', err);
243
327
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -260,6 +344,7 @@ export class Frontend extends EventEmitter {
260
344
  });
261
345
  }
262
346
  else {
347
+ // SSL is enabled, load the certificate and the private key
263
348
  let cert;
264
349
  let key;
265
350
  let ca;
@@ -269,6 +354,7 @@ export class Frontend extends EventEmitter {
269
354
  let httpsServerOptions = {};
270
355
  const fs = await import('node:fs');
271
356
  if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
357
+ // Load the p12 certificate and the passphrase
272
358
  try {
273
359
  pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
274
360
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
@@ -280,7 +366,7 @@ export class Frontend extends EventEmitter {
280
366
  }
281
367
  try {
282
368
  passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
283
- passphrase = passphrase.trim();
369
+ passphrase = passphrase.trim(); // Ensure no extra characters
284
370
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
285
371
  }
286
372
  catch (error) {
@@ -291,6 +377,7 @@ export class Frontend extends EventEmitter {
291
377
  httpsServerOptions = { pfx, passphrase };
292
378
  }
293
379
  else {
380
+ // 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.
294
381
  try {
295
382
  cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
296
383
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
@@ -320,9 +407,10 @@ export class Frontend extends EventEmitter {
320
407
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
321
408
  }
322
409
  if (hasParameter('mtls')) {
323
- httpsServerOptions.requestCert = true;
324
- httpsServerOptions.rejectUnauthorized = true;
410
+ httpsServerOptions.requestCert = true; // Request client certificate
411
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
325
412
  }
413
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
326
414
  const https = await import('node:https');
327
415
  try {
328
416
  this.log.debug(`Creating HTTPS server...`);
@@ -333,7 +421,9 @@ export class Frontend extends EventEmitter {
333
421
  this.emit('server_error', error);
334
422
  return;
335
423
  }
424
+ // Listen on the specified port
336
425
  if (hasParameter('ingress')) {
426
+ // We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
337
427
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
338
428
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
339
429
  this.listening = true;
@@ -341,13 +431,17 @@ export class Frontend extends EventEmitter {
341
431
  });
342
432
  }
343
433
  else {
434
+ // We listen to all available addresses
344
435
  this.httpsServer.listen(this.port, getParameter('bind'), () => {
345
436
  const addr = this.httpsServer?.address();
437
+ // istanbul ignore else
346
438
  if (addr && typeof addr !== 'string') {
347
439
  this.log.info(`The frontend https server is bound to ${addr.family} ${addr.address}:${addr.port}`);
348
440
  }
441
+ // istanbul ignore else
349
442
  if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
350
443
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
444
+ // istanbul ignore else
351
445
  if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
352
446
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
353
447
  this.listening = true;
@@ -356,23 +450,29 @@ export class Frontend extends EventEmitter {
356
450
  }
357
451
  this.httpsServer.on('upgrade', async (req, socket, head) => {
358
452
  try {
453
+ // Only proceed for real WebSocket upgrades
454
+ // istanbul ignore next cause is only a safety check
359
455
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
360
456
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
361
457
  return socket.destroy();
362
458
  }
459
+ // Build a URL so we can read ?password=...
363
460
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
461
+ // Validate WebSocket password
364
462
  const password = url.searchParams.get('password') ?? '';
365
463
  if (password !== this.storedPassword) {
366
464
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
367
465
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
368
466
  return socket.destroy();
369
467
  }
468
+ // Complete the WebSocket handshake
370
469
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
371
470
  this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
372
471
  this.webSocketServer?.emit('connection', ws, req);
373
472
  });
374
473
  }
375
474
  catch (err) {
475
+ /* istanbul ignore next: only triggered on unexpected internal error */
376
476
  {
377
477
  inspectError(this.log, 'WebSocket upgrade error:', err);
378
478
  socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
@@ -394,6 +494,7 @@ export class Frontend extends EventEmitter {
394
494
  return;
395
495
  });
396
496
  }
497
+ // Subscribe to cli events
397
498
  cliEmitter.removeAllListeners();
398
499
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
399
500
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -404,6 +505,8 @@ export class Frontend extends EventEmitter {
404
505
  cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
405
506
  this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
406
507
  });
508
+ // Endpoint to validate login code
509
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
407
510
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
408
511
  const { password } = req.body;
409
512
  this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
@@ -416,17 +519,20 @@ export class Frontend extends EventEmitter {
416
519
  res.json({ valid: false });
417
520
  }
418
521
  });
522
+ // Endpoint to provide health check for docker
419
523
  this.expressApp.get('/health', (req, res) => {
420
524
  this.log.debug('Express received /health');
421
525
  const healthStatus = {
422
- status: 'ok',
423
- uptime: process.uptime(),
424
- timestamp: new Date().toISOString(),
526
+ status: 'ok', // Indicate service is healthy
527
+ uptime: process.uptime(), // Server uptime in seconds
528
+ timestamp: new Date().toISOString(), // Current timestamp
425
529
  };
426
530
  res.status(200).json(healthStatus);
427
531
  });
532
+ // Endpoint to provide memory usage details
428
533
  this.expressApp.get('/memory', async (req, res) => {
429
534
  this.log.debug('Express received /memory');
535
+ // Memory usage from process
430
536
  const memoryUsageRaw = process.memoryUsage();
431
537
  const memoryUsage = {
432
538
  rss: formatBytes(memoryUsageRaw.rss),
@@ -435,10 +541,13 @@ export class Frontend extends EventEmitter {
435
541
  external: formatBytes(memoryUsageRaw.external),
436
542
  arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
437
543
  };
544
+ // V8 heap statistics
438
545
  const { default: v8 } = await import('node:v8');
439
546
  const heapStatsRaw = v8.getHeapStatistics();
440
547
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
548
+ // Format heapStats
441
549
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
550
+ // Format heapSpaces
442
551
  const heapSpaces = heapSpacesRaw.map((space) => ({
443
552
  ...space,
444
553
  space_size: formatBytes(space.space_size),
@@ -457,18 +566,22 @@ export class Frontend extends EventEmitter {
457
566
  };
458
567
  res.status(200).json(memoryReport);
459
568
  });
569
+ // Endpoint to provide settings
460
570
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
461
571
  this.log.debug('The frontend sent /api/settings');
462
572
  res.json(await this.getApiSettings());
463
573
  });
574
+ // Endpoint to provide plugins
464
575
  this.expressApp.get('/api/plugins', async (req, res) => {
465
576
  this.log.debug('The frontend sent /api/plugins');
466
577
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
467
578
  });
579
+ // Endpoint to provide devices
468
580
  this.expressApp.get('/api/devices', async (req, res) => {
469
581
  this.log.debug('The frontend sent /api/devices');
470
582
  res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
471
583
  });
584
+ // Endpoint to view the matterbridge log
472
585
  this.expressApp.get('/api/view-mblog', async (req, res) => {
473
586
  this.log.debug('The frontend sent /api/view-mblog');
474
587
  try {
@@ -482,6 +595,7 @@ export class Frontend extends EventEmitter {
482
595
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
483
596
  }
484
597
  });
598
+ // Endpoint to view the matter.js log
485
599
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
486
600
  this.log.debug('The frontend sent /api/view-mjlog');
487
601
  try {
@@ -495,6 +609,7 @@ export class Frontend extends EventEmitter {
495
609
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
496
610
  }
497
611
  });
612
+ // Endpoint to view the diagnostic.log
498
613
  this.expressApp.get('/api/view-diagnostic', async (req, res) => {
499
614
  this.log.debug('The frontend sent /api/view-diagnostic');
500
615
  await this.generateDiagnostic();
@@ -505,10 +620,13 @@ export class Frontend extends EventEmitter {
505
620
  res.send(data.slice(29));
506
621
  }
507
622
  catch (error) {
623
+ // istanbul ignore next
508
624
  this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
625
+ // istanbul ignore next
509
626
  res.status(500).send('Error reading diagnostic log file.');
510
627
  }
511
628
  });
629
+ // Endpoint to download the diagnostic.log
512
630
  this.expressApp.get('/api/download-diagnostic', async (req, res) => {
513
631
  this.log.debug(`The frontend sent /api/download-diagnostic`);
514
632
  await this.generateDiagnostic();
@@ -519,16 +637,19 @@ export class Frontend extends EventEmitter {
519
637
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
520
638
  }
521
639
  catch (error) {
640
+ // istanbul ignore next
522
641
  this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
523
642
  }
524
643
  res.type('text/plain');
525
644
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
645
+ /* istanbul ignore if */
526
646
  if (error) {
527
647
  this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
528
648
  res.status(500).send('Error downloading the diagnostic log file');
529
649
  }
530
650
  });
531
651
  });
652
+ // Endpoint to view the history.html
532
653
  this.expressApp.get('/api/viewhistory', async (req, res) => {
533
654
  this.log.debug('The frontend sent /api/viewhistory');
534
655
  try {
@@ -542,6 +663,7 @@ export class Frontend extends EventEmitter {
542
663
  res.status(500).send('Error reading history file.');
543
664
  }
544
665
  });
666
+ // Endpoint to download the history.html
545
667
  this.expressApp.get('/api/downloadhistory', async (req, res) => {
546
668
  this.log.debug(`The frontend sent /api/downloadhistory`);
547
669
  try {
@@ -551,6 +673,7 @@ export class Frontend extends EventEmitter {
551
673
  await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
552
674
  res.type('text/plain');
553
675
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
676
+ /* istanbul ignore if */
554
677
  if (error) {
555
678
  this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
556
679
  res.status(500).send('Error downloading history file');
@@ -562,6 +685,7 @@ export class Frontend extends EventEmitter {
562
685
  res.status(500).send('Error reading history file.');
563
686
  }
564
687
  });
688
+ // Endpoint to view the shelly log
565
689
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
566
690
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
567
691
  try {
@@ -575,6 +699,7 @@ export class Frontend extends EventEmitter {
575
699
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
576
700
  }
577
701
  });
702
+ // Endpoint to download the matterbridge log
578
703
  this.expressApp.get('/api/download-mblog', async (req, res) => {
579
704
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
580
705
  const fs = await import('node:fs');
@@ -589,12 +714,14 @@ export class Frontend extends EventEmitter {
589
714
  }
590
715
  res.type('text/plain');
591
716
  res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
717
+ /* istanbul ignore if */
592
718
  if (error) {
593
719
  this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
594
720
  res.status(500).send('Error downloading the matterbridge log file');
595
721
  }
596
722
  });
597
723
  });
724
+ // Endpoint to download the matter log
598
725
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
599
726
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
600
727
  const fs = await import('node:fs');
@@ -609,12 +736,14 @@ export class Frontend extends EventEmitter {
609
736
  }
610
737
  res.type('text/plain');
611
738
  res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
739
+ /* istanbul ignore if */
612
740
  if (error) {
613
741
  this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
614
742
  res.status(500).send('Error downloading the matter log file');
615
743
  }
616
744
  });
617
745
  });
746
+ // Endpoint to download the shelly log
618
747
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
619
748
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
620
749
  const fs = await import('node:fs');
@@ -629,75 +758,91 @@ export class Frontend extends EventEmitter {
629
758
  }
630
759
  res.type('text/plain');
631
760
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
761
+ /* istanbul ignore if */
632
762
  if (error) {
633
763
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
634
764
  res.status(500).send('Error downloading Shelly system log file');
635
765
  }
636
766
  });
637
767
  });
768
+ // Endpoint to download the matterbridge storage directory
638
769
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
639
770
  this.log.debug('The frontend sent /api/download-mbstorage');
640
771
  await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
641
772
  res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
773
+ /* istanbul ignore if */
642
774
  if (error) {
643
775
  this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
644
776
  res.status(500).send('Error downloading the matterbridge storage file');
645
777
  }
646
778
  });
647
779
  });
780
+ // Endpoint to download the matter storage file
648
781
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
649
782
  this.log.debug('The frontend sent /api/download-mjstorage');
650
783
  await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
651
784
  res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
785
+ /* istanbul ignore if */
652
786
  if (error) {
653
787
  this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
654
788
  res.status(500).send('Error downloading the matter storage zip file');
655
789
  }
656
790
  });
657
791
  });
792
+ // Endpoint to download the matterbridge plugin directory
658
793
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
659
794
  this.log.debug('The frontend sent /api/download-pluginstorage');
660
795
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
661
796
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
797
+ /* istanbul ignore if */
662
798
  if (error) {
663
799
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
664
800
  res.status(500).send('Error downloading the matterbridge plugin storage file');
665
801
  }
666
802
  });
667
803
  });
804
+ // Endpoint to download the matterbridge plugin config files
668
805
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
669
806
  this.log.debug('The frontend sent /api/download-pluginconfig');
670
807
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
671
808
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
809
+ /* istanbul ignore if */
672
810
  if (error) {
673
811
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
674
812
  res.status(500).send('Error downloading the matterbridge plugin config file');
675
813
  }
676
814
  });
677
815
  });
816
+ // Endpoint to download the matterbridge backup (created with the backup command)
678
817
  this.expressApp.get('/api/download-backup', async (req, res) => {
679
818
  this.log.debug('The frontend sent /api/download-backup');
680
819
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
820
+ /* istanbul ignore if */
681
821
  if (error) {
682
822
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
683
823
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
684
824
  }
685
825
  });
686
826
  });
827
+ // Endpoint to upload a package
687
828
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
688
829
  const { filename } = req.body;
689
830
  const file = req.file;
831
+ /* istanbul ignore if */
690
832
  if (!file || !filename) {
691
833
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
692
834
  res.status(400).send('Invalid request: file and filename are required');
693
835
  return;
694
836
  }
695
837
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
838
+ // Define the path where the plugin file will be saved
696
839
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
697
840
  try {
841
+ // Move the uploaded file to the specified path
698
842
  const fs = await import('node:fs');
699
843
  await fs.promises.rename(file.path, filePath);
700
844
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
845
+ // Install the plugin package
701
846
  if (filename.endsWith('.tgz')) {
702
847
  const { spawnCommand } = await import('./utils/spawn.js');
703
848
  if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
@@ -725,6 +870,7 @@ export class Frontend extends EventEmitter {
725
870
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
726
871
  }
727
872
  });
873
+ // Fallback for routing (must be the last route)
728
874
  this.expressApp.use((req, res) => {
729
875
  const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
730
876
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
@@ -735,13 +881,16 @@ export class Frontend extends EventEmitter {
735
881
  async stop() {
736
882
  this.log.debug('Stopping the frontend...');
737
883
  const ws = await import('ws');
884
+ // Remove listeners from the express app
738
885
  if (this.expressApp) {
739
886
  this.expressApp.removeAllListeners();
740
887
  this.expressApp = undefined;
741
888
  this.log.debug('Frontend app closed successfully');
742
889
  }
890
+ // Close the WebSocket server
743
891
  if (this.webSocketServer) {
744
892
  this.log.debug('Closing WebSocket server...');
893
+ // Close all active connections
745
894
  this.webSocketServer.clients.forEach((client) => {
746
895
  if (client.readyState === ws.WebSocket.OPEN) {
747
896
  client.close();
@@ -749,7 +898,9 @@ export class Frontend extends EventEmitter {
749
898
  });
750
899
  await withTimeout(new Promise((resolve) => {
751
900
  this.webSocketServer?.close((error) => {
901
+ // istanbul ignore if
752
902
  if (error) {
903
+ // istanbul ignore next
753
904
  this.log.error(`Error closing WebSocket server: ${error}`);
754
905
  }
755
906
  else {
@@ -762,8 +913,27 @@ export class Frontend extends EventEmitter {
762
913
  this.webSocketServer.removeAllListeners();
763
914
  this.webSocketServer = undefined;
764
915
  }
916
+ // Close the http server
765
917
  if (this.httpServer) {
766
918
  this.log.debug('Closing http server...');
919
+ /*
920
+ await withTimeout(
921
+ new Promise<void>((resolve) => {
922
+ this.httpServer?.close((error) => {
923
+ if (error) {
924
+ // istanbul ignore next
925
+ this.log.error(`Error closing http server: ${error}`);
926
+ } else {
927
+ this.log.debug('Http server closed successfully');
928
+ this.emit('server_stopped');
929
+ }
930
+ resolve();
931
+ });
932
+ }),
933
+ 5000,
934
+ false,
935
+ );
936
+ */
767
937
  this.httpServer.close();
768
938
  this.log.debug('Http server closed successfully');
769
939
  this.listening = false;
@@ -772,8 +942,27 @@ export class Frontend extends EventEmitter {
772
942
  this.httpServer = undefined;
773
943
  this.log.debug('Frontend http server closed successfully');
774
944
  }
945
+ // Close the https server
775
946
  if (this.httpsServer) {
776
947
  this.log.debug('Closing https server...');
948
+ /*
949
+ await withTimeout(
950
+ new Promise<void>((resolve) => {
951
+ this.httpsServer?.close((error) => {
952
+ if (error) {
953
+ // istanbul ignore next
954
+ this.log.error(`Error closing https server: ${error}`);
955
+ } else {
956
+ this.log.debug('Https server closed successfully');
957
+ this.emit('server_stopped');
958
+ }
959
+ resolve();
960
+ });
961
+ }),
962
+ 5000,
963
+ false,
964
+ );
965
+ */
777
966
  this.httpsServer.close();
778
967
  this.log.debug('Https server closed successfully');
779
968
  this.listening = false;
@@ -784,7 +973,13 @@ export class Frontend extends EventEmitter {
784
973
  }
785
974
  this.log.debug('Frontend stopped successfully');
786
975
  }
976
+ /**
977
+ * Retrieves the api settings data.
978
+ *
979
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
980
+ */
787
981
  async getApiSettings() {
982
+ // Update the variable system information properties
788
983
  this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
789
984
  this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
790
985
  this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
@@ -794,6 +989,7 @@ export class Frontend extends EventEmitter {
794
989
  this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
795
990
  this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
796
991
  this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
992
+ // Create the matterbridge information
797
993
  const info = {
798
994
  homeDirectory: this.matterbridge.homeDirectory,
799
995
  rootDirectory: this.matterbridge.rootDirectory,
@@ -829,9 +1025,15 @@ export class Frontend extends EventEmitter {
829
1025
  };
830
1026
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
831
1027
  }
1028
+ /**
1029
+ * Retrieves the reachable attribute.
1030
+ *
1031
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
1032
+ * @returns {boolean} The reachable attribute.
1033
+ */
832
1034
  getReachability(device) {
833
1035
  if (this.matterbridge.hasCleanupStarted)
834
- return false;
1036
+ return false; // Skip if cleanup has started
835
1037
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
836
1038
  return false;
837
1039
  if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
@@ -842,9 +1044,15 @@ export class Frontend extends EventEmitter {
842
1044
  return true;
843
1045
  return false;
844
1046
  }
1047
+ /**
1048
+ * Retrieves the power source attribute.
1049
+ *
1050
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1051
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
1052
+ */
845
1053
  getPowerSource(endpoint) {
846
1054
  if (this.matterbridge.hasCleanupStarted)
847
- return undefined;
1055
+ return undefined; // Skip if cleanup has started
848
1056
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
849
1057
  return undefined;
850
1058
  const powerSource = (device) => {
@@ -859,16 +1067,25 @@ export class Frontend extends EventEmitter {
859
1067
  }
860
1068
  return;
861
1069
  };
1070
+ // Root endpoint
862
1071
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
863
1072
  return powerSource(endpoint);
1073
+ // Child endpoints
864
1074
  for (const child of endpoint.getChildEndpoints()) {
1075
+ // istanbul ignore else
865
1076
  if (child.hasClusterServer(PowerSource.Cluster.id))
866
1077
  return powerSource(child);
867
1078
  }
868
1079
  }
1080
+ /**
1081
+ * Retrieves the battery level attribute.
1082
+ *
1083
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
1084
+ * @returns {number | undefined} The battery level attribute.
1085
+ */
869
1086
  getBatteryLevel(endpoint) {
870
1087
  if (this.matterbridge.hasCleanupStarted)
871
- return undefined;
1088
+ return undefined; // Skip if cleanup has started
872
1089
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
873
1090
  return undefined;
874
1091
  const batteryLevel = (device) => {
@@ -879,16 +1096,27 @@ export class Frontend extends EventEmitter {
879
1096
  }
880
1097
  return undefined;
881
1098
  };
1099
+ // Root endpoint
882
1100
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
883
1101
  return batteryLevel(endpoint);
1102
+ // Child endpoints
884
1103
  for (const child of endpoint.getChildEndpoints()) {
1104
+ // istanbul ignore else
885
1105
  if (child.hasClusterServer(PowerSource.Cluster.id))
886
1106
  return batteryLevel(child);
887
1107
  }
888
1108
  }
1109
+ /**
1110
+ * Retrieves the cluster text description from a given device.
1111
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
1112
+ *
1113
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
1114
+ * @returns {string} The attributes description of the cluster servers in the device.
1115
+ */
889
1116
  getClusterTextFromDevice(device) {
890
1117
  if (this.matterbridge.hasCleanupStarted)
891
- return '';
1118
+ return ''; // Skip if cleanup has started
1119
+ // istanbul ignore else
892
1120
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
893
1121
  return '';
894
1122
  const getUserLabel = (device) => {
@@ -898,6 +1126,7 @@ export class Frontend extends EventEmitter {
898
1126
  if (composed)
899
1127
  return 'Composed: ' + composed.value;
900
1128
  }
1129
+ // istanbul ignore next cause is not reachable
901
1130
  return '';
902
1131
  };
903
1132
  const getFixedLabel = (device) => {
@@ -907,11 +1136,13 @@ export class Frontend extends EventEmitter {
907
1136
  if (composed)
908
1137
  return 'Composed: ' + composed.value;
909
1138
  }
1139
+ // istanbul ignore next cause is not reacheable
910
1140
  return '';
911
1141
  };
912
1142
  let attributes = '';
913
1143
  let supportedModes = [];
914
1144
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1145
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
915
1146
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
916
1147
  return;
917
1148
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -1001,11 +1232,17 @@ export class Frontend extends EventEmitter {
1001
1232
  if (clusterName === 'userLabel' && attributeName === 'labelList')
1002
1233
  attributes += `${getUserLabel(device)} `;
1003
1234
  });
1235
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
1004
1236
  return attributes.trimStart().trimEnd();
1005
1237
  }
1238
+ /**
1239
+ * Retrieves the registered plugins sanitized for res.json().
1240
+ *
1241
+ * @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
1242
+ */
1006
1243
  getPlugins() {
1007
1244
  if (this.matterbridge.hasCleanupStarted)
1008
- return [];
1245
+ return []; // Skip if cleanup has started
1009
1246
  const plugins = [];
1010
1247
  for (const plugin of this.matterbridge.plugins.array()) {
1011
1248
  plugins.push({
@@ -1033,18 +1270,27 @@ export class Frontend extends EventEmitter {
1033
1270
  schemaJson: plugin.schemaJson,
1034
1271
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
1035
1272
  hasBlackList: plugin.configJson?.blackList !== undefined,
1273
+ // Childbridge mode specific data
1036
1274
  matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
1037
1275
  });
1038
1276
  }
1039
1277
  return plugins;
1040
1278
  }
1279
+ /**
1280
+ * Retrieves the devices from Matterbridge.
1281
+ *
1282
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1283
+ * @returns {ApiDevice[]} An array of ApiDevices for the frontend.
1284
+ */
1041
1285
  getDevices(pluginName) {
1042
1286
  if (this.matterbridge.hasCleanupStarted)
1043
- return [];
1287
+ return []; // Skip if cleanup has started
1044
1288
  const devices = [];
1045
1289
  for (const device of this.matterbridge.devices.array()) {
1290
+ // Filter by pluginName if provided
1046
1291
  if (pluginName && pluginName !== device.plugin)
1047
1292
  continue;
1293
+ // Check if the device has the required properties
1048
1294
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1049
1295
  continue;
1050
1296
  devices.push({
@@ -1065,24 +1311,39 @@ export class Frontend extends EventEmitter {
1065
1311
  }
1066
1312
  return devices;
1067
1313
  }
1314
+ /**
1315
+ * Retrieves the clusters from a given plugin and endpoint number.
1316
+ *
1317
+ * Response for /api/clusters
1318
+ *
1319
+ * @param {string} pluginName - The name of the plugin.
1320
+ * @param {number} endpointNumber - The endpoint number.
1321
+ * @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
1322
+ */
1068
1323
  getClusters(pluginName, endpointNumber) {
1069
1324
  if (this.matterbridge.hasCleanupStarted)
1070
- return;
1325
+ return; // Skip if cleanup has started
1071
1326
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
1072
1327
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1073
1328
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1074
1329
  return;
1075
1330
  }
1331
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1332
+ // Get the device types from the main endpoint
1076
1333
  const deviceTypes = [];
1077
1334
  const clusters = [];
1078
1335
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1079
1336
  deviceTypes.push(d.deviceType);
1080
1337
  });
1338
+ // Get the clusters from the main endpoint
1081
1339
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1082
1340
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1083
1341
  return;
1084
1342
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1085
1343
  return;
1344
+ // console.log(
1345
+ // `${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}`,
1346
+ // );
1086
1347
  clusters.push({
1087
1348
  endpoint: endpoint.number.toString(),
1088
1349
  number: endpoint.number,
@@ -1096,12 +1357,19 @@ export class Frontend extends EventEmitter {
1096
1357
  attributeLocalValue: attributeValue,
1097
1358
  });
1098
1359
  });
1360
+ // Get the child endpoints
1099
1361
  const childEndpoints = endpoint.getChildEndpoints();
1362
+ // if (childEndpoints.length === 0) {
1363
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1364
+ // }
1100
1365
  childEndpoints.forEach((childEndpoint) => {
1366
+ // istanbul ignore if cause is not reachable: should never happen but ...
1101
1367
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1102
1368
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1103
1369
  return;
1104
1370
  }
1371
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1372
+ // Get the device types of the child endpoint
1105
1373
  const deviceTypes = [];
1106
1374
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1107
1375
  deviceTypes.push(d.deviceType);
@@ -1111,6 +1379,9 @@ export class Frontend extends EventEmitter {
1111
1379
  return;
1112
1380
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1113
1381
  return;
1382
+ // console.log(
1383
+ // `${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}`,
1384
+ // );
1114
1385
  clusters.push({
1115
1386
  endpoint: childEndpoint.number.toString(),
1116
1387
  number: childEndpoint.number,
@@ -1130,6 +1401,7 @@ export class Frontend extends EventEmitter {
1130
1401
  async generateDiagnostic() {
1131
1402
  this.log.debug('Generating diagnostic...');
1132
1403
  const serverNodes = [];
1404
+ // istanbul ignore else
1133
1405
  if (this.matterbridge.bridgeMode === 'bridge') {
1134
1406
  if (this.matterbridge.serverNode)
1135
1407
  serverNodes.push(this.matterbridge.serverNode);
@@ -1140,6 +1412,7 @@ export class Frontend extends EventEmitter {
1140
1412
  serverNodes.push(plugin.serverNode);
1141
1413
  }
1142
1414
  }
1415
+ // istanbul ignore next
1143
1416
  for (const device of this.matterbridge.devices.array()) {
1144
1417
  if (device.serverNode)
1145
1418
  serverNodes.push(device.serverNode);
@@ -1163,8 +1436,15 @@ export class Frontend extends EventEmitter {
1163
1436
  values: [...serverNodes],
1164
1437
  })));
1165
1438
  delete Logger.destinations.diagnostic;
1166
- await wait(500);
1439
+ await wait(500); // Wait for the log to be written
1167
1440
  }
1441
+ /**
1442
+ * Handles incoming websocket api request messages from the Matterbridge frontend.
1443
+ *
1444
+ * @param {WebSocket} client - The websocket client that sent the message.
1445
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1446
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1447
+ */
1168
1448
  async wsMessageHandler(client, message) {
1169
1449
  let data;
1170
1450
  const sendResponse = (data) => {
@@ -1182,12 +1462,13 @@ export class Frontend extends EventEmitter {
1182
1462
  client.send(JSON.stringify(data));
1183
1463
  }
1184
1464
  else {
1465
+ // istanbul ignore next cause is only a safety check
1185
1466
  this.log.error('Cannot send api response, client not connected');
1186
1467
  }
1187
1468
  };
1188
1469
  try {
1189
1470
  data = JSON.parse(message.toString());
1190
- if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
1471
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
1191
1472
  this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
1192
1473
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
1193
1474
  return;
@@ -1244,7 +1525,22 @@ export class Frontend extends EventEmitter {
1244
1525
  }
1245
1526
  this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
1246
1527
  this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
1247
- data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
1528
+ data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
1529
+ /*
1530
+ const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
1531
+ if (plugin) {
1532
+ this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
1533
+ await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
1534
+ this.wssSendRestartRequired();
1535
+ this.wssSendRefreshRequired('plugins');
1536
+ this.wssSendRefreshRequired('devices');
1537
+ this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1538
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1539
+ } else {
1540
+ this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
1541
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
1542
+ }
1543
+ */
1248
1544
  const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
1249
1545
  if (plugin) {
1250
1546
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1257,7 +1553,7 @@ export class Frontend extends EventEmitter {
1257
1553
  this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
1258
1554
  return;
1259
1555
  })
1260
- .catch((_error) => { });
1556
+ .catch(/* istanbul ignore next */ (_error) => { });
1261
1557
  }
1262
1558
  else {
1263
1559
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
@@ -1271,6 +1567,10 @@ export class Frontend extends EventEmitter {
1271
1567
  }
1272
1568
  this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
1273
1569
  this.log.debug(`Removing plugin ${data.params.pluginName}...`);
1570
+ /*
1571
+ 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);
1572
+ await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
1573
+ */
1274
1574
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1275
1575
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1276
1576
  await this.matterbridge.plugins.remove(data.params.pluginName);
@@ -1298,8 +1598,10 @@ export class Frontend extends EventEmitter {
1298
1598
  this.wssSendSnackbarMessage(`Enabled plugin ${data.params.pluginName}`, 5, 'success');
1299
1599
  setImmediate(async () => {
1300
1600
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
1601
+ // @ts-expect-error Accessing private method
1301
1602
  if (plugin.serverNode)
1302
1603
  await this.matterbridge.startServerNode(plugin.serverNode);
1604
+ // @ts-expect-error Accessing private method
1303
1605
  for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1304
1606
  await this.matterbridge.startServerNode(device.serverNode);
1305
1607
  this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
@@ -1315,9 +1617,11 @@ export class Frontend extends EventEmitter {
1315
1617
  }
1316
1618
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1317
1619
  for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode)) {
1620
+ // @ts-expect-error Accessing private method
1318
1621
  await this.matterbridge.stopServerNode(device.serverNode);
1319
1622
  device.serverNode = undefined;
1320
1623
  }
1624
+ // @ts-expect-error Accessing private method
1321
1625
  if (plugin.serverNode)
1322
1626
  await this.matterbridge.stopServerNode(plugin.serverNode);
1323
1627
  plugin.serverNode = undefined;
@@ -1336,30 +1640,37 @@ export class Frontend extends EventEmitter {
1336
1640
  this.wssSendSnackbarMessage(`Restarting plugin ${data.params.pluginName}`, 5, 'info');
1337
1641
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1338
1642
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1643
+ // Stop server nodes
1339
1644
  if (plugin.serverNode) {
1645
+ // @ts-expect-error Accessing private method
1340
1646
  await this.matterbridge.stopServerNode(plugin.serverNode);
1341
1647
  plugin.serverNode = undefined;
1342
1648
  }
1343
1649
  for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name)) {
1650
+ // @ts-expect-error Accessing private method
1344
1651
  if (device.serverNode)
1345
1652
  await this.matterbridge.stopServerNode(device.serverNode);
1346
1653
  device.serverNode = undefined;
1347
1654
  this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
1348
1655
  this.matterbridge.devices.remove(device);
1349
1656
  }
1657
+ // @ts-expect-error Accessing private method
1350
1658
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1351
1659
  await this.matterbridge.createDynamicPlugin(plugin);
1352
1660
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1353
- plugin.restartRequired = false;
1661
+ plugin.restartRequired = false; // Reset plugin restartRequired
1354
1662
  let needRestart = 0;
1355
1663
  for (const plugin of this.matterbridge.plugins) {
1356
1664
  if (plugin.restartRequired)
1357
1665
  needRestart++;
1358
1666
  }
1359
1667
  if (needRestart === 0)
1360
- this.wssSendRestartNotRequired(true);
1668
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1669
+ // Start server nodes
1670
+ // @ts-expect-error Accessing private method
1361
1671
  if (plugin.serverNode)
1362
1672
  await this.matterbridge.startServerNode(plugin.serverNode);
1673
+ // @ts-expect-error Accessing private method
1363
1674
  for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1364
1675
  await this.matterbridge.startServerNode(device.serverNode);
1365
1676
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1504,6 +1815,9 @@ export class Frontend extends EventEmitter {
1504
1815
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1505
1816
  }
1506
1817
  if (data.params.advertise) {
1818
+ // TODO: matter.js 0.16.0
1819
+ // await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
1820
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1507
1821
  const advertiser = serverNode.env.get(DeviceAdvertiser);
1508
1822
  if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
1509
1823
  await advertiser.advertise(true);
@@ -1567,6 +1881,7 @@ export class Frontend extends EventEmitter {
1567
1881
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
1568
1882
  return;
1569
1883
  }
1884
+ // istanbul ignore next
1570
1885
  const selectDeviceValues = !plugin.platform ? [] : plugin.platform.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1571
1886
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDeviceValues });
1572
1887
  }
@@ -1580,6 +1895,7 @@ export class Frontend extends EventEmitter {
1580
1895
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
1581
1896
  return;
1582
1897
  }
1898
+ // istanbul ignore next
1583
1899
  const selectEntityValues = !plugin.platform ? [] : plugin.platform.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1584
1900
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntityValues });
1585
1901
  }
@@ -1631,22 +1947,22 @@ export class Frontend extends EventEmitter {
1631
1947
  if (isValidString(data.params.value, 4)) {
1632
1948
  this.log.debug('Matterbridge logger level:', data.params.value);
1633
1949
  if (data.params.value === 'Debug') {
1634
- await this.matterbridge.setLogLevel("debug");
1950
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1635
1951
  }
1636
1952
  else if (data.params.value === 'Info') {
1637
- await this.matterbridge.setLogLevel("info");
1953
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1638
1954
  }
1639
1955
  else if (data.params.value === 'Notice') {
1640
- await this.matterbridge.setLogLevel("notice");
1956
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1641
1957
  }
1642
1958
  else if (data.params.value === 'Warn') {
1643
- await this.matterbridge.setLogLevel("warn");
1959
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1644
1960
  }
1645
1961
  else if (data.params.value === 'Error') {
1646
- await this.matterbridge.setLogLevel("error");
1962
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1647
1963
  }
1648
1964
  else if (data.params.value === 'Fatal') {
1649
- await this.matterbridge.setLogLevel("fatal");
1965
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1650
1966
  }
1651
1967
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1652
1968
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1657,6 +1973,7 @@ export class Frontend extends EventEmitter {
1657
1973
  this.log.debug('Matterbridge file log:', data.params.value);
1658
1974
  this.matterbridge.fileLogger = data.params.value;
1659
1975
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1976
+ // Create the file logger for matterbridge
1660
1977
  if (data.params.value)
1661
1978
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1662
1979
  else
@@ -1686,11 +2003,12 @@ export class Frontend extends EventEmitter {
1686
2003
  Logger.level = MatterLogLevel.FATAL;
1687
2004
  }
1688
2005
  this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
1689
- let callbackLogLevel = "notice";
1690
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1691
- callbackLogLevel = "info";
1692
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1693
- callbackLogLevel = "debug";
2006
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
2007
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
2008
+ if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
2009
+ callbackLogLevel = "info" /* LogLevel.INFO */;
2010
+ if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
2011
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
1694
2012
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1695
2013
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1696
2014
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1742,6 +2060,7 @@ export class Frontend extends EventEmitter {
1742
2060
  }
1743
2061
  break;
1744
2062
  case 'setmatterport':
2063
+ // eslint-disable-next-line no-case-declarations
1745
2064
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1746
2065
  if (isValidNumber(port, 5540, 5600)) {
1747
2066
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1761,6 +2080,7 @@ export class Frontend extends EventEmitter {
1761
2080
  }
1762
2081
  break;
1763
2082
  case 'setmatterdiscriminator':
2083
+ // eslint-disable-next-line no-case-declarations
1764
2084
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1765
2085
  if (isValidNumber(discriminator, 0, 4095)) {
1766
2086
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1780,6 +2100,7 @@ export class Frontend extends EventEmitter {
1780
2100
  }
1781
2101
  break;
1782
2102
  case 'setmatterpasscode':
2103
+ // eslint-disable-next-line no-case-declarations
1783
2104
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1784
2105
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1785
2106
  this.matterbridge.passcode = passcode;
@@ -1825,15 +2146,19 @@ export class Frontend extends EventEmitter {
1825
2146
  return;
1826
2147
  }
1827
2148
  const config = plugin.configJson;
2149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1828
2150
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2151
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1829
2152
  if (select === 'serial')
1830
2153
  this.log.info(`Selected device serial ${data.params.serial}`);
1831
2154
  if (select === 'name')
1832
2155
  this.log.info(`Selected device name ${data.params.name}`);
1833
2156
  if (config && select && (select === 'serial' || select === 'name')) {
2157
+ // Remove postfix from the serial if it exists
1834
2158
  if (config.postfix) {
1835
2159
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1836
2160
  }
2161
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1837
2162
  if (isValidArray(config.whiteList, 1)) {
1838
2163
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1839
2164
  config.whiteList.push(data.params.serial);
@@ -1842,6 +2167,7 @@ export class Frontend extends EventEmitter {
1842
2167
  config.whiteList.push(data.params.name);
1843
2168
  }
1844
2169
  }
2170
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1845
2171
  if (isValidArray(config.blackList, 1)) {
1846
2172
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1847
2173
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1869,7 +2195,9 @@ export class Frontend extends EventEmitter {
1869
2195
  return;
1870
2196
  }
1871
2197
  const config = plugin.configJson;
2198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1872
2199
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2200
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1873
2201
  if (select === 'serial')
1874
2202
  this.log.info(`Unselected device serial ${data.params.serial}`);
1875
2203
  if (select === 'name')
@@ -1878,6 +2206,7 @@ export class Frontend extends EventEmitter {
1878
2206
  if (config.postfix) {
1879
2207
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1880
2208
  }
2209
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1881
2210
  if (isValidArray(config.whiteList, 1)) {
1882
2211
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1883
2212
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1886,6 +2215,7 @@ export class Frontend extends EventEmitter {
1886
2215
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1887
2216
  }
1888
2217
  }
2218
+ // Add the serial to the blackList
1889
2219
  if (isValidArray(config.blackList)) {
1890
2220
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1891
2221
  config.blackList.push(data.params.serial);
@@ -1908,6 +2238,7 @@ export class Frontend extends EventEmitter {
1908
2238
  }
1909
2239
  }
1910
2240
  else {
2241
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1911
2242
  const localData = data;
1912
2243
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1913
2244
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1917,23 +2248,46 @@ export class Frontend extends EventEmitter {
1917
2248
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1918
2249
  }
1919
2250
  }
2251
+ /**
2252
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
2253
+ *
2254
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2255
+ * @param {string} time - The time string of the message
2256
+ * @param {string} name - The logger name of the message
2257
+ * @param {string} message - The content of the message.
2258
+ *
2259
+ * @remarks
2260
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
2261
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
2262
+ * The function sends the message to all connected clients.
2263
+ */
1920
2264
  wssSendLogMessage(level, time, name, message) {
1921
2265
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1922
2266
  return;
1923
2267
  if (!level || !time || !name || !message)
1924
2268
  return;
2269
+ // Remove ANSI escape codes from the message
2270
+ // eslint-disable-next-line no-control-regex
1925
2271
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2272
+ // Remove leading asterisks from the message
1926
2273
  message = message.replace(/^\*+/, '');
2274
+ // Replace all occurrences of \t and \n
1927
2275
  message = message.replace(/[\t\n]/g, '');
2276
+ // Remove non-printable characters
2277
+ // eslint-disable-next-line no-control-regex
1928
2278
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2279
+ // Replace all occurrences of \" with "
1929
2280
  message = message.replace(/\\"/g, '"');
2281
+ // Define the maximum allowed length for continuous characters without a space
1930
2282
  const maxContinuousLength = 100;
1931
2283
  const keepStartLength = 20;
1932
2284
  const keepEndLength = 20;
2285
+ // Split the message into words
1933
2286
  if (level !== 'spawn') {
1934
2287
  message = message
1935
2288
  .split(' ')
1936
2289
  .map((word) => {
2290
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1937
2291
  if (word.length > maxContinuousLength) {
1938
2292
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1939
2293
  }
@@ -1941,14 +2295,34 @@ export class Frontend extends EventEmitter {
1941
2295
  })
1942
2296
  .join(' ');
1943
2297
  }
2298
+ // Send the message to all connected clients
1944
2299
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1945
2300
  }
2301
+ /**
2302
+ * Sends a need to refresh WebSocket message to all connected clients.
2303
+ *
2304
+ * @param {string} changed - The changed value.
2305
+ * @param {Record<string, unknown>} params - Additional parameters to send with the message.
2306
+ * possible values for changed:
2307
+ * - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
2308
+ * - 'plugins'
2309
+ * - 'devices'
2310
+ * - 'matter' with param 'matter' (QRDiv component)
2311
+ * @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
2312
+ */
1946
2313
  wssSendRefreshRequired(changed, params) {
1947
2314
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1948
2315
  return;
1949
2316
  this.log.debug('Sending a refresh required message to all connected clients');
2317
+ // Send the message to all connected clients
1950
2318
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1951
2319
  }
2320
+ /**
2321
+ * Sends a need to restart WebSocket message to all connected clients.
2322
+ *
2323
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2324
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2325
+ */
1952
2326
  wssSendRestartRequired(snackbar = true, fixed = false) {
1953
2327
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1954
2328
  return;
@@ -1957,8 +2331,14 @@ export class Frontend extends EventEmitter {
1957
2331
  this.matterbridge.fixedRestartRequired = fixed;
1958
2332
  if (snackbar === true)
1959
2333
  this.wssSendSnackbarMessage(`Restart required`, 0);
2334
+ // Send the message to all connected clients
1960
2335
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1961
2336
  }
2337
+ /**
2338
+ * Sends a no need to restart WebSocket message to all connected clients.
2339
+ *
2340
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
2341
+ */
1962
2342
  wssSendRestartNotRequired(snackbar = true) {
1963
2343
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1964
2344
  return;
@@ -1966,64 +2346,145 @@ export class Frontend extends EventEmitter {
1966
2346
  this.matterbridge.restartRequired = false;
1967
2347
  if (snackbar === true)
1968
2348
  this.wssSendCloseSnackbarMessage(`Restart required`);
2349
+ // Send the message to all connected clients
1969
2350
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1970
2351
  }
2352
+ /**
2353
+ * Sends a need to update WebSocket message to all connected clients.
2354
+ *
2355
+ * @param {boolean} devVersion - If true, the update is for a development version. Default is false.
2356
+ */
1971
2357
  wssSendUpdateRequired(devVersion = false) {
1972
2358
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1973
2359
  return;
1974
2360
  this.log.debug('Sending an update required message to all connected clients');
1975
2361
  this.matterbridge.updateRequired = true;
2362
+ // Send the message to all connected clients
1976
2363
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1977
2364
  }
2365
+ /**
2366
+ * Sends a cpu update message to all connected clients.
2367
+ *
2368
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2369
+ * @param {number} processCpuUsage - The CPU usage percentage of the process to send.
2370
+ */
1978
2371
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1979
2372
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1980
2373
  return;
2374
+ // istanbul ignore else
1981
2375
  if (hasParameter('debug'))
1982
2376
  this.log.debug('Sending a cpu update message to all connected clients');
2377
+ // Send the message to all connected clients
1983
2378
  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 } });
1984
2379
  }
2380
+ /**
2381
+ * Sends a memory update message to all connected clients.
2382
+ *
2383
+ * @param {string} totalMemory - The total memory in bytes.
2384
+ * @param {string} freeMemory - The free memory in bytes.
2385
+ * @param {string} rss - The resident set size in bytes.
2386
+ * @param {string} heapTotal - The total heap memory in bytes.
2387
+ * @param {string} heapUsed - The used heap memory in bytes.
2388
+ * @param {string} external - The external memory in bytes.
2389
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2390
+ */
1985
2391
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1986
2392
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1987
2393
  return;
2394
+ // istanbul ignore else
1988
2395
  if (hasParameter('debug'))
1989
2396
  this.log.debug('Sending a memory update message to all connected clients');
2397
+ // Send the message to all connected clients
1990
2398
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1991
2399
  }
2400
+ /**
2401
+ * Sends an uptime update message to all connected clients.
2402
+ *
2403
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2404
+ * @param {string} processUptime - The process uptime in a human-readable format.
2405
+ */
1992
2406
  wssSendUptimeUpdate(systemUptime, processUptime) {
1993
2407
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1994
2408
  return;
2409
+ // istanbul ignore else
1995
2410
  if (hasParameter('debug'))
1996
2411
  this.log.debug('Sending a uptime update message to all connected clients');
2412
+ // Send the message to all connected clients
1997
2413
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1998
2414
  }
2415
+ /**
2416
+ * Sends an open snackbar message to all connected clients.
2417
+ *
2418
+ * @param {string} message - The message to send.
2419
+ * @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
2420
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
2421
+ * possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
2422
+ *
2423
+ * @remarks
2424
+ * If timeout is 0, the snackbar message will be displayed until closed by the user.
2425
+ */
1999
2426
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
2000
2427
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2001
2428
  return;
2002
2429
  this.log.debug('Sending a snackbar message to all connected clients');
2430
+ // Send the message to all connected clients
2003
2431
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
2004
2432
  }
2433
+ /**
2434
+ * Sends a close snackbar message to all connected clients.
2435
+ * It will close the snackbar message with the same message and timeout = 0.
2436
+ *
2437
+ * @param {string} message - The message to send.
2438
+ */
2005
2439
  wssSendCloseSnackbarMessage(message) {
2006
2440
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2007
2441
  return;
2008
2442
  this.log.debug('Sending a close snackbar message to all connected clients');
2443
+ // Send the message to all connected clients
2009
2444
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
2010
2445
  }
2446
+ /**
2447
+ * Sends an attribute update message to all connected WebSocket clients.
2448
+ *
2449
+ * @param {string | undefined} plugin - The name of the plugin.
2450
+ * @param {string | undefined} serialNumber - The serial number of the device.
2451
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2452
+ * @param {EndpointNumber} number - The endpoint number where the attribute belongs.
2453
+ * @param {string} id - The endpoint id where the attribute belongs.
2454
+ * @param {string} cluster - The cluster name where the attribute belongs.
2455
+ * @param {string} attribute - The name of the attribute that changed.
2456
+ * @param {number | string | boolean} value - The new value of the attribute.
2457
+ *
2458
+ * @remarks
2459
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2460
+ * with the updated attribute information.
2461
+ */
2011
2462
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
2012
2463
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2013
2464
  return;
2014
2465
  this.log.debug('Sending an attribute update message to all connected clients');
2466
+ // Send the message to all connected clients
2015
2467
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
2016
2468
  }
2469
+ /**
2470
+ * Sends a message to all connected clients.
2471
+ * This is an helper function to send a broadcast message to all connected clients.
2472
+ *
2473
+ * @param {WsMessageBroadcast} msg - The message to send.
2474
+ */
2017
2475
  wssBroadcastMessage(msg) {
2018
2476
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2019
2477
  return;
2478
+ // Send the message to all connected clients
2020
2479
  const stringifiedMsg = JSON.stringify(msg);
2021
2480
  if (msg.method !== 'log')
2022
2481
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
2023
2482
  this.webSocketServer?.clients.forEach((client) => {
2483
+ // istanbul ignore else
2024
2484
  if (client.readyState === client.OPEN) {
2025
2485
  client.send(stringifiedMsg);
2026
2486
  }
2027
2487
  });
2028
2488
  }
2029
2489
  }
2490
+ //# sourceMappingURL=frontend.js.map