matterbridge 3.5.0-dev-20260117-88ddbe4 → 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 (326) hide show
  1. package/CHANGELOG.md +130 -122
  2. package/README.md +14 -14
  3. package/dist/broadcastServer.d.ts +115 -0
  4. package/dist/broadcastServer.d.ts.map +1 -0
  5. package/dist/broadcastServer.js +117 -0
  6. package/dist/broadcastServer.js.map +1 -0
  7. package/dist/broadcastServerTypes.d.ts +43 -0
  8. package/dist/broadcastServerTypes.d.ts.map +1 -0
  9. package/dist/broadcastServerTypes.js +24 -0
  10. package/dist/broadcastServerTypes.js.map +1 -0
  11. package/dist/cli.d.ts +24 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +98 -1
  14. package/dist/cli.js.map +1 -0
  15. package/dist/cliEmitter.d.ts +36 -0
  16. package/dist/cliEmitter.d.ts.map +1 -0
  17. package/dist/cliEmitter.js +37 -0
  18. package/dist/cliEmitter.js.map +1 -0
  19. package/dist/cliHistory.d.ts +42 -0
  20. package/dist/cliHistory.d.ts.map +1 -0
  21. package/dist/cliHistory.js +38 -0
  22. package/dist/cliHistory.js.map +1 -0
  23. package/dist/clusters/export.d.ts +1 -0
  24. package/dist/clusters/export.d.ts.map +1 -0
  25. package/dist/clusters/export.js +2 -0
  26. package/dist/clusters/export.js.map +1 -0
  27. package/dist/deviceManager.d.ts +108 -0
  28. package/dist/deviceManager.d.ts.map +1 -0
  29. package/dist/deviceManager.js +113 -1
  30. package/dist/deviceManager.js.map +1 -0
  31. package/dist/devices/airConditioner.d.ts +75 -0
  32. package/dist/devices/airConditioner.d.ts.map +1 -0
  33. package/dist/devices/airConditioner.js +57 -0
  34. package/dist/devices/airConditioner.js.map +1 -0
  35. package/dist/devices/batteryStorage.d.ts +43 -0
  36. package/dist/devices/batteryStorage.d.ts.map +1 -0
  37. package/dist/devices/batteryStorage.js +48 -1
  38. package/dist/devices/batteryStorage.js.map +1 -0
  39. package/dist/devices/cooktop.d.ts +55 -0
  40. package/dist/devices/cooktop.d.ts.map +1 -0
  41. package/dist/devices/cooktop.js +56 -0
  42. package/dist/devices/cooktop.js.map +1 -0
  43. package/dist/devices/dishwasher.d.ts +55 -0
  44. package/dist/devices/dishwasher.d.ts.map +1 -0
  45. package/dist/devices/dishwasher.js +57 -0
  46. package/dist/devices/dishwasher.js.map +1 -0
  47. package/dist/devices/evse.d.ts +57 -0
  48. package/dist/devices/evse.d.ts.map +1 -0
  49. package/dist/devices/evse.js +74 -10
  50. package/dist/devices/evse.js.map +1 -0
  51. package/dist/devices/export.d.ts +1 -0
  52. package/dist/devices/export.d.ts.map +1 -0
  53. package/dist/devices/export.js +5 -0
  54. package/dist/devices/export.js.map +1 -0
  55. package/dist/devices/extractorHood.d.ts +41 -0
  56. package/dist/devices/extractorHood.d.ts.map +1 -0
  57. package/dist/devices/extractorHood.js +43 -0
  58. package/dist/devices/extractorHood.js.map +1 -0
  59. package/dist/devices/heatPump.d.ts +43 -0
  60. package/dist/devices/heatPump.d.ts.map +1 -0
  61. package/dist/devices/heatPump.js +50 -2
  62. package/dist/devices/heatPump.js.map +1 -0
  63. package/dist/devices/laundryDryer.d.ts +58 -0
  64. package/dist/devices/laundryDryer.d.ts.map +1 -0
  65. package/dist/devices/laundryDryer.js +62 -3
  66. package/dist/devices/laundryDryer.js.map +1 -0
  67. package/dist/devices/laundryWasher.d.ts +64 -0
  68. package/dist/devices/laundryWasher.d.ts.map +1 -0
  69. package/dist/devices/laundryWasher.js +70 -4
  70. package/dist/devices/laundryWasher.js.map +1 -0
  71. package/dist/devices/microwaveOven.d.ts +77 -1
  72. package/dist/devices/microwaveOven.d.ts.map +1 -0
  73. package/dist/devices/microwaveOven.js +88 -5
  74. package/dist/devices/microwaveOven.js.map +1 -0
  75. package/dist/devices/oven.d.ts +82 -0
  76. package/dist/devices/oven.d.ts.map +1 -0
  77. package/dist/devices/oven.js +85 -0
  78. package/dist/devices/oven.js.map +1 -0
  79. package/dist/devices/refrigerator.d.ts +100 -0
  80. package/dist/devices/refrigerator.d.ts.map +1 -0
  81. package/dist/devices/refrigerator.js +102 -0
  82. package/dist/devices/refrigerator.js.map +1 -0
  83. package/dist/devices/roboticVacuumCleaner.d.ts +83 -0
  84. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  85. package/dist/devices/roboticVacuumCleaner.js +100 -9
  86. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  87. package/dist/devices/solarPower.d.ts +36 -0
  88. package/dist/devices/solarPower.d.ts.map +1 -0
  89. package/dist/devices/solarPower.js +38 -0
  90. package/dist/devices/solarPower.js.map +1 -0
  91. package/dist/devices/speaker.d.ts +79 -0
  92. package/dist/devices/speaker.d.ts.map +1 -0
  93. package/dist/devices/speaker.js +84 -0
  94. package/dist/devices/speaker.js.map +1 -0
  95. package/dist/devices/temperatureControl.d.ts +21 -0
  96. package/dist/devices/temperatureControl.d.ts.map +1 -0
  97. package/dist/devices/temperatureControl.js +24 -3
  98. package/dist/devices/temperatureControl.js.map +1 -0
  99. package/dist/devices/waterHeater.d.ts +74 -0
  100. package/dist/devices/waterHeater.d.ts.map +1 -0
  101. package/dist/devices/waterHeater.js +82 -2
  102. package/dist/devices/waterHeater.js.map +1 -0
  103. package/dist/dgram/coap.d.ts +171 -0
  104. package/dist/dgram/coap.d.ts.map +1 -0
  105. package/dist/dgram/coap.js +126 -13
  106. package/dist/dgram/coap.js.map +1 -0
  107. package/dist/dgram/dgram.d.ts +99 -0
  108. package/dist/dgram/dgram.d.ts.map +1 -0
  109. package/dist/dgram/dgram.js +114 -2
  110. package/dist/dgram/dgram.js.map +1 -0
  111. package/dist/dgram/mb_coap.d.ts +23 -0
  112. package/dist/dgram/mb_coap.d.ts.map +1 -0
  113. package/dist/dgram/mb_coap.js +41 -3
  114. package/dist/dgram/mb_coap.js.map +1 -0
  115. package/dist/dgram/mb_mdns.d.ts +23 -0
  116. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  117. package/dist/dgram/mb_mdns.js +80 -24
  118. package/dist/dgram/mb_mdns.js.map +1 -0
  119. package/dist/dgram/mdns.d.ts +187 -4
  120. package/dist/dgram/mdns.d.ts.map +1 -0
  121. package/dist/dgram/mdns.js +371 -139
  122. package/dist/dgram/mdns.js.map +1 -0
  123. package/dist/dgram/multicast.d.ts +49 -0
  124. package/dist/dgram/multicast.d.ts.map +1 -0
  125. package/dist/dgram/multicast.js +62 -1
  126. package/dist/dgram/multicast.js.map +1 -0
  127. package/dist/dgram/unicast.d.ts +53 -0
  128. package/dist/dgram/unicast.d.ts.map +1 -0
  129. package/dist/dgram/unicast.js +60 -0
  130. package/dist/dgram/unicast.js.map +1 -0
  131. package/dist/frontend.d.ts +187 -0
  132. package/dist/frontend.d.ts.map +1 -0
  133. package/dist/frontend.js +543 -73
  134. package/dist/frontend.js.map +1 -0
  135. package/dist/frontendTypes.d.ts +57 -0
  136. package/dist/frontendTypes.d.ts.map +1 -0
  137. package/dist/frontendTypes.js +45 -0
  138. package/dist/frontendTypes.js.map +1 -0
  139. package/dist/helpers.d.ts +43 -0
  140. package/dist/helpers.d.ts.map +1 -0
  141. package/dist/helpers.js +53 -0
  142. package/dist/helpers.js.map +1 -0
  143. package/dist/index.d.ts +23 -0
  144. package/dist/index.d.ts.map +1 -0
  145. package/dist/index.js +25 -0
  146. package/dist/index.js.map +1 -0
  147. package/dist/jestutils/export.d.ts +1 -0
  148. package/dist/jestutils/export.d.ts.map +1 -0
  149. package/dist/jestutils/export.js +1 -0
  150. package/dist/jestutils/export.js.map +1 -0
  151. package/dist/jestutils/jestHelpers.d.ts +255 -0
  152. package/dist/jestutils/jestHelpers.d.ts.map +1 -0
  153. package/dist/jestutils/jestHelpers.js +372 -14
  154. package/dist/jestutils/jestHelpers.js.map +1 -0
  155. package/dist/logger/export.d.ts +1 -0
  156. package/dist/logger/export.d.ts.map +1 -0
  157. package/dist/logger/export.js +1 -0
  158. package/dist/logger/export.js.map +1 -0
  159. package/dist/matter/behaviors.d.ts +1 -0
  160. package/dist/matter/behaviors.d.ts.map +1 -0
  161. package/dist/matter/behaviors.js +2 -0
  162. package/dist/matter/behaviors.js.map +1 -0
  163. package/dist/matter/clusters.d.ts +1 -0
  164. package/dist/matter/clusters.d.ts.map +1 -0
  165. package/dist/matter/clusters.js +2 -0
  166. package/dist/matter/clusters.js.map +1 -0
  167. package/dist/matter/devices.d.ts +1 -0
  168. package/dist/matter/devices.d.ts.map +1 -0
  169. package/dist/matter/devices.js +2 -0
  170. package/dist/matter/devices.js.map +1 -0
  171. package/dist/matter/endpoints.d.ts +1 -0
  172. package/dist/matter/endpoints.d.ts.map +1 -0
  173. package/dist/matter/endpoints.js +2 -0
  174. package/dist/matter/endpoints.js.map +1 -0
  175. package/dist/matter/export.d.ts +1 -0
  176. package/dist/matter/export.d.ts.map +1 -0
  177. package/dist/matter/export.js +2 -0
  178. package/dist/matter/export.js.map +1 -0
  179. package/dist/matter/types.d.ts +1 -0
  180. package/dist/matter/types.d.ts.map +1 -0
  181. package/dist/matter/types.js +2 -0
  182. package/dist/matter/types.js.map +1 -0
  183. package/dist/matterNode.d.ts +258 -0
  184. package/dist/matterNode.d.ts.map +1 -0
  185. package/dist/matterNode.js +362 -9
  186. package/dist/matterNode.js.map +1 -0
  187. package/dist/matterbridge.d.ts +362 -0
  188. package/dist/matterbridge.d.ts.map +1 -0
  189. package/dist/matterbridge.js +879 -56
  190. package/dist/matterbridge.js.map +1 -0
  191. package/dist/matterbridgeAccessoryPlatform.d.ts +36 -0
  192. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  193. package/dist/matterbridgeAccessoryPlatform.js +38 -0
  194. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  195. package/dist/matterbridgeBehaviors.d.ts +24 -0
  196. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  197. package/dist/matterbridgeBehaviors.js +68 -5
  198. package/dist/matterbridgeBehaviors.js.map +1 -0
  199. package/dist/matterbridgeDeviceTypes.d.ts +649 -0
  200. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  201. package/dist/matterbridgeDeviceTypes.js +673 -6
  202. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  203. package/dist/matterbridgeDynamicPlatform.d.ts +36 -0
  204. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  205. package/dist/matterbridgeDynamicPlatform.js +38 -0
  206. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  207. package/dist/matterbridgeEndpoint.d.ts +1332 -0
  208. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  209. package/dist/matterbridgeEndpoint.js +1457 -53
  210. package/dist/matterbridgeEndpoint.js.map +1 -0
  211. package/dist/matterbridgeEndpointHelpers.d.ts +425 -0
  212. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  213. package/dist/matterbridgeEndpointHelpers.js +483 -20
  214. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  215. package/dist/matterbridgeEndpointTypes.d.ts +70 -0
  216. package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
  217. package/dist/matterbridgeEndpointTypes.js +25 -0
  218. package/dist/matterbridgeEndpointTypes.js.map +1 -0
  219. package/dist/matterbridgePlatform.d.ts +425 -0
  220. package/dist/matterbridgePlatform.d.ts.map +1 -0
  221. package/dist/matterbridgePlatform.js +451 -1
  222. package/dist/matterbridgePlatform.js.map +1 -0
  223. package/dist/matterbridgeTypes.d.ts +46 -0
  224. package/dist/matterbridgeTypes.d.ts.map +1 -0
  225. package/dist/matterbridgeTypes.js +26 -0
  226. package/dist/matterbridgeTypes.js.map +1 -0
  227. package/dist/pluginManager.d.ts +305 -0
  228. package/dist/pluginManager.d.ts.map +1 -0
  229. package/dist/pluginManager.js +341 -5
  230. package/dist/pluginManager.js.map +1 -0
  231. package/dist/shelly.d.ts +157 -0
  232. package/dist/shelly.d.ts.map +1 -0
  233. package/dist/shelly.js +178 -7
  234. package/dist/shelly.js.map +1 -0
  235. package/dist/storage/export.d.ts +1 -0
  236. package/dist/storage/export.d.ts.map +1 -0
  237. package/dist/storage/export.js +1 -0
  238. package/dist/storage/export.js.map +1 -0
  239. package/dist/update.d.ts +75 -0
  240. package/dist/update.d.ts.map +1 -0
  241. package/dist/update.js +93 -1
  242. package/dist/update.js.map +1 -0
  243. package/dist/utils/colorUtils.d.ts +77 -0
  244. package/dist/utils/colorUtils.d.ts.map +1 -0
  245. package/dist/utils/colorUtils.js +97 -2
  246. package/dist/utils/colorUtils.js.map +1 -0
  247. package/dist/utils/commandLine.d.ts +60 -0
  248. package/dist/utils/commandLine.d.ts.map +1 -0
  249. package/dist/utils/commandLine.js +60 -0
  250. package/dist/utils/commandLine.js.map +1 -0
  251. package/dist/utils/copyDirectory.d.ts +33 -0
  252. package/dist/utils/copyDirectory.d.ts.map +1 -0
  253. package/dist/utils/copyDirectory.js +37 -0
  254. package/dist/utils/copyDirectory.js.map +1 -0
  255. package/dist/utils/createDirectory.d.ts +32 -0
  256. package/dist/utils/createDirectory.d.ts.map +1 -0
  257. package/dist/utils/createDirectory.js +33 -0
  258. package/dist/utils/createDirectory.js.map +1 -0
  259. package/dist/utils/createZip.d.ts +38 -0
  260. package/dist/utils/createZip.d.ts.map +1 -0
  261. package/dist/utils/createZip.js +47 -2
  262. package/dist/utils/createZip.js.map +1 -0
  263. package/dist/utils/deepCopy.d.ts +31 -0
  264. package/dist/utils/deepCopy.d.ts.map +1 -0
  265. package/dist/utils/deepCopy.js +39 -0
  266. package/dist/utils/deepCopy.js.map +1 -0
  267. package/dist/utils/deepEqual.d.ts +53 -0
  268. package/dist/utils/deepEqual.d.ts.map +1 -0
  269. package/dist/utils/deepEqual.js +72 -1
  270. package/dist/utils/deepEqual.js.map +1 -0
  271. package/dist/utils/error.d.ts +42 -0
  272. package/dist/utils/error.d.ts.map +1 -0
  273. package/dist/utils/error.js +42 -0
  274. package/dist/utils/error.js.map +1 -0
  275. package/dist/utils/export.d.ts +1 -0
  276. package/dist/utils/export.d.ts.map +1 -0
  277. package/dist/utils/export.js +1 -0
  278. package/dist/utils/export.js.map +1 -0
  279. package/dist/utils/format.d.ts +49 -0
  280. package/dist/utils/format.d.ts.map +1 -0
  281. package/dist/utils/format.js +49 -0
  282. package/dist/utils/format.js.map +1 -0
  283. package/dist/utils/hex.d.ts +85 -0
  284. package/dist/utils/hex.d.ts.map +1 -0
  285. package/dist/utils/hex.js +124 -0
  286. package/dist/utils/hex.js.map +1 -0
  287. package/dist/utils/inspector.d.ts +63 -0
  288. package/dist/utils/inspector.d.ts.map +1 -0
  289. package/dist/utils/inspector.js +69 -1
  290. package/dist/utils/inspector.js.map +1 -0
  291. package/dist/utils/isValid.d.ts +93 -0
  292. package/dist/utils/isValid.d.ts.map +1 -0
  293. package/dist/utils/isValid.js +93 -0
  294. package/dist/utils/isValid.js.map +1 -0
  295. package/dist/utils/network.d.ts +116 -0
  296. package/dist/utils/network.d.ts.map +1 -0
  297. package/dist/utils/network.js +126 -5
  298. package/dist/utils/network.js.map +1 -0
  299. package/dist/utils/spawn.d.ts +32 -0
  300. package/dist/utils/spawn.d.ts.map +1 -0
  301. package/dist/utils/spawn.js +71 -1
  302. package/dist/utils/spawn.js.map +1 -0
  303. package/dist/utils/tracker.d.ts +56 -0
  304. package/dist/utils/tracker.d.ts.map +1 -0
  305. package/dist/utils/tracker.js +64 -1
  306. package/dist/utils/tracker.js.map +1 -0
  307. package/dist/utils/wait.d.ts +51 -0
  308. package/dist/utils/wait.d.ts.map +1 -0
  309. package/dist/utils/wait.js +60 -8
  310. package/dist/utils/wait.js.map +1 -0
  311. package/dist/workerGlobalPrefix.d.ts +24 -0
  312. package/dist/workerGlobalPrefix.d.ts.map +1 -0
  313. package/dist/workerGlobalPrefix.js +37 -5
  314. package/dist/workerGlobalPrefix.js.map +1 -0
  315. package/dist/workerTypes.d.ts +25 -0
  316. package/dist/workerTypes.d.ts.map +1 -0
  317. package/dist/workerTypes.js +24 -0
  318. package/dist/workerTypes.js.map +1 -0
  319. package/dist/workers.d.ts +61 -0
  320. package/dist/workers.d.ts.map +1 -0
  321. package/dist/workers.js +68 -4
  322. package/dist/workers.js.map +1 -0
  323. package/frontend/build/assets/index.js +4 -4
  324. package/frontend/package.json +1 -1
  325. package/npm-shrinkwrap.json +5 -35
  326. 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';
@@ -13,7 +39,7 @@ import { PowerSource } from '@matter/types/clusters/power-source';
13
39
  import { MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg } from './matterbridgeTypes.js';
14
40
  import { isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean } from './utils/isValid.js';
15
41
  import { createZip } from './utils/createZip.js';
16
- import { hasParameter } from './utils/commandLine.js';
42
+ import { hasParameter, getParameter } from './utils/commandLine.js';
17
43
  import { withTimeout, wait } from './utils/wait.js';
18
44
  import { inspectError } from './utils/error.js';
19
45
  import { formatBytes, formatUptime, formatPercent } from './utils/format.js';
@@ -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,14 +279,18 @@ export class Frontend extends EventEmitter {
205
279
  });
206
280
  }
207
281
  else {
208
- this.httpServer.listen(this.port, () => {
282
+ // We listen to all available addresses
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
  }
213
- if (this.matterbridge.systemInformation.ipv4Address !== '')
289
+ // istanbul ignore else
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}`);
215
- if (this.matterbridge.systemInformation.ipv6Address !== '')
292
+ // istanbul ignore else
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;
218
296
  this.emit('server_listening', 'http', this.port);
@@ -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,14 +431,18 @@ export class Frontend extends EventEmitter {
341
431
  });
342
432
  }
343
433
  else {
344
- this.httpsServer.listen(this.port, () => {
434
+ // We listen to all available addresses
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
  }
349
- if (this.matterbridge.systemInformation.ipv4Address !== '')
441
+ // istanbul ignore else
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}`);
351
- if (this.matterbridge.systemInformation.ipv6Address !== '')
444
+ // istanbul ignore else
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;
354
448
  this.emit('server_listening', 'https', this.port);
@@ -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);
@@ -1286,28 +1586,29 @@ export class Frontend extends EventEmitter {
1286
1586
  return;
1287
1587
  }
1288
1588
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1289
- if (plugin && !plugin.enabled) {
1290
- plugin.locked = undefined;
1291
- plugin.error = undefined;
1292
- plugin.loaded = undefined;
1293
- plugin.started = undefined;
1294
- plugin.configured = undefined;
1295
- plugin.platform = undefined;
1296
- plugin.registeredDevices = undefined;
1297
- plugin.matter = undefined;
1298
- await this.matterbridge.plugins.enable(data.params.pluginName);
1299
- this.wssSendSnackbarMessage(`Enabled plugin ${data.params.pluginName}`, 5, 'success');
1300
- this.matterbridge.plugins
1301
- .load(plugin, true, 'The plugin has been enabled', true)
1302
- .then(() => {
1303
- this.wssSendRefreshRequired('plugins');
1304
- this.wssSendRefreshRequired('devices');
1305
- this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
1306
- return;
1307
- })
1308
- .catch((_error) => { });
1309
- }
1310
- sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
1589
+ plugin.locked = undefined;
1590
+ plugin.error = undefined;
1591
+ plugin.loaded = undefined;
1592
+ plugin.started = undefined;
1593
+ plugin.configured = undefined;
1594
+ plugin.platform = undefined;
1595
+ plugin.registeredDevices = undefined;
1596
+ plugin.matter = undefined;
1597
+ await this.matterbridge.plugins.enable(data.params.pluginName);
1598
+ this.wssSendSnackbarMessage(`Enabled plugin ${data.params.pluginName}`, 5, 'success');
1599
+ setImmediate(async () => {
1600
+ await this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
1601
+ // @ts-expect-error Accessing private method
1602
+ if (plugin.serverNode)
1603
+ await this.matterbridge.startServerNode(plugin.serverNode);
1604
+ // @ts-expect-error Accessing private method
1605
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1606
+ await this.matterbridge.startServerNode(device.serverNode);
1607
+ this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
1608
+ this.wssSendRefreshRequired('plugins');
1609
+ this.wssSendRefreshRequired('devices');
1610
+ sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, success: true });
1611
+ });
1311
1612
  }
1312
1613
  else if (data.method === '/api/disableplugin') {
1313
1614
  if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
@@ -1315,6 +1616,15 @@ export class Frontend extends EventEmitter {
1315
1616
  return;
1316
1617
  }
1317
1618
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1619
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode)) {
1620
+ // @ts-expect-error Accessing private method
1621
+ await this.matterbridge.stopServerNode(device.serverNode);
1622
+ device.serverNode = undefined;
1623
+ }
1624
+ // @ts-expect-error Accessing private method
1625
+ if (plugin.serverNode)
1626
+ await this.matterbridge.stopServerNode(plugin.serverNode);
1627
+ plugin.serverNode = undefined;
1318
1628
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
1319
1629
  await this.matterbridge.plugins.disable(data.params.pluginName);
1320
1630
  this.wssSendSnackbarMessage(`Disabled plugin ${data.params.pluginName}`, 5, 'success');
@@ -1327,32 +1637,42 @@ export class Frontend extends EventEmitter {
1327
1637
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/restartplugin' });
1328
1638
  return;
1329
1639
  }
1640
+ this.wssSendSnackbarMessage(`Restarting plugin ${data.params.pluginName}`, 5, 'info');
1330
1641
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1331
1642
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1643
+ // Stop server nodes
1332
1644
  if (plugin.serverNode) {
1645
+ // @ts-expect-error Accessing private method
1333
1646
  await this.matterbridge.stopServerNode(plugin.serverNode);
1334
1647
  plugin.serverNode = undefined;
1335
1648
  }
1336
- for (const device of this.matterbridge.devices) {
1337
- if (device.plugin === plugin.name) {
1338
- this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
1339
- this.matterbridge.devices.remove(device);
1340
- }
1649
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name)) {
1650
+ // @ts-expect-error Accessing private method
1651
+ if (device.serverNode)
1652
+ await this.matterbridge.stopServerNode(device.serverNode);
1653
+ device.serverNode = undefined;
1654
+ this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
1655
+ this.matterbridge.devices.remove(device);
1341
1656
  }
1657
+ // @ts-expect-error Accessing private method
1342
1658
  if (plugin.type === 'DynamicPlatform' && !plugin.locked)
1343
1659
  await this.matterbridge.createDynamicPlugin(plugin);
1344
1660
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1345
- plugin.restartRequired = false;
1661
+ plugin.restartRequired = false; // Reset plugin restartRequired
1346
1662
  let needRestart = 0;
1347
1663
  for (const plugin of this.matterbridge.plugins) {
1348
1664
  if (plugin.restartRequired)
1349
1665
  needRestart++;
1350
1666
  }
1351
- if (needRestart === 0) {
1352
- this.wssSendRestartNotRequired(true);
1353
- }
1667
+ if (needRestart === 0)
1668
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1669
+ // Start server nodes
1670
+ // @ts-expect-error Accessing private method
1354
1671
  if (plugin.serverNode)
1355
1672
  await this.matterbridge.startServerNode(plugin.serverNode);
1673
+ // @ts-expect-error Accessing private method
1674
+ for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
1675
+ await this.matterbridge.startServerNode(device.serverNode);
1356
1676
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
1357
1677
  this.wssSendRefreshRequired('plugins');
1358
1678
  this.wssSendRefreshRequired('devices');
@@ -1495,6 +1815,9 @@ export class Frontend extends EventEmitter {
1495
1815
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1496
1816
  }
1497
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
1498
1821
  const advertiser = serverNode.env.get(DeviceAdvertiser);
1499
1822
  if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
1500
1823
  await advertiser.advertise(true);
@@ -1558,6 +1881,7 @@ export class Frontend extends EventEmitter {
1558
1881
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
1559
1882
  return;
1560
1883
  }
1884
+ // istanbul ignore next
1561
1885
  const selectDeviceValues = !plugin.platform ? [] : plugin.platform.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1562
1886
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDeviceValues });
1563
1887
  }
@@ -1571,6 +1895,7 @@ export class Frontend extends EventEmitter {
1571
1895
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
1572
1896
  return;
1573
1897
  }
1898
+ // istanbul ignore next
1574
1899
  const selectEntityValues = !plugin.platform ? [] : plugin.platform.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1575
1900
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntityValues });
1576
1901
  }
@@ -1622,22 +1947,22 @@ export class Frontend extends EventEmitter {
1622
1947
  if (isValidString(data.params.value, 4)) {
1623
1948
  this.log.debug('Matterbridge logger level:', data.params.value);
1624
1949
  if (data.params.value === 'Debug') {
1625
- await this.matterbridge.setLogLevel("debug");
1950
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1626
1951
  }
1627
1952
  else if (data.params.value === 'Info') {
1628
- await this.matterbridge.setLogLevel("info");
1953
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1629
1954
  }
1630
1955
  else if (data.params.value === 'Notice') {
1631
- await this.matterbridge.setLogLevel("notice");
1956
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1632
1957
  }
1633
1958
  else if (data.params.value === 'Warn') {
1634
- await this.matterbridge.setLogLevel("warn");
1959
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1635
1960
  }
1636
1961
  else if (data.params.value === 'Error') {
1637
- await this.matterbridge.setLogLevel("error");
1962
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1638
1963
  }
1639
1964
  else if (data.params.value === 'Fatal') {
1640
- await this.matterbridge.setLogLevel("fatal");
1965
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1641
1966
  }
1642
1967
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1643
1968
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
@@ -1648,6 +1973,7 @@ export class Frontend extends EventEmitter {
1648
1973
  this.log.debug('Matterbridge file log:', data.params.value);
1649
1974
  this.matterbridge.fileLogger = data.params.value;
1650
1975
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1976
+ // Create the file logger for matterbridge
1651
1977
  if (data.params.value)
1652
1978
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
1653
1979
  else
@@ -1677,11 +2003,12 @@ export class Frontend extends EventEmitter {
1677
2003
  Logger.level = MatterLogLevel.FATAL;
1678
2004
  }
1679
2005
  this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
1680
- let callbackLogLevel = "notice";
1681
- if (this.matterbridge.getLogLevel() === "info" || Logger.level === MatterLogLevel.INFO)
1682
- callbackLogLevel = "info";
1683
- if (this.matterbridge.getLogLevel() === "debug" || Logger.level === MatterLogLevel.DEBUG)
1684
- 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 */;
1685
2012
  AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
1686
2013
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
1687
2014
  await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
@@ -1733,6 +2060,7 @@ export class Frontend extends EventEmitter {
1733
2060
  }
1734
2061
  break;
1735
2062
  case 'setmatterport':
2063
+ // eslint-disable-next-line no-case-declarations
1736
2064
  const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1737
2065
  if (isValidNumber(port, 5540, 5600)) {
1738
2066
  this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
@@ -1752,6 +2080,7 @@ export class Frontend extends EventEmitter {
1752
2080
  }
1753
2081
  break;
1754
2082
  case 'setmatterdiscriminator':
2083
+ // eslint-disable-next-line no-case-declarations
1755
2084
  const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1756
2085
  if (isValidNumber(discriminator, 0, 4095)) {
1757
2086
  this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
@@ -1771,6 +2100,7 @@ export class Frontend extends EventEmitter {
1771
2100
  }
1772
2101
  break;
1773
2102
  case 'setmatterpasscode':
2103
+ // eslint-disable-next-line no-case-declarations
1774
2104
  const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
1775
2105
  if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
1776
2106
  this.matterbridge.passcode = passcode;
@@ -1816,15 +2146,19 @@ export class Frontend extends EventEmitter {
1816
2146
  return;
1817
2147
  }
1818
2148
  const config = plugin.configJson;
2149
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1819
2150
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2151
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1820
2152
  if (select === 'serial')
1821
2153
  this.log.info(`Selected device serial ${data.params.serial}`);
1822
2154
  if (select === 'name')
1823
2155
  this.log.info(`Selected device name ${data.params.name}`);
1824
2156
  if (config && select && (select === 'serial' || select === 'name')) {
2157
+ // Remove postfix from the serial if it exists
1825
2158
  if (config.postfix) {
1826
2159
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1827
2160
  }
2161
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1828
2162
  if (isValidArray(config.whiteList, 1)) {
1829
2163
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1830
2164
  config.whiteList.push(data.params.serial);
@@ -1833,6 +2167,7 @@ export class Frontend extends EventEmitter {
1833
2167
  config.whiteList.push(data.params.name);
1834
2168
  }
1835
2169
  }
2170
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1836
2171
  if (isValidArray(config.blackList, 1)) {
1837
2172
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1838
2173
  config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
@@ -1860,7 +2195,9 @@ export class Frontend extends EventEmitter {
1860
2195
  return;
1861
2196
  }
1862
2197
  const config = plugin.configJson;
2198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1863
2199
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
2200
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1864
2201
  if (select === 'serial')
1865
2202
  this.log.info(`Unselected device serial ${data.params.serial}`);
1866
2203
  if (select === 'name')
@@ -1869,6 +2206,7 @@ export class Frontend extends EventEmitter {
1869
2206
  if (config.postfix) {
1870
2207
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1871
2208
  }
2209
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1872
2210
  if (isValidArray(config.whiteList, 1)) {
1873
2211
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1874
2212
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
@@ -1877,6 +2215,7 @@ export class Frontend extends EventEmitter {
1877
2215
  config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
1878
2216
  }
1879
2217
  }
2218
+ // Add the serial to the blackList
1880
2219
  if (isValidArray(config.blackList)) {
1881
2220
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1882
2221
  config.blackList.push(data.params.serial);
@@ -1899,6 +2238,7 @@ export class Frontend extends EventEmitter {
1899
2238
  }
1900
2239
  }
1901
2240
  else {
2241
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1902
2242
  const localData = data;
1903
2243
  this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
1904
2244
  sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
@@ -1908,23 +2248,46 @@ export class Frontend extends EventEmitter {
1908
2248
  inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1909
2249
  }
1910
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
+ */
1911
2264
  wssSendLogMessage(level, time, name, message) {
1912
2265
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1913
2266
  return;
1914
2267
  if (!level || !time || !name || !message)
1915
2268
  return;
2269
+ // Remove ANSI escape codes from the message
2270
+ // eslint-disable-next-line no-control-regex
1916
2271
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2272
+ // Remove leading asterisks from the message
1917
2273
  message = message.replace(/^\*+/, '');
2274
+ // Replace all occurrences of \t and \n
1918
2275
  message = message.replace(/[\t\n]/g, '');
2276
+ // Remove non-printable characters
2277
+ // eslint-disable-next-line no-control-regex
1919
2278
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
2279
+ // Replace all occurrences of \" with "
1920
2280
  message = message.replace(/\\"/g, '"');
2281
+ // Define the maximum allowed length for continuous characters without a space
1921
2282
  const maxContinuousLength = 100;
1922
2283
  const keepStartLength = 20;
1923
2284
  const keepEndLength = 20;
2285
+ // Split the message into words
1924
2286
  if (level !== 'spawn') {
1925
2287
  message = message
1926
2288
  .split(' ')
1927
2289
  .map((word) => {
2290
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1928
2291
  if (word.length > maxContinuousLength) {
1929
2292
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1930
2293
  }
@@ -1932,14 +2295,34 @@ export class Frontend extends EventEmitter {
1932
2295
  })
1933
2296
  .join(' ');
1934
2297
  }
2298
+ // Send the message to all connected clients
1935
2299
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
1936
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
+ */
1937
2313
  wssSendRefreshRequired(changed, params) {
1938
2314
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1939
2315
  return;
1940
2316
  this.log.debug('Sending a refresh required message to all connected clients');
2317
+ // Send the message to all connected clients
1941
2318
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
1942
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
+ */
1943
2326
  wssSendRestartRequired(snackbar = true, fixed = false) {
1944
2327
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1945
2328
  return;
@@ -1948,8 +2331,14 @@ export class Frontend extends EventEmitter {
1948
2331
  this.matterbridge.fixedRestartRequired = fixed;
1949
2332
  if (snackbar === true)
1950
2333
  this.wssSendSnackbarMessage(`Restart required`, 0);
2334
+ // Send the message to all connected clients
1951
2335
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
1952
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
+ */
1953
2342
  wssSendRestartNotRequired(snackbar = true) {
1954
2343
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1955
2344
  return;
@@ -1957,64 +2346,145 @@ export class Frontend extends EventEmitter {
1957
2346
  this.matterbridge.restartRequired = false;
1958
2347
  if (snackbar === true)
1959
2348
  this.wssSendCloseSnackbarMessage(`Restart required`);
2349
+ // Send the message to all connected clients
1960
2350
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
1961
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
+ */
1962
2357
  wssSendUpdateRequired(devVersion = false) {
1963
2358
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1964
2359
  return;
1965
2360
  this.log.debug('Sending an update required message to all connected clients');
1966
2361
  this.matterbridge.updateRequired = true;
2362
+ // Send the message to all connected clients
1967
2363
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
1968
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
+ */
1969
2371
  wssSendCpuUpdate(cpuUsage, processCpuUsage) {
1970
2372
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1971
2373
  return;
2374
+ // istanbul ignore else
1972
2375
  if (hasParameter('debug'))
1973
2376
  this.log.debug('Sending a cpu update message to all connected clients');
2377
+ // Send the message to all connected clients
1974
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 } });
1975
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
+ */
1976
2391
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1977
2392
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1978
2393
  return;
2394
+ // istanbul ignore else
1979
2395
  if (hasParameter('debug'))
1980
2396
  this.log.debug('Sending a memory update message to all connected clients');
2397
+ // Send the message to all connected clients
1981
2398
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
1982
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
+ */
1983
2406
  wssSendUptimeUpdate(systemUptime, processUptime) {
1984
2407
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1985
2408
  return;
2409
+ // istanbul ignore else
1986
2410
  if (hasParameter('debug'))
1987
2411
  this.log.debug('Sending a uptime update message to all connected clients');
2412
+ // Send the message to all connected clients
1988
2413
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
1989
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
+ */
1990
2426
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1991
2427
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1992
2428
  return;
1993
2429
  this.log.debug('Sending a snackbar message to all connected clients');
2430
+ // Send the message to all connected clients
1994
2431
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
1995
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
+ */
1996
2439
  wssSendCloseSnackbarMessage(message) {
1997
2440
  if (!this.listening || this.webSocketServer?.clients.size === 0)
1998
2441
  return;
1999
2442
  this.log.debug('Sending a close snackbar message to all connected clients');
2443
+ // Send the message to all connected clients
2000
2444
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
2001
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
+ */
2002
2462
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
2003
2463
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2004
2464
  return;
2005
2465
  this.log.debug('Sending an attribute update message to all connected clients');
2466
+ // Send the message to all connected clients
2006
2467
  this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
2007
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
+ */
2008
2475
  wssBroadcastMessage(msg) {
2009
2476
  if (!this.listening || this.webSocketServer?.clients.size === 0)
2010
2477
  return;
2478
+ // Send the message to all connected clients
2011
2479
  const stringifiedMsg = JSON.stringify(msg);
2012
2480
  if (msg.method !== 'log')
2013
2481
  this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
2014
2482
  this.webSocketServer?.clients.forEach((client) => {
2483
+ // istanbul ignore else
2015
2484
  if (client.readyState === client.OPEN) {
2016
2485
  client.send(stringifiedMsg);
2017
2486
  }
2018
2487
  });
2019
2488
  }
2020
2489
  }
2490
+ //# sourceMappingURL=frontend.js.map