matterbridge 3.2.6-dev-20250906-34345e5 → 3.2.6

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 (279) hide show
  1. package/CHANGELOG.md +2 -1
  2. package/dist/cli.d.ts +26 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +91 -2
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cliEmitter.d.ts +34 -0
  7. package/dist/cliEmitter.d.ts.map +1 -0
  8. package/dist/cliEmitter.js +30 -0
  9. package/dist/cliEmitter.js.map +1 -0
  10. package/dist/clusters/export.d.ts +2 -0
  11. package/dist/clusters/export.d.ts.map +1 -0
  12. package/dist/clusters/export.js +2 -0
  13. package/dist/clusters/export.js.map +1 -0
  14. package/dist/defaultConfigSchema.d.ts +28 -0
  15. package/dist/defaultConfigSchema.d.ts.map +1 -0
  16. package/dist/defaultConfigSchema.js +24 -0
  17. package/dist/defaultConfigSchema.js.map +1 -0
  18. package/dist/deviceManager.d.ts +112 -0
  19. package/dist/deviceManager.d.ts.map +1 -0
  20. package/dist/deviceManager.js +94 -1
  21. package/dist/deviceManager.js.map +1 -0
  22. package/dist/devices/airConditioner.d.ts +98 -0
  23. package/dist/devices/airConditioner.d.ts.map +1 -0
  24. package/dist/devices/airConditioner.js +57 -0
  25. package/dist/devices/airConditioner.js.map +1 -0
  26. package/dist/devices/batteryStorage.d.ts +48 -0
  27. package/dist/devices/batteryStorage.d.ts.map +1 -0
  28. package/dist/devices/batteryStorage.js +48 -1
  29. package/dist/devices/batteryStorage.js.map +1 -0
  30. package/dist/devices/cooktop.d.ts +60 -0
  31. package/dist/devices/cooktop.d.ts.map +1 -0
  32. package/dist/devices/cooktop.js +55 -0
  33. package/dist/devices/cooktop.js.map +1 -0
  34. package/dist/devices/dishwasher.d.ts +71 -0
  35. package/dist/devices/dishwasher.d.ts.map +1 -0
  36. package/dist/devices/dishwasher.js +57 -0
  37. package/dist/devices/dishwasher.js.map +1 -0
  38. package/dist/devices/evse.d.ts +75 -0
  39. package/dist/devices/evse.d.ts.map +1 -0
  40. package/dist/devices/evse.js +74 -10
  41. package/dist/devices/evse.js.map +1 -0
  42. package/dist/devices/export.d.ts +17 -0
  43. package/dist/devices/export.d.ts.map +1 -0
  44. package/dist/devices/export.js +5 -0
  45. package/dist/devices/export.js.map +1 -0
  46. package/dist/devices/extractorHood.d.ts +46 -0
  47. package/dist/devices/extractorHood.d.ts.map +1 -0
  48. package/dist/devices/extractorHood.js +42 -0
  49. package/dist/devices/extractorHood.js.map +1 -0
  50. package/dist/devices/heatPump.d.ts +47 -0
  51. package/dist/devices/heatPump.d.ts.map +1 -0
  52. package/dist/devices/heatPump.js +50 -2
  53. package/dist/devices/heatPump.js.map +1 -0
  54. package/dist/devices/laundryDryer.d.ts +67 -0
  55. package/dist/devices/laundryDryer.d.ts.map +1 -0
  56. package/dist/devices/laundryDryer.js +62 -3
  57. package/dist/devices/laundryDryer.js.map +1 -0
  58. package/dist/devices/laundryWasher.d.ts +81 -0
  59. package/dist/devices/laundryWasher.d.ts.map +1 -0
  60. package/dist/devices/laundryWasher.js +70 -4
  61. package/dist/devices/laundryWasher.js.map +1 -0
  62. package/dist/devices/microwaveOven.d.ts +168 -0
  63. package/dist/devices/microwaveOven.d.ts.map +1 -0
  64. package/dist/devices/microwaveOven.js +88 -5
  65. package/dist/devices/microwaveOven.js.map +1 -0
  66. package/dist/devices/oven.d.ts +105 -0
  67. package/dist/devices/oven.d.ts.map +1 -0
  68. package/dist/devices/oven.js +85 -0
  69. package/dist/devices/oven.js.map +1 -0
  70. package/dist/devices/refrigerator.d.ts +118 -0
  71. package/dist/devices/refrigerator.d.ts.map +1 -0
  72. package/dist/devices/refrigerator.js +102 -0
  73. package/dist/devices/refrigerator.js.map +1 -0
  74. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  75. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  76. package/dist/devices/roboticVacuumCleaner.js +100 -9
  77. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  78. package/dist/devices/solarPower.d.ts +40 -0
  79. package/dist/devices/solarPower.d.ts.map +1 -0
  80. package/dist/devices/solarPower.js +38 -0
  81. package/dist/devices/solarPower.js.map +1 -0
  82. package/dist/devices/speaker.d.ts +83 -0
  83. package/dist/devices/speaker.d.ts.map +1 -0
  84. package/dist/devices/speaker.js +80 -0
  85. package/dist/devices/speaker.js.map +1 -0
  86. package/dist/devices/temperatureControl.d.ts +166 -0
  87. package/dist/devices/temperatureControl.d.ts.map +1 -0
  88. package/dist/devices/temperatureControl.js +25 -3
  89. package/dist/devices/temperatureControl.js.map +1 -0
  90. package/dist/devices/waterHeater.d.ts +111 -0
  91. package/dist/devices/waterHeater.d.ts.map +1 -0
  92. package/dist/devices/waterHeater.js +82 -2
  93. package/dist/devices/waterHeater.js.map +1 -0
  94. package/dist/dgram/coap.d.ts +205 -0
  95. package/dist/dgram/coap.d.ts.map +1 -0
  96. package/dist/dgram/coap.js +126 -13
  97. package/dist/dgram/coap.js.map +1 -0
  98. package/dist/dgram/dgram.d.ts +141 -0
  99. package/dist/dgram/dgram.d.ts.map +1 -0
  100. package/dist/dgram/dgram.js +114 -2
  101. package/dist/dgram/dgram.js.map +1 -0
  102. package/dist/dgram/mb_coap.d.ts +24 -0
  103. package/dist/dgram/mb_coap.d.ts.map +1 -0
  104. package/dist/dgram/mb_coap.js +41 -3
  105. package/dist/dgram/mb_coap.js.map +1 -0
  106. package/dist/dgram/mb_mdns.d.ts +24 -0
  107. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  108. package/dist/dgram/mb_mdns.js +80 -15
  109. package/dist/dgram/mb_mdns.js.map +1 -0
  110. package/dist/dgram/mdns.d.ts +290 -0
  111. package/dist/dgram/mdns.d.ts.map +1 -0
  112. package/dist/dgram/mdns.js +299 -137
  113. package/dist/dgram/mdns.js.map +1 -0
  114. package/dist/dgram/multicast.d.ts +67 -0
  115. package/dist/dgram/multicast.d.ts.map +1 -0
  116. package/dist/dgram/multicast.js +62 -1
  117. package/dist/dgram/multicast.js.map +1 -0
  118. package/dist/dgram/unicast.d.ts +56 -0
  119. package/dist/dgram/unicast.d.ts.map +1 -0
  120. package/dist/dgram/unicast.js +54 -0
  121. package/dist/dgram/unicast.js.map +1 -0
  122. package/dist/frontend.d.ts +313 -0
  123. package/dist/frontend.d.ts.map +1 -0
  124. package/dist/frontend.js +450 -24
  125. package/dist/frontend.js.map +1 -0
  126. package/dist/globalMatterbridge.d.ts +59 -0
  127. package/dist/globalMatterbridge.d.ts.map +1 -0
  128. package/dist/globalMatterbridge.js +47 -0
  129. package/dist/globalMatterbridge.js.map +1 -0
  130. package/dist/helpers.d.ts +48 -0
  131. package/dist/helpers.d.ts.map +1 -0
  132. package/dist/helpers.js +53 -0
  133. package/dist/helpers.js.map +1 -0
  134. package/dist/index.d.ts +33 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +30 -1
  137. package/dist/index.js.map +1 -0
  138. package/dist/jest-utils/jestHelpers.d.ts +103 -0
  139. package/dist/jest-utils/jestHelpers.d.ts.map +1 -0
  140. package/dist/jest-utils/jestHelpers.js +124 -2
  141. package/dist/jest-utils/jestHelpers.js.map +1 -0
  142. package/dist/logger/export.d.ts +2 -0
  143. package/dist/logger/export.d.ts.map +1 -0
  144. package/dist/logger/export.js +1 -0
  145. package/dist/logger/export.js.map +1 -0
  146. package/dist/matter/behaviors.d.ts +2 -0
  147. package/dist/matter/behaviors.d.ts.map +1 -0
  148. package/dist/matter/behaviors.js +2 -0
  149. package/dist/matter/behaviors.js.map +1 -0
  150. package/dist/matter/clusters.d.ts +2 -0
  151. package/dist/matter/clusters.d.ts.map +1 -0
  152. package/dist/matter/clusters.js +2 -0
  153. package/dist/matter/clusters.js.map +1 -0
  154. package/dist/matter/devices.d.ts +2 -0
  155. package/dist/matter/devices.d.ts.map +1 -0
  156. package/dist/matter/devices.js +2 -0
  157. package/dist/matter/devices.js.map +1 -0
  158. package/dist/matter/endpoints.d.ts +2 -0
  159. package/dist/matter/endpoints.d.ts.map +1 -0
  160. package/dist/matter/endpoints.js +2 -0
  161. package/dist/matter/endpoints.js.map +1 -0
  162. package/dist/matter/export.d.ts +5 -0
  163. package/dist/matter/export.d.ts.map +1 -0
  164. package/dist/matter/export.js +3 -0
  165. package/dist/matter/export.js.map +1 -0
  166. package/dist/matter/types.d.ts +3 -0
  167. package/dist/matter/types.d.ts.map +1 -0
  168. package/dist/matter/types.js +3 -0
  169. package/dist/matter/types.js.map +1 -0
  170. package/dist/matterbridge.d.ts +457 -0
  171. package/dist/matterbridge.d.ts.map +1 -0
  172. package/dist/matterbridge.js +780 -49
  173. package/dist/matterbridge.js.map +1 -0
  174. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  175. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  176. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  177. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  178. package/dist/matterbridgeBehaviors.d.ts +1351 -0
  179. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  180. package/dist/matterbridgeBehaviors.js +65 -5
  181. package/dist/matterbridgeBehaviors.js.map +1 -0
  182. package/dist/matterbridgeDeviceTypes.d.ts +761 -0
  183. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  184. package/dist/matterbridgeDeviceTypes.js +630 -17
  185. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  186. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  187. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  188. package/dist/matterbridgeDynamicPlatform.js +36 -0
  189. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  190. package/dist/matterbridgeEndpoint.d.ts +1438 -0
  191. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  192. package/dist/matterbridgeEndpoint.js +1301 -54
  193. package/dist/matterbridgeEndpoint.js.map +1 -0
  194. package/dist/matterbridgeEndpointHelpers.d.ts +407 -0
  195. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  196. package/dist/matterbridgeEndpointHelpers.js +345 -12
  197. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  198. package/dist/matterbridgePlatform.d.ts +379 -0
  199. package/dist/matterbridgePlatform.d.ts.map +1 -0
  200. package/dist/matterbridgePlatform.js +304 -0
  201. package/dist/matterbridgePlatform.js.map +1 -0
  202. package/dist/matterbridgeTypes.d.ts +198 -0
  203. package/dist/matterbridgeTypes.d.ts.map +1 -0
  204. package/dist/matterbridgeTypes.js +25 -0
  205. package/dist/matterbridgeTypes.js.map +1 -0
  206. package/dist/pluginManager.d.ts +270 -0
  207. package/dist/pluginManager.d.ts.map +1 -0
  208. package/dist/pluginManager.js +249 -3
  209. package/dist/pluginManager.js.map +1 -0
  210. package/dist/shelly.d.ts +174 -0
  211. package/dist/shelly.d.ts.map +1 -0
  212. package/dist/shelly.js +168 -7
  213. package/dist/shelly.js.map +1 -0
  214. package/dist/storage/export.d.ts +2 -0
  215. package/dist/storage/export.d.ts.map +1 -0
  216. package/dist/storage/export.js +1 -0
  217. package/dist/storage/export.js.map +1 -0
  218. package/dist/update.d.ts +75 -0
  219. package/dist/update.d.ts.map +1 -0
  220. package/dist/update.js +69 -0
  221. package/dist/update.js.map +1 -0
  222. package/dist/utils/colorUtils.d.ts +99 -0
  223. package/dist/utils/colorUtils.d.ts.map +1 -0
  224. package/dist/utils/colorUtils.js +97 -2
  225. package/dist/utils/colorUtils.js.map +1 -0
  226. package/dist/utils/commandLine.d.ts +59 -0
  227. package/dist/utils/commandLine.d.ts.map +1 -0
  228. package/dist/utils/commandLine.js +54 -0
  229. package/dist/utils/commandLine.js.map +1 -0
  230. package/dist/utils/copyDirectory.d.ts +33 -0
  231. package/dist/utils/copyDirectory.d.ts.map +1 -0
  232. package/dist/utils/copyDirectory.js +38 -1
  233. package/dist/utils/copyDirectory.js.map +1 -0
  234. package/dist/utils/createDirectory.d.ts +34 -0
  235. package/dist/utils/createDirectory.d.ts.map +1 -0
  236. package/dist/utils/createDirectory.js +33 -0
  237. package/dist/utils/createDirectory.js.map +1 -0
  238. package/dist/utils/createZip.d.ts +39 -0
  239. package/dist/utils/createZip.d.ts.map +1 -0
  240. package/dist/utils/createZip.js +47 -2
  241. package/dist/utils/createZip.js.map +1 -0
  242. package/dist/utils/deepCopy.d.ts +32 -0
  243. package/dist/utils/deepCopy.d.ts.map +1 -0
  244. package/dist/utils/deepCopy.js +39 -0
  245. package/dist/utils/deepCopy.js.map +1 -0
  246. package/dist/utils/deepEqual.d.ts +54 -0
  247. package/dist/utils/deepEqual.d.ts.map +1 -0
  248. package/dist/utils/deepEqual.js +72 -1
  249. package/dist/utils/deepEqual.js.map +1 -0
  250. package/dist/utils/error.d.ts +44 -0
  251. package/dist/utils/error.d.ts.map +1 -0
  252. package/dist/utils/error.js +41 -0
  253. package/dist/utils/error.js.map +1 -0
  254. package/dist/utils/export.d.ts +13 -0
  255. package/dist/utils/export.d.ts.map +1 -0
  256. package/dist/utils/export.js +1 -0
  257. package/dist/utils/export.js.map +1 -0
  258. package/dist/utils/hex.d.ts +89 -0
  259. package/dist/utils/hex.d.ts.map +1 -0
  260. package/dist/utils/hex.js +124 -0
  261. package/dist/utils/hex.js.map +1 -0
  262. package/dist/utils/isvalid.d.ts +103 -0
  263. package/dist/utils/isvalid.d.ts.map +1 -0
  264. package/dist/utils/isvalid.js +101 -0
  265. package/dist/utils/isvalid.js.map +1 -0
  266. package/dist/utils/network.d.ts +84 -0
  267. package/dist/utils/network.d.ts.map +1 -0
  268. package/dist/utils/network.js +91 -5
  269. package/dist/utils/network.js.map +1 -0
  270. package/dist/utils/spawn.d.ts +33 -0
  271. package/dist/utils/spawn.d.ts.map +1 -0
  272. package/dist/utils/spawn.js +40 -0
  273. package/dist/utils/spawn.js.map +1 -0
  274. package/dist/utils/wait.d.ts +54 -0
  275. package/dist/utils/wait.d.ts.map +1 -0
  276. package/dist/utils/wait.js +60 -8
  277. package/dist/utils/wait.js.map +1 -0
  278. package/npm-shrinkwrap.json +2 -2
  279. package/package.json +2 -1
package/dist/frontend.js CHANGED
@@ -1,30 +1,126 @@
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.2.0
8
+ * @license Apache-2.0
9
+ *
10
+ * Copyright 2025, 2026, 2027 Luca Liguori.
11
+ *
12
+ * Licensed under the Apache License, Version 2.0 (the "License");
13
+ * you may not use this file except in compliance with the License.
14
+ * You may obtain a copy of the License at
15
+ *
16
+ * http://www.apache.org/licenses/LICENSE-2.0
17
+ *
18
+ * Unless required by applicable law or agreed to in writing, software
19
+ * distributed under the License is distributed on an "AS IS" BASIS,
20
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
+ * See the License for the specific language governing permissions and
22
+ * limitations under the License.
23
+ */
24
+ // Node modules
1
25
  import { createServer } from 'node:http';
2
26
  import https from 'node:https';
3
27
  import os from 'node:os';
4
28
  import path from 'node:path';
5
29
  import { existsSync, promises as fs } from 'node:fs';
6
30
  import EventEmitter from 'node:events';
31
+ // Third-party modules
7
32
  import express from 'express';
8
33
  import WebSocket, { WebSocketServer } from 'ws';
9
34
  import multer from 'multer';
35
+ // AnsiLogger module
10
36
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
37
+ // @matter
11
38
  import { Logger, LogLevel as MatterLogLevel, Lifecycle } from '@matter/main';
12
39
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
40
+ // Matterbridge
13
41
  import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
14
42
  import { plg } from './matterbridgeTypes.js';
15
43
  import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
16
44
  import { cliEmitter, lastCpuUsage } from './cliEmitter.js';
45
+ /**
46
+ * Websocket message ID for logging.
47
+ *
48
+ * @constant {number}
49
+ */
17
50
  export const WS_ID_LOG = 0;
51
+ /**
52
+ * Websocket message ID indicating a refresh is needed.
53
+ *
54
+ * @constant {number}
55
+ */
18
56
  export const WS_ID_REFRESH_NEEDED = 1;
57
+ /**
58
+ * Websocket message ID indicating a restart is needed.
59
+ *
60
+ * @constant {number}
61
+ */
19
62
  export const WS_ID_RESTART_NEEDED = 2;
63
+ /**
64
+ * Websocket message ID indicating a cpu update.
65
+ *
66
+ * @constant {number}
67
+ */
20
68
  export const WS_ID_CPU_UPDATE = 3;
69
+ /**
70
+ * Websocket message ID indicating a memory update.
71
+ *
72
+ * @constant {number}
73
+ */
21
74
  export const WS_ID_MEMORY_UPDATE = 4;
75
+ /**
76
+ * Websocket message ID indicating an uptime update.
77
+ *
78
+ * @constant {number}
79
+ */
22
80
  export const WS_ID_UPTIME_UPDATE = 5;
81
+ /**
82
+ * Websocket message ID indicating a snackbar message.
83
+ *
84
+ * @constant {number}
85
+ */
23
86
  export const WS_ID_SNACKBAR = 6;
87
+ /**
88
+ * Websocket message ID indicating matterbridge has un update available.
89
+ *
90
+ * @constant {number}
91
+ */
24
92
  export const WS_ID_UPDATE_NEEDED = 7;
93
+ /**
94
+ * Websocket message ID indicating a state update.
95
+ *
96
+ * @constant {number}
97
+ */
25
98
  export const WS_ID_STATEUPDATE = 8;
99
+ /**
100
+ * Websocket message ID indicating to close a permanent snackbar message.
101
+ *
102
+ * @constant {number}
103
+ */
26
104
  export const WS_ID_CLOSE_SNACKBAR = 9;
105
+ /**
106
+ * Websocket message ID indicating a shelly system update.
107
+ * check:
108
+ * curl -k http://127.0.0.1:8101/api/updates/sys/check
109
+ * perform:
110
+ * curl -k http://127.0.0.1:8101/api/updates/sys/perform
111
+ *
112
+ * @constant {number}
113
+ */
27
114
  export const WS_ID_SHELLY_SYS_UPDATE = 100;
115
+ /**
116
+ * Websocket message ID indicating a shelly main update.
117
+ * check:
118
+ * curl -k http://127.0.0.1:8101/api/updates/main/check
119
+ * perform:
120
+ * curl -k http://127.0.0.1:8101/api/updates/main/perform
121
+ *
122
+ * @constant {number}
123
+ */
28
124
  export const WS_ID_SHELLY_MAIN_UPDATE = 101;
29
125
  export class Frontend extends EventEmitter {
30
126
  matterbridge;
@@ -37,7 +133,7 @@ export class Frontend extends EventEmitter {
37
133
  constructor(matterbridge) {
38
134
  super();
39
135
  this.matterbridge = matterbridge;
40
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
136
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
41
137
  }
42
138
  set logLevel(logLevel) {
43
139
  this.log.logLevel = logLevel;
@@ -45,10 +141,39 @@ export class Frontend extends EventEmitter {
45
141
  async start(port = 8283) {
46
142
  this.port = port;
47
143
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
48
- const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
144
+ // Initialize multer with the upload directory
145
+ const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
49
146
  const upload = multer({ dest: uploadDir });
147
+ // Create the express app that serves the frontend
50
148
  this.expressApp = express();
149
+ // Inject logging/debug wrapper for route/middleware registration
150
+ /*
151
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
152
+ for (const method of methods) {
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
155
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
156
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
157
+ try {
158
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
159
+ return original(path, ...rest);
160
+ } catch (err) {
161
+ console.error(`[ERROR] Failed to register route: ${path}`);
162
+ throw err;
163
+ }
164
+ };
165
+ }
166
+ */
167
+ // Log all requests to the server for debugging
168
+ /*
169
+ this.expressApp.use((req, res, next) => {
170
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
171
+ next();
172
+ });
173
+ */
174
+ // Serve static files from '/static' endpoint
51
175
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
176
+ // Read the package.json file to get the frontend version
52
177
  try {
53
178
  this.log.debug(`Reading frontend package.json...`);
54
179
  const frontendJson = await fs.readFile(path.join(this.matterbridge.rootDirectory, 'frontend/package.json'), 'utf8');
@@ -56,9 +181,11 @@ export class Frontend extends EventEmitter {
56
181
  this.log.debug(`Frontend version ${CYAN}${this.matterbridge.matterbridgeInformation.frontendVersion}${db}`);
57
182
  }
58
183
  catch (error) {
184
+ // istanbul ignore next
59
185
  this.log.error(`Failed to read frontend package.json: ${error instanceof Error ? error.message : String(error)}`);
60
186
  }
61
187
  if (!hasParameter('ssl')) {
188
+ // Create an HTTP server and attach the express app
62
189
  try {
63
190
  this.log.debug(`Creating HTTP server...`);
64
191
  this.httpServer = createServer(this.expressApp);
@@ -68,6 +195,7 @@ export class Frontend extends EventEmitter {
68
195
  this.emit('server_error', error);
69
196
  return;
70
197
  }
198
+ // Listen on the specified port
71
199
  if (hasParameter('ingress')) {
72
200
  this.httpServer.listen(this.port, '0.0.0.0', () => {
73
201
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -106,6 +234,7 @@ export class Frontend extends EventEmitter {
106
234
  let passphrase;
107
235
  let httpsServerOptions = {};
108
236
  if (existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
237
+ // Load the p12 certificate and the passphrase
109
238
  try {
110
239
  pfx = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
111
240
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
@@ -117,7 +246,7 @@ export class Frontend extends EventEmitter {
117
246
  }
118
247
  try {
119
248
  passphrase = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
120
- passphrase = passphrase.trim();
249
+ passphrase = passphrase.trim(); // Ensure no extra characters
121
250
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
122
251
  }
123
252
  catch (error) {
@@ -128,6 +257,7 @@ export class Frontend extends EventEmitter {
128
257
  httpsServerOptions = { pfx, passphrase };
129
258
  }
130
259
  else {
260
+ // 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.
131
261
  try {
132
262
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
133
263
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
@@ -157,9 +287,10 @@ export class Frontend extends EventEmitter {
157
287
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
158
288
  }
159
289
  if (hasParameter('mtls')) {
160
- httpsServerOptions.requestCert = true;
161
- httpsServerOptions.rejectUnauthorized = true;
290
+ httpsServerOptions.requestCert = true; // Request client certificate
291
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
162
292
  }
293
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
163
294
  try {
164
295
  this.log.debug(`Creating HTTPS server...`);
165
296
  this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
@@ -169,6 +300,7 @@ export class Frontend extends EventEmitter {
169
300
  this.emit('server_error', error);
170
301
  return;
171
302
  }
303
+ // Listen on the specified port
172
304
  if (hasParameter('ingress')) {
173
305
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
174
306
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -198,17 +330,19 @@ export class Frontend extends EventEmitter {
198
330
  return;
199
331
  });
200
332
  }
333
+ // Create a WebSocket server and attach it to the http or https server
201
334
  const wssPort = this.port;
202
335
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
203
336
  this.log.debug(`Creating WebSocketServer on host ${CYAN}${wssHost}${db}...`);
204
337
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
205
338
  this.webSocketServer.on('connection', (ws, request) => {
206
339
  const clientIp = request.socket.remoteAddress;
207
- let callbackLogLevel = "notice";
208
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
209
- callbackLogLevel = "info";
210
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
211
- callbackLogLevel = "debug";
340
+ // Set the global logger callback for the WebSocketServer
341
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
342
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
343
+ callbackLogLevel = "info" /* LogLevel.INFO */;
344
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
345
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
212
346
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
213
347
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
214
348
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -230,6 +364,7 @@ export class Frontend extends EventEmitter {
230
364
  }
231
365
  });
232
366
  ws.on('error', (error) => {
367
+ // istanbul ignore next
233
368
  this.log.error(`WebSocket client error: ${error}`);
234
369
  });
235
370
  });
@@ -243,6 +378,7 @@ export class Frontend extends EventEmitter {
243
378
  this.webSocketServer.on('error', (ws, error) => {
244
379
  this.log.error(`WebSocketServer error: ${error}`);
245
380
  });
381
+ // Subscribe to cli events
246
382
  cliEmitter.removeAllListeners();
247
383
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
248
384
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -253,6 +389,8 @@ export class Frontend extends EventEmitter {
253
389
  cliEmitter.on('cpu', (cpuUsage) => {
254
390
  this.wssSendCpuUpdate(cpuUsage);
255
391
  });
392
+ // Endpoint to validate login code
393
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
256
394
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
257
395
  const { password } = req.body;
258
396
  this.log.debug('The frontend sent /api/login', password);
@@ -271,23 +409,27 @@ export class Frontend extends EventEmitter {
271
409
  this.log.warn('/api/login error wrong password');
272
410
  res.json({ valid: false });
273
411
  }
412
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
274
413
  }
275
414
  catch (error) {
276
415
  this.log.error('/api/login error getting password');
277
416
  res.json({ valid: false });
278
417
  }
279
418
  });
419
+ // Endpoint to provide health check for docker
280
420
  this.expressApp.get('/health', (req, res) => {
281
421
  this.log.debug('Express received /health');
282
422
  const healthStatus = {
283
- status: 'ok',
284
- uptime: process.uptime(),
285
- timestamp: new Date().toISOString(),
423
+ status: 'ok', // Indicate service is healthy
424
+ uptime: process.uptime(), // Server uptime in seconds
425
+ timestamp: new Date().toISOString(), // Current timestamp
286
426
  };
287
427
  res.status(200).json(healthStatus);
288
428
  });
429
+ // Endpoint to provide memory usage details
289
430
  this.expressApp.get('/memory', async (req, res) => {
290
431
  this.log.debug('Express received /memory');
432
+ // Memory usage from process
291
433
  const memoryUsageRaw = process.memoryUsage();
292
434
  const memoryUsage = {
293
435
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -296,10 +438,13 @@ export class Frontend extends EventEmitter {
296
438
  external: this.formatMemoryUsage(memoryUsageRaw.external),
297
439
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
298
440
  };
441
+ // V8 heap statistics
299
442
  const { default: v8 } = await import('node:v8');
300
443
  const heapStatsRaw = v8.getHeapStatistics();
301
444
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
445
+ // Format heapStats
302
446
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
447
+ // Format heapSpaces
303
448
  const heapSpaces = heapSpacesRaw.map((space) => ({
304
449
  ...space,
305
450
  space_size: this.formatMemoryUsage(space.space_size),
@@ -317,19 +462,23 @@ export class Frontend extends EventEmitter {
317
462
  };
318
463
  res.status(200).json(memoryReport);
319
464
  });
465
+ // Endpoint to provide settings
320
466
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
321
467
  this.log.debug('The frontend sent /api/settings');
322
468
  res.json(await this.getApiSettings());
323
469
  });
470
+ // Endpoint to provide plugins
324
471
  this.expressApp.get('/api/plugins', async (req, res) => {
325
472
  this.log.debug('The frontend sent /api/plugins');
326
473
  res.json(this.getPlugins());
327
474
  });
475
+ // Endpoint to provide devices
328
476
  this.expressApp.get('/api/devices', async (req, res) => {
329
477
  this.log.debug('The frontend sent /api/devices');
330
478
  const devices = await this.getDevices();
331
479
  res.json(devices);
332
480
  });
481
+ // Endpoint to view the matterbridge log
333
482
  this.expressApp.get('/api/view-mblog', async (req, res) => {
334
483
  this.log.debug('The frontend sent /api/view-mblog');
335
484
  try {
@@ -342,6 +491,7 @@ export class Frontend extends EventEmitter {
342
491
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
343
492
  }
344
493
  });
494
+ // Endpoint to view the matter.js log
345
495
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
346
496
  this.log.debug('The frontend sent /api/view-mjlog');
347
497
  try {
@@ -354,6 +504,7 @@ export class Frontend extends EventEmitter {
354
504
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
355
505
  }
356
506
  });
507
+ // Endpoint to view the shelly log
357
508
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
358
509
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
359
510
  try {
@@ -366,6 +517,7 @@ export class Frontend extends EventEmitter {
366
517
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
367
518
  }
368
519
  });
520
+ // Endpoint to download the matterbridge log
369
521
  this.expressApp.get('/api/download-mblog', async (req, res) => {
370
522
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
371
523
  try {
@@ -379,12 +531,14 @@ export class Frontend extends EventEmitter {
379
531
  }
380
532
  res.type('text/plain');
381
533
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), 'matterbridge.log', (error) => {
534
+ /* istanbul ignore if */
382
535
  if (error) {
383
536
  this.log.error(`Error downloading log file ${this.matterbridge.matterbridgeLoggerFile}: ${error instanceof Error ? error.message : error}`);
384
537
  res.status(500).send('Error downloading the matterbridge log file');
385
538
  }
386
539
  });
387
540
  });
541
+ // Endpoint to download the matter log
388
542
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
389
543
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
390
544
  try {
@@ -398,12 +552,14 @@ export class Frontend extends EventEmitter {
398
552
  }
399
553
  res.type('text/plain');
400
554
  res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
555
+ /* istanbul ignore if */
401
556
  if (error) {
402
557
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
403
558
  res.status(500).send('Error downloading the matter log file');
404
559
  }
405
560
  });
406
561
  });
562
+ // Endpoint to download the shelly log
407
563
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
408
564
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
409
565
  try {
@@ -417,74 +573,90 @@ export class Frontend extends EventEmitter {
417
573
  }
418
574
  res.type('text/plain');
419
575
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
576
+ /* istanbul ignore if */
420
577
  if (error) {
421
578
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
422
579
  res.status(500).send('Error downloading Shelly system log file');
423
580
  }
424
581
  });
425
582
  });
583
+ // Endpoint to download the matterbridge storage directory
426
584
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
427
585
  this.log.debug('The frontend sent /api/download-mbstorage');
428
586
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
429
587
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
588
+ /* istanbul ignore if */
430
589
  if (error) {
431
590
  this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
432
591
  res.status(500).send('Error downloading the matterbridge storage file');
433
592
  }
434
593
  });
435
594
  });
595
+ // Endpoint to download the matter storage file
436
596
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
437
597
  this.log.debug('The frontend sent /api/download-mjstorage');
438
598
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
439
599
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => {
600
+ /* istanbul ignore if */
440
601
  if (error) {
441
602
  this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`);
442
603
  res.status(500).send('Error downloading the matter storage zip file');
443
604
  }
444
605
  });
445
606
  });
607
+ // Endpoint to download the matterbridge plugin directory
446
608
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
447
609
  this.log.debug('The frontend sent /api/download-pluginstorage');
448
610
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
449
611
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
612
+ /* istanbul ignore if */
450
613
  if (error) {
451
614
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
452
615
  res.status(500).send('Error downloading the matterbridge plugin storage file');
453
616
  }
454
617
  });
455
618
  });
619
+ // Endpoint to download the matterbridge plugin config files
456
620
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
457
621
  this.log.debug('The frontend sent /api/download-pluginconfig');
458
622
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
459
623
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
624
+ /* istanbul ignore if */
460
625
  if (error) {
461
626
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
462
627
  res.status(500).send('Error downloading the matterbridge plugin config file');
463
628
  }
464
629
  });
465
630
  });
631
+ // Endpoint to download the matterbridge backup (created with the backup command)
466
632
  this.expressApp.get('/api/download-backup', async (req, res) => {
467
633
  this.log.debug('The frontend sent /api/download-backup');
468
634
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
635
+ /* istanbul ignore if */
469
636
  if (error) {
470
637
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
471
638
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
472
639
  }
473
640
  });
474
641
  });
642
+ // Endpoint to upload a package
475
643
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
476
644
  const { filename } = req.body;
477
645
  const file = req.file;
646
+ /* istanbul ignore if */
478
647
  if (!file || !filename) {
479
648
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
480
649
  res.status(400).send('Invalid request: file and filename are required');
481
650
  return;
482
651
  }
483
652
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
653
+ // Define the path where the plugin file will be saved
484
654
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
485
655
  try {
656
+ // Move the uploaded file to the specified path
486
657
  await fs.rename(file.path, filePath);
487
658
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
659
+ // Install the plugin package
488
660
  if (filename.endsWith('.tgz')) {
489
661
  const { spawnCommand } = await import('./utils/spawn.js');
490
662
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
@@ -504,6 +676,7 @@ export class Frontend extends EventEmitter {
504
676
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
505
677
  }
506
678
  });
679
+ // Fallback for routing (must be the last route)
507
680
  this.expressApp.use((req, res) => {
508
681
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
509
682
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -512,13 +685,16 @@ export class Frontend extends EventEmitter {
512
685
  }
513
686
  async stop() {
514
687
  this.log.debug('Stopping the frontend...');
688
+ // Remove listeners from the express app
515
689
  if (this.expressApp) {
516
690
  this.expressApp.removeAllListeners();
517
691
  this.expressApp = undefined;
518
692
  this.log.debug('Frontend app closed successfully');
519
693
  }
694
+ // Close the WebSocket server
520
695
  if (this.webSocketServer) {
521
696
  this.log.debug('Closing WebSocket server...');
697
+ // Close all active connections
522
698
  this.webSocketServer.clients.forEach((client) => {
523
699
  if (client.readyState === WebSocket.OPEN) {
524
700
  client.close();
@@ -527,6 +703,7 @@ export class Frontend extends EventEmitter {
527
703
  await withTimeout(new Promise((resolve) => {
528
704
  this.webSocketServer?.close((error) => {
529
705
  if (error) {
706
+ // istanbul ignore next
530
707
  this.log.error(`Error closing WebSocket server: ${error}`);
531
708
  }
532
709
  else {
@@ -539,11 +716,13 @@ export class Frontend extends EventEmitter {
539
716
  this.webSocketServer.removeAllListeners();
540
717
  this.webSocketServer = undefined;
541
718
  }
719
+ // Close the http server
542
720
  if (this.httpServer) {
543
721
  this.log.debug('Closing http server...');
544
722
  await withTimeout(new Promise((resolve) => {
545
723
  this.httpServer?.close((error) => {
546
724
  if (error) {
725
+ // istanbul ignore next
547
726
  this.log.error(`Error closing http server: ${error}`);
548
727
  }
549
728
  else {
@@ -557,11 +736,13 @@ export class Frontend extends EventEmitter {
557
736
  this.httpServer = undefined;
558
737
  this.log.debug('Frontend http server closed successfully');
559
738
  }
739
+ // Close the https server
560
740
  if (this.httpsServer) {
561
741
  this.log.debug('Closing https server...');
562
742
  await withTimeout(new Promise((resolve) => {
563
743
  this.httpsServer?.close((error) => {
564
744
  if (error) {
745
+ // istanbul ignore next
565
746
  this.log.error(`Error closing https server: ${error}`);
566
747
  }
567
748
  else {
@@ -577,6 +758,7 @@ export class Frontend extends EventEmitter {
577
758
  }
578
759
  this.log.debug('Frontend stopped successfully');
579
760
  }
761
+ // Function to format bytes to KB, MB, or GB
580
762
  formatMemoryUsage = (bytes) => {
581
763
  if (bytes >= 1024 ** 3) {
582
764
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -588,6 +770,7 @@ export class Frontend extends EventEmitter {
588
770
  return `${(bytes / 1024).toFixed(2)} KB`;
589
771
  }
590
772
  };
773
+ // Function to format system uptime with only the most significant unit
591
774
  formatOsUpTime = (seconds) => {
592
775
  if (seconds >= 86400) {
593
776
  const days = Math.floor(seconds / 86400);
@@ -603,7 +786,13 @@ export class Frontend extends EventEmitter {
603
786
  }
604
787
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
605
788
  };
789
+ /**
790
+ * Retrieves the api settings data.
791
+ *
792
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
793
+ */
606
794
  async getApiSettings() {
795
+ // Update the system information
607
796
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
608
797
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
609
798
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -612,6 +801,7 @@ export class Frontend extends EventEmitter {
612
801
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
613
802
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
614
803
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
804
+ // Update the matterbridge information
615
805
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
616
806
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
617
807
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
@@ -623,6 +813,7 @@ export class Frontend extends EventEmitter {
623
813
  this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
624
814
  this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
625
815
  this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
816
+ // Update the matterbridge information in bridge mode
626
817
  if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode && !this.matterbridge.hasCleanupStarted) {
627
818
  this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.serverNode.state.commissioning.commissioned;
628
819
  this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.qrPairingCode;
@@ -632,6 +823,12 @@ export class Frontend extends EventEmitter {
632
823
  }
633
824
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
634
825
  }
826
+ /**
827
+ * Retrieves the reachable attribute.
828
+ *
829
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
830
+ * @returns {boolean} The reachable attribute.
831
+ */
635
832
  getReachability(device) {
636
833
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
637
834
  return false;
@@ -643,6 +840,12 @@ export class Frontend extends EventEmitter {
643
840
  return true;
644
841
  return false;
645
842
  }
843
+ /**
844
+ * Retrieves the power source attribute.
845
+ *
846
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
847
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
848
+ */
646
849
  getPowerSource(endpoint) {
647
850
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
648
851
  return undefined;
@@ -658,13 +861,21 @@ export class Frontend extends EventEmitter {
658
861
  }
659
862
  return;
660
863
  };
864
+ // Root endpoint
661
865
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
662
866
  return powerSource(endpoint);
867
+ // Child endpoints
663
868
  for (const child of endpoint.getChildEndpoints()) {
664
869
  if (child.hasClusterServer(PowerSource.Cluster.id))
665
870
  return powerSource(child);
666
871
  }
667
872
  }
873
+ /**
874
+ * Retrieves the commissioned status, matter pairing codes, fabrics and sessions from a given device in server mode.
875
+ *
876
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the data from.
877
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
878
+ */
668
879
  getMatterDataFromDevice(device) {
669
880
  if (device.mode === 'server' && device.serverNode) {
670
881
  return {
@@ -677,6 +888,13 @@ export class Frontend extends EventEmitter {
677
888
  };
678
889
  }
679
890
  }
891
+ /**
892
+ * Retrieves the cluster text description from a given device.
893
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
894
+ *
895
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
896
+ * @returns {string} The attributes description of the cluster servers in the device.
897
+ */
680
898
  getClusterTextFromDevice(device) {
681
899
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
682
900
  return '';
@@ -687,6 +905,7 @@ export class Frontend extends EventEmitter {
687
905
  if (composed)
688
906
  return 'Composed: ' + composed.value;
689
907
  }
908
+ // istanbul ignore next cause is not reachable
690
909
  return '';
691
910
  };
692
911
  const getFixedLabel = (device) => {
@@ -696,11 +915,13 @@ export class Frontend extends EventEmitter {
696
915
  if (composed)
697
916
  return 'Composed: ' + composed.value;
698
917
  }
918
+ // istanbul ignore next cause is not reacheable
699
919
  return '';
700
920
  };
701
921
  let attributes = '';
702
922
  let supportedModes = [];
703
923
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
924
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
704
925
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
705
926
  return;
706
927
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -790,11 +1011,17 @@ export class Frontend extends EventEmitter {
790
1011
  if (clusterName === 'userLabel' && attributeName === 'labelList')
791
1012
  attributes += `${getUserLabel(device)} `;
792
1013
  });
1014
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
793
1015
  return attributes.trimStart().trimEnd();
794
1016
  }
1017
+ /**
1018
+ * Retrieves the registered plugins sanitized for res.json().
1019
+ *
1020
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1021
+ */
795
1022
  getPlugins() {
796
1023
  if (this.matterbridge.hasCleanupStarted)
797
- return [];
1024
+ return []; // Skip if cleanup has started
798
1025
  const baseRegisteredPlugins = [];
799
1026
  for (const plugin of this.matterbridge.plugins) {
800
1027
  baseRegisteredPlugins.push({
@@ -824,6 +1051,7 @@ export class Frontend extends EventEmitter {
824
1051
  schemaJson: plugin.schemaJson,
825
1052
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
826
1053
  hasBlackList: plugin.configJson?.blackList !== undefined,
1054
+ // Childbridge mode specific data
827
1055
  paired: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.commissioned : undefined,
828
1056
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.qrPairingCode : undefined,
829
1057
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.manualPairingCode : undefined,
@@ -833,13 +1061,21 @@ export class Frontend extends EventEmitter {
833
1061
  }
834
1062
  return baseRegisteredPlugins;
835
1063
  }
1064
+ /**
1065
+ * Retrieves the devices from Matterbridge.
1066
+ *
1067
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1068
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1069
+ */
836
1070
  async getDevices(pluginName) {
837
1071
  if (this.matterbridge.hasCleanupStarted)
838
- return [];
1072
+ return []; // Skip if cleanup has started
839
1073
  const devices = [];
840
1074
  for (const device of this.matterbridge.devices.array()) {
1075
+ // Filter by pluginName if provided
841
1076
  if (pluginName && pluginName !== device.plugin)
842
1077
  continue;
1078
+ // Check if the device has the required properties
843
1079
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
844
1080
  continue;
845
1081
  devices.push({
@@ -859,22 +1095,37 @@ export class Frontend extends EventEmitter {
859
1095
  }
860
1096
  return devices;
861
1097
  }
1098
+ /**
1099
+ * Retrieves the clusters from a given plugin and endpoint number.
1100
+ *
1101
+ * Response for /api/clusters
1102
+ *
1103
+ * @param {string} pluginName - The name of the plugin.
1104
+ * @param {number} endpointNumber - The endpoint number.
1105
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1106
+ */
862
1107
  getClusters(pluginName, endpointNumber) {
863
1108
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
864
1109
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
865
1110
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
866
1111
  return;
867
1112
  }
1113
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1114
+ // Get the device types from the main endpoint
868
1115
  const deviceTypes = [];
869
1116
  const clusters = [];
870
1117
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
871
1118
  deviceTypes.push(d.deviceType);
872
1119
  });
1120
+ // Get the clusters from the main endpoint
873
1121
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
874
1122
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
875
1123
  return;
876
1124
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
877
1125
  return;
1126
+ // console.log(
1127
+ // `${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}`,
1128
+ // );
878
1129
  clusters.push({
879
1130
  endpoint: endpoint.number.toString(),
880
1131
  id: 'main',
@@ -887,12 +1138,19 @@ export class Frontend extends EventEmitter {
887
1138
  attributeLocalValue: attributeValue,
888
1139
  });
889
1140
  });
1141
+ // Get the child endpoints
890
1142
  const childEndpoints = endpoint.getChildEndpoints();
1143
+ // if (childEndpoints.length === 0) {
1144
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1145
+ // }
891
1146
  childEndpoints.forEach((childEndpoint) => {
1147
+ // istanbul ignore if cause is not reachable: should never happen but ...
892
1148
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
893
1149
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
894
1150
  return;
895
1151
  }
1152
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1153
+ // Get the device types of the child endpoint
896
1154
  const deviceTypes = [];
897
1155
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
898
1156
  deviceTypes.push(d.deviceType);
@@ -902,9 +1160,12 @@ export class Frontend extends EventEmitter {
902
1160
  return;
903
1161
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
904
1162
  return;
1163
+ // console.log(
1164
+ // `${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}`,
1165
+ // );
905
1166
  clusters.push({
906
1167
  endpoint: childEndpoint.number.toString(),
907
- id: childEndpoint.maybeId ?? 'null',
1168
+ id: childEndpoint.maybeId ?? 'null', // Never happens
908
1169
  deviceTypes,
909
1170
  clusterName: capitalizeFirstLetter(clusterName),
910
1171
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -917,6 +1178,13 @@ export class Frontend extends EventEmitter {
917
1178
  });
918
1179
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
919
1180
  }
1181
+ /**
1182
+ * Handles incoming websocket messages for the Matterbridge frontend.
1183
+ *
1184
+ * @param {WebSocket} client - The websocket client that sent the message.
1185
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1186
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1187
+ */
920
1188
  async wsMessageHandler(client, message) {
921
1189
  let data;
922
1190
  try {
@@ -963,35 +1231,48 @@ export class Frontend extends EventEmitter {
963
1231
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
964
1232
  const packageName = data.params.packageName.replace(/@.*$/, '');
965
1233
  if (data.params.restart === false && packageName !== 'matterbridge') {
1234
+ // The install comes from InstallPlugins
966
1235
  this.matterbridge.plugins
967
1236
  .add(packageName)
968
1237
  .then((plugin) => {
1238
+ // istanbul ignore next if
969
1239
  if (plugin) {
1240
+ // The plugin is not registered
970
1241
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
1242
+ // In childbridge mode the plugins server node is not started when added
971
1243
  if (this.matterbridge.bridgeMode === 'childbridge')
972
1244
  this.wssSendRestartRequired(true, true);
973
1245
  this.matterbridge.plugins
974
1246
  .load(plugin, true, 'The plugin has been added', true)
1247
+ // eslint-disable-next-line promise/no-nesting
975
1248
  .then(() => {
976
1249
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
977
1250
  this.wssSendRefreshRequired('plugins');
978
1251
  return;
979
1252
  })
1253
+ // eslint-disable-next-line promise/no-nesting
980
1254
  .catch((_error) => {
1255
+ //
981
1256
  });
982
1257
  }
983
1258
  else {
1259
+ // The plugin is already registered
984
1260
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
985
1261
  this.wssSendRefreshRequired('plugins');
986
1262
  this.wssSendRestartRequired(true, true);
987
1263
  }
988
1264
  return;
989
1265
  })
1266
+ // eslint-disable-next-line promise/no-nesting
990
1267
  .catch((_error) => {
1268
+ //
991
1269
  });
992
1270
  }
993
1271
  else {
1272
+ // The package is matterbridge
1273
+ // istanbul ignore next
994
1274
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
1275
+ // istanbul ignore next if
995
1276
  if (this.matterbridge.restartMode !== '') {
996
1277
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
997
1278
  this.matterbridge.shutdownProcess();
@@ -1013,7 +1294,9 @@ export class Frontend extends EventEmitter {
1013
1294
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1014
1295
  return;
1015
1296
  }
1297
+ // The package is a plugin
1016
1298
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
1299
+ // istanbul ignore next if
1017
1300
  if (plugin) {
1018
1301
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1019
1302
  await this.matterbridge.plugins.remove(data.params.packageName);
@@ -1021,6 +1304,7 @@ export class Frontend extends EventEmitter {
1021
1304
  this.wssSendRefreshRequired('plugins');
1022
1305
  this.wssSendRefreshRequired('devices');
1023
1306
  }
1307
+ // Uninstall the package
1024
1308
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1025
1309
  const { spawnCommand } = await import('./utils/spawn.js');
1026
1310
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1061,6 +1345,7 @@ export class Frontend extends EventEmitter {
1061
1345
  return;
1062
1346
  })
1063
1347
  .catch((_error) => {
1348
+ //
1064
1349
  });
1065
1350
  }
1066
1351
  else {
@@ -1107,6 +1392,7 @@ export class Frontend extends EventEmitter {
1107
1392
  return;
1108
1393
  })
1109
1394
  .catch((_error) => {
1395
+ //
1110
1396
  });
1111
1397
  }
1112
1398
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1132,6 +1418,7 @@ export class Frontend extends EventEmitter {
1132
1418
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1133
1419
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1134
1420
  if (plugin.serverNode) {
1421
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1135
1422
  await this.matterbridge.stopServerNode(plugin.serverNode);
1136
1423
  plugin.serverNode = undefined;
1137
1424
  }
@@ -1142,15 +1429,16 @@ export class Frontend extends EventEmitter {
1142
1429
  }
1143
1430
  }
1144
1431
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1145
- plugin.restartRequired = false;
1432
+ plugin.restartRequired = false; // Reset plugin restartRequired
1146
1433
  let needRestart = 0;
1147
1434
  for (const plugin of this.matterbridge.plugins) {
1148
1435
  if (plugin.restartRequired)
1149
1436
  needRestart++;
1150
1437
  }
1151
1438
  if (needRestart === 0) {
1152
- this.wssSendRestartNotRequired(true);
1439
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1153
1440
  }
1441
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1154
1442
  if (plugin.serverNode)
1155
1443
  await this.matterbridge.startServerNode(plugin.serverNode);
1156
1444
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1256,6 +1544,8 @@ export class Frontend extends EventEmitter {
1256
1544
  else if (data.method === '/api/advertise') {
1257
1545
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1258
1546
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1547
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1548
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1259
1549
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1260
1550
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1261
1551
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1378,22 +1668,22 @@ export class Frontend extends EventEmitter {
1378
1668
  if (isValidString(data.params.value, 4)) {
1379
1669
  this.log.debug('Matterbridge logger level:', data.params.value);
1380
1670
  if (data.params.value === 'Debug') {
1381
- await this.matterbridge.setLogLevel("debug");
1671
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1382
1672
  }
1383
1673
  else if (data.params.value === 'Info') {
1384
- await this.matterbridge.setLogLevel("info");
1674
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1385
1675
  }
1386
1676
  else if (data.params.value === 'Notice') {
1387
- await this.matterbridge.setLogLevel("notice");
1677
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1388
1678
  }
1389
1679
  else if (data.params.value === 'Warn') {
1390
- await this.matterbridge.setLogLevel("warn");
1680
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1391
1681
  }
1392
1682
  else if (data.params.value === 'Error') {
1393
- await this.matterbridge.setLogLevel("error");
1683
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1394
1684
  }
1395
1685
  else if (data.params.value === 'Fatal') {
1396
- await this.matterbridge.setLogLevel("fatal");
1686
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1397
1687
  }
1398
1688
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1399
1689
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1404,6 +1694,7 @@ export class Frontend extends EventEmitter {
1404
1694
  this.log.debug('Matterbridge file log:', data.params.value);
1405
1695
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1406
1696
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1697
+ // Create the file logger for matterbridge
1407
1698
  if (data.params.value)
1408
1699
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1409
1700
  else
@@ -1557,15 +1848,19 @@ export class Frontend extends EventEmitter {
1557
1848
  return;
1558
1849
  }
1559
1850
  const config = plugin.configJson;
1851
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1560
1852
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1853
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1561
1854
  if (select === 'serial')
1562
1855
  this.log.info(`Selected device serial ${data.params.serial}`);
1563
1856
  if (select === 'name')
1564
1857
  this.log.info(`Selected device name ${data.params.name}`);
1565
1858
  if (config && select && (select === 'serial' || select === 'name')) {
1859
+ // Remove postfix from the serial if it exists
1566
1860
  if (config.postfix) {
1567
1861
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1568
1862
  }
1863
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1569
1864
  if (isValidArray(config.whiteList, 1)) {
1570
1865
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1571
1866
  config.whiteList.push(data.params.serial);
@@ -1574,6 +1869,7 @@ export class Frontend extends EventEmitter {
1574
1869
  config.whiteList.push(data.params.name);
1575
1870
  }
1576
1871
  }
1872
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1577
1873
  if (isValidArray(config.blackList, 1)) {
1578
1874
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1579
1875
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1604,7 +1900,9 @@ export class Frontend extends EventEmitter {
1604
1900
  return;
1605
1901
  }
1606
1902
  const config = plugin.configJson;
1903
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1607
1904
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1905
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1608
1906
  if (select === 'serial')
1609
1907
  this.log.info(`Unselected device serial ${data.params.serial}`);
1610
1908
  if (select === 'name')
@@ -1613,6 +1911,7 @@ export class Frontend extends EventEmitter {
1613
1911
  if (config.postfix) {
1614
1912
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1615
1913
  }
1914
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1616
1915
  if (isValidArray(config.whiteList, 1)) {
1617
1916
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1618
1917
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1621,6 +1920,7 @@ export class Frontend extends EventEmitter {
1621
1920
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1622
1921
  }
1623
1922
  }
1923
+ // Add the serial to the blackList
1624
1924
  if (isValidArray(config.blackList)) {
1625
1925
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1626
1926
  config.blackList.push(data.params.serial);
@@ -1654,126 +1954,251 @@ export class Frontend extends EventEmitter {
1654
1954
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1655
1955
  }
1656
1956
  }
1957
+ /**
1958
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1959
+ *
1960
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1961
+ * @param {string} time - The time string of the message
1962
+ * @param {string} name - The logger name of the message
1963
+ * @param {string} message - The content of the message.
1964
+ *
1965
+ * @remarks
1966
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1967
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1968
+ * The function sends the message to all connected clients.
1969
+ */
1657
1970
  wssSendMessage(level, time, name, message) {
1658
1971
  if (!level || !time || !name || !message)
1659
1972
  return;
1973
+ // Remove ANSI escape codes from the message
1974
+ // eslint-disable-next-line no-control-regex
1660
1975
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1976
+ // Remove leading asterisks from the message
1661
1977
  message = message.replace(/^\*+/, '');
1978
+ // Replace all occurrences of \t and \n
1662
1979
  message = message.replace(/[\t\n]/g, '');
1980
+ // Remove non-printable characters
1981
+ // eslint-disable-next-line no-control-regex
1663
1982
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1983
+ // Replace all occurrences of \" with "
1664
1984
  message = message.replace(/\\"/g, '"');
1985
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1665
1986
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1987
+ // Define the maximum allowed length for continuous characters without a space
1666
1988
  const maxContinuousLength = 100;
1667
1989
  const keepStartLength = 20;
1668
1990
  const keepEndLength = 20;
1991
+ // Split the message into words
1669
1992
  message = message
1670
1993
  .split(' ')
1671
1994
  .map((word) => {
1995
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1672
1996
  if (word.length > maxContinuousLength) {
1673
1997
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1674
1998
  }
1675
1999
  return word;
1676
2000
  })
1677
2001
  .join(' ');
2002
+ // Send the message to all connected clients
1678
2003
  this.webSocketServer?.clients.forEach((client) => {
1679
2004
  if (client.readyState === WebSocket.OPEN) {
1680
2005
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1681
2006
  }
1682
2007
  });
1683
2008
  }
2009
+ /**
2010
+ * Sends a need to refresh WebSocket message to all connected clients.
2011
+ *
2012
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
2013
+ * possible values:
2014
+ * - 'matterbridgeLatestVersion'
2015
+ * - 'matterbridgeDevVersion'
2016
+ * - 'matterbridgeAdvertise'
2017
+ * - 'online'
2018
+ * - 'offline'
2019
+ * - 'reachability'
2020
+ * - 'settings'
2021
+ * - 'plugins'
2022
+ * - 'pluginsRestart'
2023
+ * - 'devices'
2024
+ * - 'fabrics'
2025
+ * - 'sessions'
2026
+ */
1684
2027
  wssSendRefreshRequired(changed = null) {
1685
2028
  this.log.debug('Sending a refresh required message to all connected clients');
2029
+ // Send the message to all connected clients
1686
2030
  this.webSocketServer?.clients.forEach((client) => {
1687
2031
  if (client.readyState === WebSocket.OPEN) {
1688
2032
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1689
2033
  }
1690
2034
  });
1691
2035
  }
2036
+ /**
2037
+ * Sends a need to restart WebSocket message to all connected clients.
2038
+ *
2039
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2040
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2041
+ */
1692
2042
  wssSendRestartRequired(snackbar = true, fixed = false) {
1693
2043
  this.log.debug('Sending a restart required message to all connected clients');
1694
2044
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1695
2045
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = fixed;
1696
2046
  if (snackbar === true)
1697
2047
  this.wssSendSnackbarMessage(`Restart required`, 0);
2048
+ // Send the message to all connected clients
1698
2049
  this.webSocketServer?.clients.forEach((client) => {
1699
2050
  if (client.readyState === WebSocket.OPEN) {
1700
2051
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: { fixed } }));
1701
2052
  }
1702
2053
  });
1703
2054
  }
2055
+ /**
2056
+ * Sends a no need to restart WebSocket message to all connected clients.
2057
+ *
2058
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients.
2059
+ */
1704
2060
  wssSendRestartNotRequired(snackbar = true) {
1705
2061
  this.log.debug('Sending a restart not required message to all connected clients');
1706
2062
  this.matterbridge.matterbridgeInformation.restartRequired = false;
1707
2063
  if (snackbar === true)
1708
2064
  this.wssSendCloseSnackbarMessage(`Restart required`);
2065
+ // Send the message to all connected clients
1709
2066
  this.webSocketServer?.clients.forEach((client) => {
1710
2067
  if (client.readyState === WebSocket.OPEN) {
1711
2068
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', params: {} }));
1712
2069
  }
1713
2070
  });
1714
2071
  }
2072
+ /**
2073
+ * Sends a need to update WebSocket message to all connected clients.
2074
+ *
2075
+ * @param {boolean} devVersion - If true, the update is for a development version.
2076
+ */
1715
2077
  wssSendUpdateRequired(devVersion = false) {
1716
2078
  this.log.debug('Sending an update required message to all connected clients');
1717
2079
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2080
+ // Send the message to all connected clients
1718
2081
  this.webSocketServer?.clients.forEach((client) => {
1719
2082
  if (client.readyState === WebSocket.OPEN) {
1720
2083
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: devVersion ? 'update_required_dev' : 'update_required', params: {} }));
1721
2084
  }
1722
2085
  });
1723
2086
  }
2087
+ /**
2088
+ * Sends a cpu update message to all connected clients.
2089
+ *
2090
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2091
+ */
1724
2092
  wssSendCpuUpdate(cpuUsage) {
1725
2093
  if (hasParameter('debug'))
1726
2094
  this.log.debug('Sending a cpu update message to all connected clients');
2095
+ // Send the message to all connected clients
1727
2096
  this.webSocketServer?.clients.forEach((client) => {
1728
2097
  if (client.readyState === WebSocket.OPEN) {
1729
2098
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1730
2099
  }
1731
2100
  });
1732
2101
  }
2102
+ /**
2103
+ * Sends a memory update message to all connected clients.
2104
+ *
2105
+ * @param {string} totalMemory - The total memory in bytes.
2106
+ * @param {string} freeMemory - The free memory in bytes.
2107
+ * @param {string} rss - The resident set size in bytes.
2108
+ * @param {string} heapTotal - The total heap memory in bytes.
2109
+ * @param {string} heapUsed - The used heap memory in bytes.
2110
+ * @param {string} external - The external memory in bytes.
2111
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2112
+ */
1733
2113
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1734
2114
  if (hasParameter('debug'))
1735
2115
  this.log.debug('Sending a memory update message to all connected clients');
2116
+ // Send the message to all connected clients
1736
2117
  this.webSocketServer?.clients.forEach((client) => {
1737
2118
  if (client.readyState === WebSocket.OPEN) {
1738
2119
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1739
2120
  }
1740
2121
  });
1741
2122
  }
2123
+ /**
2124
+ * Sends an uptime update message to all connected clients.
2125
+ *
2126
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2127
+ * @param {string} processUptime - The process uptime in a human-readable format.
2128
+ */
1742
2129
  wssSendUptimeUpdate(systemUptime, processUptime) {
1743
2130
  if (hasParameter('debug'))
1744
2131
  this.log.debug('Sending a uptime update message to all connected clients');
2132
+ // Send the message to all connected clients
1745
2133
  this.webSocketServer?.clients.forEach((client) => {
1746
2134
  if (client.readyState === WebSocket.OPEN) {
1747
2135
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1748
2136
  }
1749
2137
  });
1750
2138
  }
2139
+ /**
2140
+ * Sends an open snackbar message to all connected clients.
2141
+ *
2142
+ * @param {string} message - The message to send.
2143
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2144
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2145
+ */
1751
2146
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1752
2147
  this.log.debug('Sending a snackbar message to all connected clients');
2148
+ // Send the message to all connected clients
1753
2149
  this.webSocketServer?.clients.forEach((client) => {
1754
2150
  if (client.readyState === WebSocket.OPEN) {
1755
2151
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1756
2152
  }
1757
2153
  });
1758
2154
  }
2155
+ /**
2156
+ * Sends a close snackbar message to all connected clients.
2157
+ *
2158
+ * @param {string} message - The message to send.
2159
+ */
1759
2160
  wssSendCloseSnackbarMessage(message) {
1760
2161
  this.log.debug('Sending a close snackbar message to all connected clients');
2162
+ // Send the message to all connected clients
1761
2163
  this.webSocketServer?.clients.forEach((client) => {
1762
2164
  if (client.readyState === WebSocket.OPEN) {
1763
2165
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1764
2166
  }
1765
2167
  });
1766
2168
  }
2169
+ /**
2170
+ * Sends an attribute update message to all connected WebSocket clients.
2171
+ *
2172
+ * @param {string | undefined} plugin - The name of the plugin.
2173
+ * @param {string | undefined} serialNumber - The serial number of the device.
2174
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2175
+ * @param {string} cluster - The cluster name where the attribute belongs.
2176
+ * @param {string} attribute - The name of the attribute that changed.
2177
+ * @param {number | string | boolean} value - The new value of the attribute.
2178
+ *
2179
+ * @remarks
2180
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2181
+ * with the updated attribute information.
2182
+ */
1767
2183
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1768
2184
  this.log.debug('Sending an attribute update message to all connected clients');
2185
+ // Send the message to all connected clients
1769
2186
  this.webSocketServer?.clients.forEach((client) => {
1770
2187
  if (client.readyState === WebSocket.OPEN) {
1771
2188
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1772
2189
  }
1773
2190
  });
1774
2191
  }
2192
+ /**
2193
+ * Sends a message to all connected clients.
2194
+ *
2195
+ * @param {number} id - The message id.
2196
+ * @param {string} method - The message method.
2197
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2198
+ */
1775
2199
  wssBroadcastMessage(id, method, params) {
1776
2200
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2201
+ // Send the message to all connected clients
1777
2202
  this.webSocketServer?.clients.forEach((client) => {
1778
2203
  if (client.readyState === WebSocket.OPEN) {
1779
2204
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1781,3 +2206,4 @@ export class Frontend extends EventEmitter {
1781
2206
  });
1782
2207
  }
1783
2208
  }
2209
+ //# sourceMappingURL=frontend.js.map