matterbridge 3.2.4-dev-20250830-5c48452 → 3.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/dist/cli.d.ts +26 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +91 -2
  4. package/dist/cli.js.map +1 -0
  5. package/dist/cliEmitter.d.ts +34 -0
  6. package/dist/cliEmitter.d.ts.map +1 -0
  7. package/dist/cliEmitter.js +30 -0
  8. package/dist/cliEmitter.js.map +1 -0
  9. package/dist/clusters/export.d.ts +2 -0
  10. package/dist/clusters/export.d.ts.map +1 -0
  11. package/dist/clusters/export.js +2 -0
  12. package/dist/clusters/export.js.map +1 -0
  13. package/dist/defaultConfigSchema.d.ts +28 -0
  14. package/dist/defaultConfigSchema.d.ts.map +1 -0
  15. package/dist/defaultConfigSchema.js +24 -0
  16. package/dist/defaultConfigSchema.js.map +1 -0
  17. package/dist/deviceManager.d.ts +112 -0
  18. package/dist/deviceManager.d.ts.map +1 -0
  19. package/dist/deviceManager.js +94 -1
  20. package/dist/deviceManager.js.map +1 -0
  21. package/dist/devices/batteryStorage.d.ts +48 -0
  22. package/dist/devices/batteryStorage.d.ts.map +1 -0
  23. package/dist/devices/batteryStorage.js +48 -1
  24. package/dist/devices/batteryStorage.js.map +1 -0
  25. package/dist/devices/cooktop.d.ts +60 -0
  26. package/dist/devices/cooktop.d.ts.map +1 -0
  27. package/dist/devices/cooktop.js +55 -0
  28. package/dist/devices/cooktop.js.map +1 -0
  29. package/dist/devices/dishwasher.d.ts +71 -0
  30. package/dist/devices/dishwasher.d.ts.map +1 -0
  31. package/dist/devices/dishwasher.js +57 -0
  32. package/dist/devices/dishwasher.js.map +1 -0
  33. package/dist/devices/evse.d.ts +75 -0
  34. package/dist/devices/evse.d.ts.map +1 -0
  35. package/dist/devices/evse.js +74 -10
  36. package/dist/devices/evse.js.map +1 -0
  37. package/dist/devices/export.d.ts +15 -0
  38. package/dist/devices/export.d.ts.map +1 -0
  39. package/dist/devices/export.js +4 -0
  40. package/dist/devices/export.js.map +1 -0
  41. package/dist/devices/extractorHood.d.ts +46 -0
  42. package/dist/devices/extractorHood.d.ts.map +1 -0
  43. package/dist/devices/extractorHood.js +42 -0
  44. package/dist/devices/extractorHood.js.map +1 -0
  45. package/dist/devices/heatPump.d.ts +47 -0
  46. package/dist/devices/heatPump.d.ts.map +1 -0
  47. package/dist/devices/heatPump.js +50 -2
  48. package/dist/devices/heatPump.js.map +1 -0
  49. package/dist/devices/laundryDryer.d.ts +67 -0
  50. package/dist/devices/laundryDryer.d.ts.map +1 -0
  51. package/dist/devices/laundryDryer.js +62 -3
  52. package/dist/devices/laundryDryer.js.map +1 -0
  53. package/dist/devices/laundryWasher.d.ts +81 -0
  54. package/dist/devices/laundryWasher.d.ts.map +1 -0
  55. package/dist/devices/laundryWasher.js +70 -4
  56. package/dist/devices/laundryWasher.js.map +1 -0
  57. package/dist/devices/microwaveOven.d.ts +168 -0
  58. package/dist/devices/microwaveOven.d.ts.map +1 -0
  59. package/dist/devices/microwaveOven.js +88 -5
  60. package/dist/devices/microwaveOven.js.map +1 -0
  61. package/dist/devices/oven.d.ts +105 -0
  62. package/dist/devices/oven.d.ts.map +1 -0
  63. package/dist/devices/oven.js +85 -0
  64. package/dist/devices/oven.js.map +1 -0
  65. package/dist/devices/refrigerator.d.ts +93 -0
  66. package/dist/devices/refrigerator.d.ts.map +1 -0
  67. package/dist/devices/refrigerator.js +80 -0
  68. package/dist/devices/refrigerator.js.map +1 -0
  69. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  70. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  71. package/dist/devices/roboticVacuumCleaner.js +93 -7
  72. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  73. package/dist/devices/solarPower.d.ts +40 -0
  74. package/dist/devices/solarPower.d.ts.map +1 -0
  75. package/dist/devices/solarPower.js +38 -0
  76. package/dist/devices/solarPower.js.map +1 -0
  77. package/dist/devices/temperatureControl.d.ts +166 -0
  78. package/dist/devices/temperatureControl.d.ts.map +1 -0
  79. package/dist/devices/temperatureControl.js +25 -3
  80. package/dist/devices/temperatureControl.js.map +1 -0
  81. package/dist/devices/waterHeater.d.ts +111 -0
  82. package/dist/devices/waterHeater.d.ts.map +1 -0
  83. package/dist/devices/waterHeater.js +82 -2
  84. package/dist/devices/waterHeater.js.map +1 -0
  85. package/dist/dgram/coap.d.ts +205 -0
  86. package/dist/dgram/coap.d.ts.map +1 -0
  87. package/dist/dgram/coap.js +126 -13
  88. package/dist/dgram/coap.js.map +1 -0
  89. package/dist/dgram/dgram.d.ts +140 -0
  90. package/dist/dgram/dgram.d.ts.map +1 -0
  91. package/dist/dgram/dgram.js +113 -2
  92. package/dist/dgram/dgram.js.map +1 -0
  93. package/dist/dgram/mb_coap.d.ts +24 -0
  94. package/dist/dgram/mb_coap.d.ts.map +1 -0
  95. package/dist/dgram/mb_coap.js +41 -3
  96. package/dist/dgram/mb_coap.js.map +1 -0
  97. package/dist/dgram/mb_mdns.d.ts +24 -0
  98. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  99. package/dist/dgram/mb_mdns.js +51 -13
  100. package/dist/dgram/mb_mdns.js.map +1 -0
  101. package/dist/dgram/mdns.d.ts +288 -0
  102. package/dist/dgram/mdns.d.ts.map +1 -0
  103. package/dist/dgram/mdns.js +298 -137
  104. package/dist/dgram/mdns.js.map +1 -0
  105. package/dist/dgram/multicast.d.ts +65 -0
  106. package/dist/dgram/multicast.d.ts.map +1 -0
  107. package/dist/dgram/multicast.js +60 -1
  108. package/dist/dgram/multicast.js.map +1 -0
  109. package/dist/dgram/unicast.d.ts +56 -0
  110. package/dist/dgram/unicast.d.ts.map +1 -0
  111. package/dist/dgram/unicast.js +54 -0
  112. package/dist/dgram/unicast.js.map +1 -0
  113. package/dist/frontend.d.ts +313 -0
  114. package/dist/frontend.d.ts.map +1 -0
  115. package/dist/frontend.js +451 -24
  116. package/dist/frontend.js.map +1 -0
  117. package/dist/globalMatterbridge.d.ts +59 -0
  118. package/dist/globalMatterbridge.d.ts.map +1 -0
  119. package/dist/globalMatterbridge.js +47 -0
  120. package/dist/globalMatterbridge.js.map +1 -0
  121. package/dist/helpers.d.ts +48 -0
  122. package/dist/helpers.d.ts.map +1 -0
  123. package/dist/helpers.js +53 -0
  124. package/dist/helpers.js.map +1 -0
  125. package/dist/index.d.ts +33 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/index.js +30 -1
  128. package/dist/index.js.map +1 -0
  129. package/dist/logger/export.d.ts +2 -0
  130. package/dist/logger/export.d.ts.map +1 -0
  131. package/dist/logger/export.js +1 -0
  132. package/dist/logger/export.js.map +1 -0
  133. package/dist/matter/behaviors.d.ts +2 -0
  134. package/dist/matter/behaviors.d.ts.map +1 -0
  135. package/dist/matter/behaviors.js +2 -0
  136. package/dist/matter/behaviors.js.map +1 -0
  137. package/dist/matter/clusters.d.ts +2 -0
  138. package/dist/matter/clusters.d.ts.map +1 -0
  139. package/dist/matter/clusters.js +2 -0
  140. package/dist/matter/clusters.js.map +1 -0
  141. package/dist/matter/devices.d.ts +2 -0
  142. package/dist/matter/devices.d.ts.map +1 -0
  143. package/dist/matter/devices.js +2 -0
  144. package/dist/matter/devices.js.map +1 -0
  145. package/dist/matter/endpoints.d.ts +2 -0
  146. package/dist/matter/endpoints.d.ts.map +1 -0
  147. package/dist/matter/endpoints.js +2 -0
  148. package/dist/matter/endpoints.js.map +1 -0
  149. package/dist/matter/export.d.ts +5 -0
  150. package/dist/matter/export.d.ts.map +1 -0
  151. package/dist/matter/export.js +3 -0
  152. package/dist/matter/export.js.map +1 -0
  153. package/dist/matter/types.d.ts +3 -0
  154. package/dist/matter/types.d.ts.map +1 -0
  155. package/dist/matter/types.js +3 -0
  156. package/dist/matter/types.js.map +1 -0
  157. package/dist/matterbridge.d.ts +462 -0
  158. package/dist/matterbridge.d.ts.map +1 -0
  159. package/dist/matterbridge.js +789 -50
  160. package/dist/matterbridge.js.map +1 -0
  161. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  162. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  163. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  164. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  165. package/dist/matterbridgeBehaviors.d.ts +1351 -0
  166. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  167. package/dist/matterbridgeBehaviors.js +65 -5
  168. package/dist/matterbridgeBehaviors.js.map +1 -0
  169. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  170. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  171. package/dist/matterbridgeDeviceTypes.js +579 -15
  172. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  173. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  174. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  175. package/dist/matterbridgeDynamicPlatform.js +36 -0
  176. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  177. package/dist/matterbridgeEndpoint.d.ts +1356 -0
  178. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  179. package/dist/matterbridgeEndpoint.js +1220 -54
  180. package/dist/matterbridgeEndpoint.js.map +1 -0
  181. package/dist/matterbridgeEndpointHelpers.d.ts +407 -0
  182. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  183. package/dist/matterbridgeEndpointHelpers.js +345 -12
  184. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  185. package/dist/matterbridgePlatform.d.ts +331 -0
  186. package/dist/matterbridgePlatform.d.ts.map +1 -0
  187. package/dist/matterbridgePlatform.js +256 -0
  188. package/dist/matterbridgePlatform.js.map +1 -0
  189. package/dist/matterbridgeTypes.d.ts +198 -0
  190. package/dist/matterbridgeTypes.d.ts.map +1 -0
  191. package/dist/matterbridgeTypes.js +25 -0
  192. package/dist/matterbridgeTypes.js.map +1 -0
  193. package/dist/pluginManager.d.ts +270 -0
  194. package/dist/pluginManager.d.ts.map +1 -0
  195. package/dist/pluginManager.js +249 -3
  196. package/dist/pluginManager.js.map +1 -0
  197. package/dist/shelly.d.ts +174 -0
  198. package/dist/shelly.d.ts.map +1 -0
  199. package/dist/shelly.js +168 -7
  200. package/dist/shelly.js.map +1 -0
  201. package/dist/storage/export.d.ts +2 -0
  202. package/dist/storage/export.d.ts.map +1 -0
  203. package/dist/storage/export.js +1 -0
  204. package/dist/storage/export.js.map +1 -0
  205. package/dist/update.d.ts +75 -0
  206. package/dist/update.d.ts.map +1 -0
  207. package/dist/update.js +69 -0
  208. package/dist/update.js.map +1 -0
  209. package/dist/utils/colorUtils.d.ts +99 -0
  210. package/dist/utils/colorUtils.d.ts.map +1 -0
  211. package/dist/utils/colorUtils.js +97 -2
  212. package/dist/utils/colorUtils.js.map +1 -0
  213. package/dist/utils/commandLine.d.ts +59 -0
  214. package/dist/utils/commandLine.d.ts.map +1 -0
  215. package/dist/utils/commandLine.js +54 -0
  216. package/dist/utils/commandLine.js.map +1 -0
  217. package/dist/utils/copyDirectory.d.ts +33 -0
  218. package/dist/utils/copyDirectory.d.ts.map +1 -0
  219. package/dist/utils/copyDirectory.js +38 -1
  220. package/dist/utils/copyDirectory.js.map +1 -0
  221. package/dist/utils/createDirectory.d.ts +34 -0
  222. package/dist/utils/createDirectory.d.ts.map +1 -0
  223. package/dist/utils/createDirectory.js +33 -0
  224. package/dist/utils/createDirectory.js.map +1 -0
  225. package/dist/utils/createZip.d.ts +39 -0
  226. package/dist/utils/createZip.d.ts.map +1 -0
  227. package/dist/utils/createZip.js +47 -2
  228. package/dist/utils/createZip.js.map +1 -0
  229. package/dist/utils/deepCopy.d.ts +32 -0
  230. package/dist/utils/deepCopy.d.ts.map +1 -0
  231. package/dist/utils/deepCopy.js +39 -0
  232. package/dist/utils/deepCopy.js.map +1 -0
  233. package/dist/utils/deepEqual.d.ts +54 -0
  234. package/dist/utils/deepEqual.d.ts.map +1 -0
  235. package/dist/utils/deepEqual.js +72 -1
  236. package/dist/utils/deepEqual.js.map +1 -0
  237. package/dist/utils/error.d.ts +44 -0
  238. package/dist/utils/error.d.ts.map +1 -0
  239. package/dist/utils/error.js +41 -0
  240. package/dist/utils/error.js.map +1 -0
  241. package/dist/utils/export.d.ts +12 -0
  242. package/dist/utils/export.d.ts.map +1 -0
  243. package/dist/utils/export.js +1 -0
  244. package/dist/utils/export.js.map +1 -0
  245. package/dist/utils/hex.d.ts +89 -0
  246. package/dist/utils/hex.d.ts.map +1 -0
  247. package/dist/utils/hex.js +124 -0
  248. package/dist/utils/hex.js.map +1 -0
  249. package/dist/utils/isvalid.d.ts +103 -0
  250. package/dist/utils/isvalid.d.ts.map +1 -0
  251. package/dist/utils/isvalid.js +101 -0
  252. package/dist/utils/isvalid.js.map +1 -0
  253. package/dist/utils/network.d.ts +84 -0
  254. package/dist/utils/network.d.ts.map +1 -0
  255. package/dist/utils/network.js +91 -5
  256. package/dist/utils/network.js.map +1 -0
  257. package/dist/utils/spawn.d.ts +33 -0
  258. package/dist/utils/spawn.d.ts.map +1 -0
  259. package/dist/utils/spawn.js +40 -0
  260. package/dist/utils/spawn.js.map +1 -0
  261. package/dist/utils/wait.d.ts +54 -0
  262. package/dist/utils/wait.d.ts.map +1 -0
  263. package/dist/utils/wait.js +60 -8
  264. package/dist/utils/wait.js.map +1 -0
  265. package/npm-shrinkwrap.json +2 -2
  266. 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, LogFormat as MatterLogFormat, 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 {
@@ -676,6 +887,13 @@ export class Frontend extends EventEmitter {
676
887
  };
677
888
  }
678
889
  }
890
+ /**
891
+ * Retrieves the cluster text description from a given device.
892
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
893
+ *
894
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
895
+ * @returns {string} The attributes description of the cluster servers in the device.
896
+ */
679
897
  getClusterTextFromDevice(device) {
680
898
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
681
899
  return '';
@@ -686,6 +904,7 @@ export class Frontend extends EventEmitter {
686
904
  if (composed)
687
905
  return 'Composed: ' + composed.value;
688
906
  }
907
+ // istanbul ignore next cause is not reachable
689
908
  return '';
690
909
  };
691
910
  const getFixedLabel = (device) => {
@@ -695,11 +914,13 @@ export class Frontend extends EventEmitter {
695
914
  if (composed)
696
915
  return 'Composed: ' + composed.value;
697
916
  }
917
+ // istanbul ignore next cause is not reacheable
698
918
  return '';
699
919
  };
700
920
  let attributes = '';
701
921
  let supportedModes = [];
702
922
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
923
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
703
924
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
704
925
  return;
705
926
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -789,11 +1010,17 @@ export class Frontend extends EventEmitter {
789
1010
  if (clusterName === 'userLabel' && attributeName === 'labelList')
790
1011
  attributes += `${getUserLabel(device)} `;
791
1012
  });
1013
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
792
1014
  return attributes.trimStart().trimEnd();
793
1015
  }
1016
+ /**
1017
+ * Retrieves the registered plugins sanitized for res.json().
1018
+ *
1019
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1020
+ */
794
1021
  getPlugins() {
795
1022
  if (this.matterbridge.hasCleanupStarted)
796
- return [];
1023
+ return []; // Skip if cleanup has started
797
1024
  const baseRegisteredPlugins = [];
798
1025
  for (const plugin of this.matterbridge.plugins) {
799
1026
  baseRegisteredPlugins.push({
@@ -823,6 +1050,7 @@ export class Frontend extends EventEmitter {
823
1050
  schemaJson: plugin.schemaJson,
824
1051
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
825
1052
  hasBlackList: plugin.configJson?.blackList !== undefined,
1053
+ // Childbridge mode specific data
826
1054
  paired: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.commissioned : undefined,
827
1055
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.qrPairingCode : undefined,
828
1056
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.manualPairingCode : undefined,
@@ -832,13 +1060,21 @@ export class Frontend extends EventEmitter {
832
1060
  }
833
1061
  return baseRegisteredPlugins;
834
1062
  }
1063
+ /**
1064
+ * Retrieves the devices from Matterbridge.
1065
+ *
1066
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1067
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1068
+ */
835
1069
  async getDevices(pluginName) {
836
1070
  if (this.matterbridge.hasCleanupStarted)
837
- return [];
1071
+ return []; // Skip if cleanup has started
838
1072
  const devices = [];
839
1073
  for (const device of this.matterbridge.devices.array()) {
1074
+ // Filter by pluginName if provided
840
1075
  if (pluginName && pluginName !== device.plugin)
841
1076
  continue;
1077
+ // Check if the device has the required properties
842
1078
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
843
1079
  continue;
844
1080
  devices.push({
@@ -858,22 +1094,37 @@ export class Frontend extends EventEmitter {
858
1094
  }
859
1095
  return devices;
860
1096
  }
1097
+ /**
1098
+ * Retrieves the clusters from a given plugin and endpoint number.
1099
+ *
1100
+ * Response for /api/clusters
1101
+ *
1102
+ * @param {string} pluginName - The name of the plugin.
1103
+ * @param {number} endpointNumber - The endpoint number.
1104
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1105
+ */
861
1106
  getClusters(pluginName, endpointNumber) {
862
1107
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
863
1108
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
864
1109
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
865
1110
  return;
866
1111
  }
1112
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1113
+ // Get the device types from the main endpoint
867
1114
  const deviceTypes = [];
868
1115
  const clusters = [];
869
1116
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
870
1117
  deviceTypes.push(d.deviceType);
871
1118
  });
1119
+ // Get the clusters from the main endpoint
872
1120
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
873
1121
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
874
1122
  return;
875
1123
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
876
1124
  return;
1125
+ // console.log(
1126
+ // `${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}`,
1127
+ // );
877
1128
  clusters.push({
878
1129
  endpoint: endpoint.number.toString(),
879
1130
  id: 'main',
@@ -886,12 +1137,19 @@ export class Frontend extends EventEmitter {
886
1137
  attributeLocalValue: attributeValue,
887
1138
  });
888
1139
  });
1140
+ // Get the child endpoints
889
1141
  const childEndpoints = endpoint.getChildEndpoints();
1142
+ // if (childEndpoints.length === 0) {
1143
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1144
+ // }
890
1145
  childEndpoints.forEach((childEndpoint) => {
1146
+ // istanbul ignore if cause is not reachable: should never happen but ...
891
1147
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
892
1148
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
893
1149
  return;
894
1150
  }
1151
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1152
+ // Get the device types of the child endpoint
895
1153
  const deviceTypes = [];
896
1154
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
897
1155
  deviceTypes.push(d.deviceType);
@@ -901,9 +1159,12 @@ export class Frontend extends EventEmitter {
901
1159
  return;
902
1160
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
903
1161
  return;
1162
+ // console.log(
1163
+ // `${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}`,
1164
+ // );
904
1165
  clusters.push({
905
1166
  endpoint: childEndpoint.number.toString(),
906
- id: childEndpoint.maybeId ?? 'null',
1167
+ id: childEndpoint.maybeId ?? 'null', // Never happens
907
1168
  deviceTypes,
908
1169
  clusterName: capitalizeFirstLetter(clusterName),
909
1170
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -916,6 +1177,13 @@ export class Frontend extends EventEmitter {
916
1177
  });
917
1178
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
918
1179
  }
1180
+ /**
1181
+ * Handles incoming websocket messages for the Matterbridge frontend.
1182
+ *
1183
+ * @param {WebSocket} client - The websocket client that sent the message.
1184
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1185
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1186
+ */
919
1187
  async wsMessageHandler(client, message) {
920
1188
  let data;
921
1189
  try {
@@ -962,33 +1230,45 @@ export class Frontend extends EventEmitter {
962
1230
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
963
1231
  const packageName = data.params.packageName.replace(/@.*$/, '');
964
1232
  if (data.params.restart === false && packageName !== 'matterbridge') {
1233
+ // The install comes from InstallPlugins
965
1234
  this.matterbridge.plugins
966
1235
  .add(packageName)
967
1236
  .then((plugin) => {
1237
+ // istanbul ignore next if
968
1238
  if (plugin) {
1239
+ // The plugin is not registered
969
1240
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
970
1241
  this.matterbridge.plugins
971
1242
  .load(plugin, true, 'The plugin has been added', true)
1243
+ // eslint-disable-next-line promise/no-nesting
972
1244
  .then(() => {
973
1245
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
974
1246
  this.wssSendRefreshRequired('plugins');
975
1247
  return;
976
1248
  })
1249
+ // eslint-disable-next-line promise/no-nesting
977
1250
  .catch((_error) => {
1251
+ //
978
1252
  });
979
1253
  }
980
1254
  else {
1255
+ // The plugin is already registered
981
1256
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
982
1257
  this.wssSendRefreshRequired('plugins');
983
1258
  this.wssSendRestartRequired(true, true);
984
1259
  }
985
1260
  return;
986
1261
  })
1262
+ // eslint-disable-next-line promise/no-nesting
987
1263
  .catch((_error) => {
1264
+ //
988
1265
  });
989
1266
  }
990
1267
  else {
1268
+ // The package is matterbridge
1269
+ // istanbul ignore next
991
1270
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
1271
+ // istanbul ignore next if
992
1272
  if (this.matterbridge.restartMode !== '') {
993
1273
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
994
1274
  this.matterbridge.shutdownProcess();
@@ -1010,7 +1290,9 @@ export class Frontend extends EventEmitter {
1010
1290
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1011
1291
  return;
1012
1292
  }
1293
+ // The package is a plugin
1013
1294
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
1295
+ // istanbul ignore next if
1014
1296
  if (plugin) {
1015
1297
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
1016
1298
  await this.matterbridge.plugins.remove(data.params.packageName);
@@ -1018,6 +1300,7 @@ export class Frontend extends EventEmitter {
1018
1300
  this.wssSendRefreshRequired('plugins');
1019
1301
  this.wssSendRefreshRequired('devices');
1020
1302
  }
1303
+ // Uninstall the package
1021
1304
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1022
1305
  const { spawnCommand } = await import('./utils/spawn.js');
1023
1306
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1058,6 +1341,7 @@ export class Frontend extends EventEmitter {
1058
1341
  return;
1059
1342
  })
1060
1343
  .catch((_error) => {
1344
+ //
1061
1345
  });
1062
1346
  }
1063
1347
  else {
@@ -1104,6 +1388,7 @@ export class Frontend extends EventEmitter {
1104
1388
  return;
1105
1389
  })
1106
1390
  .catch((_error) => {
1391
+ //
1107
1392
  });
1108
1393
  }
1109
1394
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1129,6 +1414,7 @@ export class Frontend extends EventEmitter {
1129
1414
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1130
1415
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1131
1416
  if (plugin.serverNode) {
1417
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1132
1418
  await this.matterbridge.stopServerNode(plugin.serverNode);
1133
1419
  plugin.serverNode = undefined;
1134
1420
  }
@@ -1139,15 +1425,16 @@ export class Frontend extends EventEmitter {
1139
1425
  }
1140
1426
  }
1141
1427
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1142
- plugin.restartRequired = false;
1428
+ plugin.restartRequired = false; // Reset plugin restartRequired
1143
1429
  let needRestart = 0;
1144
1430
  for (const plugin of this.matterbridge.plugins) {
1145
1431
  if (plugin.restartRequired)
1146
1432
  needRestart++;
1147
1433
  }
1148
1434
  if (needRestart === 0) {
1149
- this.wssSendRestartNotRequired(true);
1435
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1150
1436
  }
1437
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1151
1438
  if (plugin.serverNode)
1152
1439
  await this.matterbridge.startServerNode(plugin.serverNode);
1153
1440
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1253,6 +1540,8 @@ export class Frontend extends EventEmitter {
1253
1540
  else if (data.method === '/api/advertise') {
1254
1541
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1255
1542
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1543
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1544
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1256
1545
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1257
1546
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1258
1547
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1375,22 +1664,22 @@ export class Frontend extends EventEmitter {
1375
1664
  if (isValidString(data.params.value, 4)) {
1376
1665
  this.log.debug('Matterbridge logger level:', data.params.value);
1377
1666
  if (data.params.value === 'Debug') {
1378
- await this.matterbridge.setLogLevel("debug");
1667
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1379
1668
  }
1380
1669
  else if (data.params.value === 'Info') {
1381
- await this.matterbridge.setLogLevel("info");
1670
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1382
1671
  }
1383
1672
  else if (data.params.value === 'Notice') {
1384
- await this.matterbridge.setLogLevel("notice");
1673
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1385
1674
  }
1386
1675
  else if (data.params.value === 'Warn') {
1387
- await this.matterbridge.setLogLevel("warn");
1676
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1388
1677
  }
1389
1678
  else if (data.params.value === 'Error') {
1390
- await this.matterbridge.setLogLevel("error");
1679
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1391
1680
  }
1392
1681
  else if (data.params.value === 'Fatal') {
1393
- await this.matterbridge.setLogLevel("fatal");
1682
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1394
1683
  }
1395
1684
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1396
1685
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1401,6 +1690,7 @@ export class Frontend extends EventEmitter {
1401
1690
  this.log.debug('Matterbridge file log:', data.params.value);
1402
1691
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1403
1692
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1693
+ // Create the file logger for matterbridge
1404
1694
  if (data.params.value)
1405
1695
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1406
1696
  else
@@ -1447,6 +1737,7 @@ export class Frontend extends EventEmitter {
1447
1737
  });
1448
1738
  }
1449
1739
  catch (error) {
1740
+ /* istanbul ignore next */
1450
1741
  this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1451
1742
  }
1452
1743
  }
@@ -1455,6 +1746,7 @@ export class Frontend extends EventEmitter {
1455
1746
  Logger.removeLogger('matterfilelogger');
1456
1747
  }
1457
1748
  catch (error) {
1749
+ /* istanbul ignore next */
1458
1750
  this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1459
1751
  }
1460
1752
  }
@@ -1567,15 +1859,19 @@ export class Frontend extends EventEmitter {
1567
1859
  return;
1568
1860
  }
1569
1861
  const config = plugin.configJson;
1862
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1570
1863
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1864
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1571
1865
  if (select === 'serial')
1572
1866
  this.log.info(`Selected device serial ${data.params.serial}`);
1573
1867
  if (select === 'name')
1574
1868
  this.log.info(`Selected device name ${data.params.name}`);
1575
1869
  if (config && select && (select === 'serial' || select === 'name')) {
1870
+ // Remove postfix from the serial if it exists
1576
1871
  if (config.postfix) {
1577
1872
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1578
1873
  }
1874
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1579
1875
  if (isValidArray(config.whiteList, 1)) {
1580
1876
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1581
1877
  config.whiteList.push(data.params.serial);
@@ -1584,6 +1880,7 @@ export class Frontend extends EventEmitter {
1584
1880
  config.whiteList.push(data.params.name);
1585
1881
  }
1586
1882
  }
1883
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1587
1884
  if (isValidArray(config.blackList, 1)) {
1588
1885
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1589
1886
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1614,7 +1911,9 @@ export class Frontend extends EventEmitter {
1614
1911
  return;
1615
1912
  }
1616
1913
  const config = plugin.configJson;
1914
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1617
1915
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1916
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1618
1917
  if (select === 'serial')
1619
1918
  this.log.info(`Unselected device serial ${data.params.serial}`);
1620
1919
  if (select === 'name')
@@ -1623,6 +1922,7 @@ export class Frontend extends EventEmitter {
1623
1922
  if (config.postfix) {
1624
1923
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1625
1924
  }
1925
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1626
1926
  if (isValidArray(config.whiteList, 1)) {
1627
1927
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1628
1928
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1631,6 +1931,7 @@ export class Frontend extends EventEmitter {
1631
1931
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1632
1932
  }
1633
1933
  }
1934
+ // Add the serial to the blackList
1634
1935
  if (isValidArray(config.blackList)) {
1635
1936
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1636
1937
  config.blackList.push(data.params.serial);
@@ -1664,126 +1965,251 @@ export class Frontend extends EventEmitter {
1664
1965
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1665
1966
  }
1666
1967
  }
1968
+ /**
1969
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1970
+ *
1971
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1972
+ * @param {string} time - The time string of the message
1973
+ * @param {string} name - The logger name of the message
1974
+ * @param {string} message - The content of the message.
1975
+ *
1976
+ * @remarks
1977
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1978
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1979
+ * The function sends the message to all connected clients.
1980
+ */
1667
1981
  wssSendMessage(level, time, name, message) {
1668
1982
  if (!level || !time || !name || !message)
1669
1983
  return;
1984
+ // Remove ANSI escape codes from the message
1985
+ // eslint-disable-next-line no-control-regex
1670
1986
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1987
+ // Remove leading asterisks from the message
1671
1988
  message = message.replace(/^\*+/, '');
1989
+ // Replace all occurrences of \t and \n
1672
1990
  message = message.replace(/[\t\n]/g, '');
1991
+ // Remove non-printable characters
1992
+ // eslint-disable-next-line no-control-regex
1673
1993
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1994
+ // Replace all occurrences of \" with "
1674
1995
  message = message.replace(/\\"/g, '"');
1996
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1675
1997
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1998
+ // Define the maximum allowed length for continuous characters without a space
1676
1999
  const maxContinuousLength = 100;
1677
2000
  const keepStartLength = 20;
1678
2001
  const keepEndLength = 20;
2002
+ // Split the message into words
1679
2003
  message = message
1680
2004
  .split(' ')
1681
2005
  .map((word) => {
2006
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1682
2007
  if (word.length > maxContinuousLength) {
1683
2008
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1684
2009
  }
1685
2010
  return word;
1686
2011
  })
1687
2012
  .join(' ');
2013
+ // Send the message to all connected clients
1688
2014
  this.webSocketServer?.clients.forEach((client) => {
1689
2015
  if (client.readyState === WebSocket.OPEN) {
1690
2016
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1691
2017
  }
1692
2018
  });
1693
2019
  }
2020
+ /**
2021
+ * Sends a need to refresh WebSocket message to all connected clients.
2022
+ *
2023
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
2024
+ * possible values:
2025
+ * - 'matterbridgeLatestVersion'
2026
+ * - 'matterbridgeDevVersion'
2027
+ * - 'matterbridgeAdvertise'
2028
+ * - 'online'
2029
+ * - 'offline'
2030
+ * - 'reachability'
2031
+ * - 'settings'
2032
+ * - 'plugins'
2033
+ * - 'pluginsRestart'
2034
+ * - 'devices'
2035
+ * - 'fabrics'
2036
+ * - 'sessions'
2037
+ */
1694
2038
  wssSendRefreshRequired(changed = null) {
1695
2039
  this.log.debug('Sending a refresh required message to all connected clients');
2040
+ // Send the message to all connected clients
1696
2041
  this.webSocketServer?.clients.forEach((client) => {
1697
2042
  if (client.readyState === WebSocket.OPEN) {
1698
2043
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1699
2044
  }
1700
2045
  });
1701
2046
  }
2047
+ /**
2048
+ * Sends a need to restart WebSocket message to all connected clients.
2049
+ *
2050
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2051
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2052
+ */
1702
2053
  wssSendRestartRequired(snackbar = true, fixed = false) {
1703
2054
  this.log.debug('Sending a restart required message to all connected clients');
1704
2055
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1705
2056
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = fixed;
1706
2057
  if (snackbar === true)
1707
2058
  this.wssSendSnackbarMessage(`Restart required`, 0);
2059
+ // Send the message to all connected clients
1708
2060
  this.webSocketServer?.clients.forEach((client) => {
1709
2061
  if (client.readyState === WebSocket.OPEN) {
1710
2062
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: { fixed } }));
1711
2063
  }
1712
2064
  });
1713
2065
  }
2066
+ /**
2067
+ * Sends a no need to restart WebSocket message to all connected clients.
2068
+ *
2069
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients.
2070
+ */
1714
2071
  wssSendRestartNotRequired(snackbar = true) {
1715
2072
  this.log.debug('Sending a restart not required message to all connected clients');
1716
2073
  this.matterbridge.matterbridgeInformation.restartRequired = false;
1717
2074
  if (snackbar === true)
1718
2075
  this.wssSendCloseSnackbarMessage(`Restart required`);
2076
+ // Send the message to all connected clients
1719
2077
  this.webSocketServer?.clients.forEach((client) => {
1720
2078
  if (client.readyState === WebSocket.OPEN) {
1721
2079
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', params: {} }));
1722
2080
  }
1723
2081
  });
1724
2082
  }
2083
+ /**
2084
+ * Sends a need to update WebSocket message to all connected clients.
2085
+ *
2086
+ * @param {boolean} devVersion - If true, the update is for a development version.
2087
+ */
1725
2088
  wssSendUpdateRequired(devVersion = false) {
1726
2089
  this.log.debug('Sending an update required message to all connected clients');
1727
2090
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2091
+ // Send the message to all connected clients
1728
2092
  this.webSocketServer?.clients.forEach((client) => {
1729
2093
  if (client.readyState === WebSocket.OPEN) {
1730
2094
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: devVersion ? 'update_required_dev' : 'update_required', params: {} }));
1731
2095
  }
1732
2096
  });
1733
2097
  }
2098
+ /**
2099
+ * Sends a cpu update message to all connected clients.
2100
+ *
2101
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2102
+ */
1734
2103
  wssSendCpuUpdate(cpuUsage) {
1735
2104
  if (hasParameter('debug'))
1736
2105
  this.log.debug('Sending a cpu update message to all connected clients');
2106
+ // Send the message to all connected clients
1737
2107
  this.webSocketServer?.clients.forEach((client) => {
1738
2108
  if (client.readyState === WebSocket.OPEN) {
1739
2109
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1740
2110
  }
1741
2111
  });
1742
2112
  }
2113
+ /**
2114
+ * Sends a memory update message to all connected clients.
2115
+ *
2116
+ * @param {string} totalMemory - The total memory in bytes.
2117
+ * @param {string} freeMemory - The free memory in bytes.
2118
+ * @param {string} rss - The resident set size in bytes.
2119
+ * @param {string} heapTotal - The total heap memory in bytes.
2120
+ * @param {string} heapUsed - The used heap memory in bytes.
2121
+ * @param {string} external - The external memory in bytes.
2122
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2123
+ */
1743
2124
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1744
2125
  if (hasParameter('debug'))
1745
2126
  this.log.debug('Sending a memory update message to all connected clients');
2127
+ // Send the message to all connected clients
1746
2128
  this.webSocketServer?.clients.forEach((client) => {
1747
2129
  if (client.readyState === WebSocket.OPEN) {
1748
2130
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1749
2131
  }
1750
2132
  });
1751
2133
  }
2134
+ /**
2135
+ * Sends an uptime update message to all connected clients.
2136
+ *
2137
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2138
+ * @param {string} processUptime - The process uptime in a human-readable format.
2139
+ */
1752
2140
  wssSendUptimeUpdate(systemUptime, processUptime) {
1753
2141
  if (hasParameter('debug'))
1754
2142
  this.log.debug('Sending a uptime update message to all connected clients');
2143
+ // Send the message to all connected clients
1755
2144
  this.webSocketServer?.clients.forEach((client) => {
1756
2145
  if (client.readyState === WebSocket.OPEN) {
1757
2146
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1758
2147
  }
1759
2148
  });
1760
2149
  }
2150
+ /**
2151
+ * Sends an open snackbar message to all connected clients.
2152
+ *
2153
+ * @param {string} message - The message to send.
2154
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2155
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2156
+ */
1761
2157
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1762
2158
  this.log.debug('Sending a snackbar message to all connected clients');
2159
+ // Send the message to all connected clients
1763
2160
  this.webSocketServer?.clients.forEach((client) => {
1764
2161
  if (client.readyState === WebSocket.OPEN) {
1765
2162
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1766
2163
  }
1767
2164
  });
1768
2165
  }
2166
+ /**
2167
+ * Sends a close snackbar message to all connected clients.
2168
+ *
2169
+ * @param {string} message - The message to send.
2170
+ */
1769
2171
  wssSendCloseSnackbarMessage(message) {
1770
2172
  this.log.debug('Sending a close snackbar message to all connected clients');
2173
+ // Send the message to all connected clients
1771
2174
  this.webSocketServer?.clients.forEach((client) => {
1772
2175
  if (client.readyState === WebSocket.OPEN) {
1773
2176
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1774
2177
  }
1775
2178
  });
1776
2179
  }
2180
+ /**
2181
+ * Sends an attribute update message to all connected WebSocket clients.
2182
+ *
2183
+ * @param {string | undefined} plugin - The name of the plugin.
2184
+ * @param {string | undefined} serialNumber - The serial number of the device.
2185
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2186
+ * @param {string} cluster - The cluster name where the attribute belongs.
2187
+ * @param {string} attribute - The name of the attribute that changed.
2188
+ * @param {number | string | boolean} value - The new value of the attribute.
2189
+ *
2190
+ * @remarks
2191
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2192
+ * with the updated attribute information.
2193
+ */
1777
2194
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1778
2195
  this.log.debug('Sending an attribute update message to all connected clients');
2196
+ // Send the message to all connected clients
1779
2197
  this.webSocketServer?.clients.forEach((client) => {
1780
2198
  if (client.readyState === WebSocket.OPEN) {
1781
2199
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1782
2200
  }
1783
2201
  });
1784
2202
  }
2203
+ /**
2204
+ * Sends a message to all connected clients.
2205
+ *
2206
+ * @param {number} id - The message id.
2207
+ * @param {string} method - The message method.
2208
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2209
+ */
1785
2210
  wssBroadcastMessage(id, method, params) {
1786
2211
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2212
+ // Send the message to all connected clients
1787
2213
  this.webSocketServer?.clients.forEach((client) => {
1788
2214
  if (client.readyState === WebSocket.OPEN) {
1789
2215
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1791,3 +2217,4 @@ export class Frontend extends EventEmitter {
1791
2217
  });
1792
2218
  }
1793
2219
  }
2220
+ //# sourceMappingURL=frontend.js.map