matterbridge 3.1.8-dev-20250727-662308b → 3.1.8

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 (248) hide show
  1. package/CHANGELOG.md +1 -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/batteryStorage.d.ts +48 -0
  23. package/dist/devices/batteryStorage.d.ts.map +1 -0
  24. package/dist/devices/batteryStorage.js +48 -1
  25. package/dist/devices/batteryStorage.js.map +1 -0
  26. package/dist/devices/dishwasher.d.ts +91 -0
  27. package/dist/devices/dishwasher.d.ts.map +1 -0
  28. package/dist/devices/dishwasher.js +78 -3
  29. package/dist/devices/dishwasher.js.map +1 -0
  30. package/dist/devices/evse.d.ts +75 -0
  31. package/dist/devices/evse.d.ts.map +1 -0
  32. package/dist/devices/evse.js +74 -10
  33. package/dist/devices/evse.js.map +1 -0
  34. package/dist/devices/export.d.ts +11 -0
  35. package/dist/devices/export.d.ts.map +1 -0
  36. package/dist/devices/export.js +2 -0
  37. package/dist/devices/export.js.map +1 -0
  38. package/dist/devices/extractorHood.d.ts +46 -0
  39. package/dist/devices/extractorHood.d.ts.map +1 -0
  40. package/dist/devices/extractorHood.js +42 -0
  41. package/dist/devices/extractorHood.js.map +1 -0
  42. package/dist/devices/heatPump.d.ts +47 -0
  43. package/dist/devices/heatPump.d.ts.map +1 -0
  44. package/dist/devices/heatPump.js +50 -2
  45. package/dist/devices/heatPump.js.map +1 -0
  46. package/dist/devices/laundryDryer.d.ts +87 -0
  47. package/dist/devices/laundryDryer.d.ts.map +1 -0
  48. package/dist/devices/laundryDryer.js +83 -6
  49. package/dist/devices/laundryDryer.js.map +1 -0
  50. package/dist/devices/laundryWasher.d.ts +242 -0
  51. package/dist/devices/laundryWasher.d.ts.map +1 -0
  52. package/dist/devices/laundryWasher.js +91 -7
  53. package/dist/devices/laundryWasher.js.map +1 -0
  54. package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
  55. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  56. package/dist/devices/roboticVacuumCleaner.js +93 -7
  57. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  58. package/dist/devices/solarPower.d.ts +40 -0
  59. package/dist/devices/solarPower.d.ts.map +1 -0
  60. package/dist/devices/solarPower.js +38 -0
  61. package/dist/devices/solarPower.js.map +1 -0
  62. package/dist/devices/waterHeater.d.ts +111 -0
  63. package/dist/devices/waterHeater.d.ts.map +1 -0
  64. package/dist/devices/waterHeater.js +82 -2
  65. package/dist/devices/waterHeater.js.map +1 -0
  66. package/dist/dgram/coap.d.ts +205 -0
  67. package/dist/dgram/coap.d.ts.map +1 -0
  68. package/dist/dgram/coap.js +126 -13
  69. package/dist/dgram/coap.js.map +1 -0
  70. package/dist/dgram/dgram.d.ts +140 -0
  71. package/dist/dgram/dgram.d.ts.map +1 -0
  72. package/dist/dgram/dgram.js +113 -2
  73. package/dist/dgram/dgram.js.map +1 -0
  74. package/dist/dgram/mb_coap.d.ts +24 -0
  75. package/dist/dgram/mb_coap.d.ts.map +1 -0
  76. package/dist/dgram/mb_coap.js +41 -3
  77. package/dist/dgram/mb_coap.js.map +1 -0
  78. package/dist/dgram/mb_mdns.d.ts +24 -0
  79. package/dist/dgram/mb_mdns.d.ts.map +1 -0
  80. package/dist/dgram/mb_mdns.js +51 -13
  81. package/dist/dgram/mb_mdns.js.map +1 -0
  82. package/dist/dgram/mdns.d.ts +288 -0
  83. package/dist/dgram/mdns.d.ts.map +1 -0
  84. package/dist/dgram/mdns.js +298 -137
  85. package/dist/dgram/mdns.js.map +1 -0
  86. package/dist/dgram/multicast.d.ts +65 -0
  87. package/dist/dgram/multicast.d.ts.map +1 -0
  88. package/dist/dgram/multicast.js +60 -1
  89. package/dist/dgram/multicast.js.map +1 -0
  90. package/dist/dgram/unicast.d.ts +56 -0
  91. package/dist/dgram/unicast.d.ts.map +1 -0
  92. package/dist/dgram/unicast.js +54 -0
  93. package/dist/dgram/unicast.js.map +1 -0
  94. package/dist/frontend.d.ts +313 -0
  95. package/dist/frontend.d.ts.map +1 -0
  96. package/dist/frontend.js +449 -23
  97. package/dist/frontend.js.map +1 -0
  98. package/dist/globalMatterbridge.d.ts +59 -0
  99. package/dist/globalMatterbridge.d.ts.map +1 -0
  100. package/dist/globalMatterbridge.js +47 -0
  101. package/dist/globalMatterbridge.js.map +1 -0
  102. package/dist/helpers.d.ts +48 -0
  103. package/dist/helpers.d.ts.map +1 -0
  104. package/dist/helpers.js +53 -0
  105. package/dist/helpers.js.map +1 -0
  106. package/dist/index.d.ts +33 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +30 -1
  109. package/dist/index.js.map +1 -0
  110. package/dist/logger/export.d.ts +2 -0
  111. package/dist/logger/export.d.ts.map +1 -0
  112. package/dist/logger/export.js +1 -0
  113. package/dist/logger/export.js.map +1 -0
  114. package/dist/matter/behaviors.d.ts +2 -0
  115. package/dist/matter/behaviors.d.ts.map +1 -0
  116. package/dist/matter/behaviors.js +2 -0
  117. package/dist/matter/behaviors.js.map +1 -0
  118. package/dist/matter/clusters.d.ts +2 -0
  119. package/dist/matter/clusters.d.ts.map +1 -0
  120. package/dist/matter/clusters.js +2 -0
  121. package/dist/matter/clusters.js.map +1 -0
  122. package/dist/matter/devices.d.ts +2 -0
  123. package/dist/matter/devices.d.ts.map +1 -0
  124. package/dist/matter/devices.js +2 -0
  125. package/dist/matter/devices.js.map +1 -0
  126. package/dist/matter/endpoints.d.ts +2 -0
  127. package/dist/matter/endpoints.d.ts.map +1 -0
  128. package/dist/matter/endpoints.js +2 -0
  129. package/dist/matter/endpoints.js.map +1 -0
  130. package/dist/matter/export.d.ts +5 -0
  131. package/dist/matter/export.d.ts.map +1 -0
  132. package/dist/matter/export.js +3 -0
  133. package/dist/matter/export.js.map +1 -0
  134. package/dist/matter/types.d.ts +3 -0
  135. package/dist/matter/types.d.ts.map +1 -0
  136. package/dist/matter/types.js +3 -0
  137. package/dist/matter/types.js.map +1 -0
  138. package/dist/matterbridge.d.ts +463 -0
  139. package/dist/matterbridge.d.ts.map +1 -0
  140. package/dist/matterbridge.js +803 -50
  141. package/dist/matterbridge.js.map +1 -0
  142. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  143. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  144. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  145. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  146. package/dist/matterbridgeBehaviors.d.ts +1351 -0
  147. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  148. package/dist/matterbridgeBehaviors.js +65 -5
  149. package/dist/matterbridgeBehaviors.js.map +1 -0
  150. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  151. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  152. package/dist/matterbridgeDeviceTypes.js +579 -15
  153. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  154. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  155. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  156. package/dist/matterbridgeDynamicPlatform.js +36 -0
  157. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  158. package/dist/matterbridgeEndpoint.d.ts +1348 -0
  159. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  160. package/dist/matterbridgeEndpoint.js +1214 -54
  161. package/dist/matterbridgeEndpoint.js.map +1 -0
  162. package/dist/matterbridgeEndpointHelpers.d.ts +406 -0
  163. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  164. package/dist/matterbridgeEndpointHelpers.js +344 -12
  165. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  166. package/dist/matterbridgePlatform.d.ts +310 -0
  167. package/dist/matterbridgePlatform.d.ts.map +1 -0
  168. package/dist/matterbridgePlatform.js +233 -0
  169. package/dist/matterbridgePlatform.js.map +1 -0
  170. package/dist/matterbridgeTypes.d.ts +197 -0
  171. package/dist/matterbridgeTypes.d.ts.map +1 -0
  172. package/dist/matterbridgeTypes.js +25 -0
  173. package/dist/matterbridgeTypes.js.map +1 -0
  174. package/dist/pluginManager.d.ts +270 -0
  175. package/dist/pluginManager.d.ts.map +1 -0
  176. package/dist/pluginManager.js +249 -3
  177. package/dist/pluginManager.js.map +1 -0
  178. package/dist/shelly.d.ts +174 -0
  179. package/dist/shelly.d.ts.map +1 -0
  180. package/dist/shelly.js +168 -7
  181. package/dist/shelly.js.map +1 -0
  182. package/dist/storage/export.d.ts +2 -0
  183. package/dist/storage/export.d.ts.map +1 -0
  184. package/dist/storage/export.js +1 -0
  185. package/dist/storage/export.js.map +1 -0
  186. package/dist/update.d.ts +68 -0
  187. package/dist/update.d.ts.map +1 -0
  188. package/dist/update.js +63 -0
  189. package/dist/update.js.map +1 -0
  190. package/dist/utils/colorUtils.d.ts +117 -0
  191. package/dist/utils/colorUtils.d.ts.map +1 -0
  192. package/dist/utils/colorUtils.js +263 -2
  193. package/dist/utils/colorUtils.js.map +1 -0
  194. package/dist/utils/commandLine.d.ts +59 -0
  195. package/dist/utils/commandLine.d.ts.map +1 -0
  196. package/dist/utils/commandLine.js +54 -0
  197. package/dist/utils/commandLine.js.map +1 -0
  198. package/dist/utils/copyDirectory.d.ts +33 -0
  199. package/dist/utils/copyDirectory.d.ts.map +1 -0
  200. package/dist/utils/copyDirectory.js +38 -1
  201. package/dist/utils/copyDirectory.js.map +1 -0
  202. package/dist/utils/createDirectory.d.ts +34 -0
  203. package/dist/utils/createDirectory.d.ts.map +1 -0
  204. package/dist/utils/createDirectory.js +33 -0
  205. package/dist/utils/createDirectory.js.map +1 -0
  206. package/dist/utils/createZip.d.ts +39 -0
  207. package/dist/utils/createZip.d.ts.map +1 -0
  208. package/dist/utils/createZip.js +47 -2
  209. package/dist/utils/createZip.js.map +1 -0
  210. package/dist/utils/deepCopy.d.ts +32 -0
  211. package/dist/utils/deepCopy.d.ts.map +1 -0
  212. package/dist/utils/deepCopy.js +39 -0
  213. package/dist/utils/deepCopy.js.map +1 -0
  214. package/dist/utils/deepEqual.d.ts +54 -0
  215. package/dist/utils/deepEqual.d.ts.map +1 -0
  216. package/dist/utils/deepEqual.js +72 -1
  217. package/dist/utils/deepEqual.js.map +1 -0
  218. package/dist/utils/error.d.ts +44 -0
  219. package/dist/utils/error.d.ts.map +1 -0
  220. package/dist/utils/error.js +41 -0
  221. package/dist/utils/error.js.map +1 -0
  222. package/dist/utils/export.d.ts +12 -0
  223. package/dist/utils/export.d.ts.map +1 -0
  224. package/dist/utils/export.js +1 -0
  225. package/dist/utils/export.js.map +1 -0
  226. package/dist/utils/hex.d.ts +89 -0
  227. package/dist/utils/hex.d.ts.map +1 -0
  228. package/dist/utils/hex.js +123 -0
  229. package/dist/utils/hex.js.map +1 -0
  230. package/dist/utils/isvalid.d.ts +103 -0
  231. package/dist/utils/isvalid.d.ts.map +1 -0
  232. package/dist/utils/isvalid.js +101 -0
  233. package/dist/utils/isvalid.js.map +1 -0
  234. package/dist/utils/network.d.ts +74 -0
  235. package/dist/utils/network.d.ts.map +1 -0
  236. package/dist/utils/network.js +81 -5
  237. package/dist/utils/network.js.map +1 -0
  238. package/dist/utils/spawn.d.ts +33 -0
  239. package/dist/utils/spawn.d.ts.map +1 -0
  240. package/dist/utils/spawn.js +40 -0
  241. package/dist/utils/spawn.js.map +1 -0
  242. package/dist/utils/wait.d.ts +56 -0
  243. package/dist/utils/wait.d.ts.map +1 -0
  244. package/dist/utils/wait.js +62 -9
  245. package/dist/utils/wait.js.map +1 -0
  246. package/npm-shrinkwrap.json +2 -2
  247. package/package.json +2 -1
  248. package/vitest/matterbridge.test.ts +0 -218
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,12 +141,41 @@ 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}`);
144
+ // Initialize multer with the upload directory
48
145
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
49
146
  await fs.mkdir(uploadDir, { recursive: true });
50
147
  const upload = multer({ dest: uploadDir });
148
+ // Create the express app that serves the frontend
51
149
  this.expressApp = express();
150
+ // Inject logging/debug wrapper for route/middleware registration
151
+ /*
152
+ const methods = ['get', 'post', 'put', 'delete', 'use'];
153
+ for (const method of methods) {
154
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
155
+ const original = (this.expressApp as any)[method].bind(this.expressApp);
156
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
+ (this.expressApp as any)[method] = (path: any, ...rest: any) => {
158
+ try {
159
+ console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
160
+ return original(path, ...rest);
161
+ } catch (err) {
162
+ console.error(`[ERROR] Failed to register route: ${path}`);
163
+ throw err;
164
+ }
165
+ };
166
+ }
167
+ */
168
+ // Log all requests to the server for debugging
169
+ /*
170
+ this.expressApp.use((req, res, next) => {
171
+ this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
172
+ next();
173
+ });
174
+ */
175
+ // Serve static files from '/static' endpoint
52
176
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
53
177
  if (!hasParameter('ssl')) {
178
+ // Create an HTTP server and attach the express app
54
179
  try {
55
180
  this.log.debug(`Creating HTTP server...`);
56
181
  this.httpServer = createServer(this.expressApp);
@@ -60,6 +185,7 @@ export class Frontend extends EventEmitter {
60
185
  this.emit('server_error', error);
61
186
  return;
62
187
  }
188
+ // Listen on the specified port
63
189
  if (hasParameter('ingress')) {
64
190
  this.httpServer.listen(this.port, '0.0.0.0', () => {
65
191
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -98,6 +224,7 @@ export class Frontend extends EventEmitter {
98
224
  let passphrase;
99
225
  let httpsServerOptions = {};
100
226
  if (existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
227
+ // Load the p12 certificate and the passphrase
101
228
  try {
102
229
  pfx = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
103
230
  this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
@@ -109,7 +236,7 @@ export class Frontend extends EventEmitter {
109
236
  }
110
237
  try {
111
238
  passphrase = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
112
- passphrase = passphrase.trim();
239
+ passphrase = passphrase.trim(); // Ensure no extra characters
113
240
  this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
114
241
  }
115
242
  catch (error) {
@@ -120,6 +247,7 @@ export class Frontend extends EventEmitter {
120
247
  httpsServerOptions = { pfx, passphrase };
121
248
  }
122
249
  else {
250
+ // 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.
123
251
  try {
124
252
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
125
253
  this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
@@ -149,9 +277,10 @@ export class Frontend extends EventEmitter {
149
277
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
150
278
  }
151
279
  if (hasParameter('mtls')) {
152
- httpsServerOptions.requestCert = true;
153
- httpsServerOptions.rejectUnauthorized = true;
280
+ httpsServerOptions.requestCert = true; // Request client certificate
281
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
154
282
  }
283
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
155
284
  try {
156
285
  this.log.debug(`Creating HTTPS server...`);
157
286
  this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
@@ -161,6 +290,7 @@ export class Frontend extends EventEmitter {
161
290
  this.emit('server_error', error);
162
291
  return;
163
292
  }
293
+ // Listen on the specified port
164
294
  if (hasParameter('ingress')) {
165
295
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
166
296
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -190,17 +320,19 @@ export class Frontend extends EventEmitter {
190
320
  return;
191
321
  });
192
322
  }
323
+ // Create a WebSocket server and attach it to the http or https server
193
324
  const wssPort = this.port;
194
325
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
195
326
  this.log.debug(`Creating WebSocketServer on host ${CYAN}${wssHost}${db}...`);
196
327
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
197
328
  this.webSocketServer.on('connection', (ws, request) => {
198
329
  const clientIp = request.socket.remoteAddress;
199
- let callbackLogLevel = "notice";
200
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
201
- callbackLogLevel = "info";
202
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
203
- callbackLogLevel = "debug";
330
+ // Set the global logger callback for the WebSocketServer
331
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
332
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
333
+ callbackLogLevel = "info" /* LogLevel.INFO */;
334
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
335
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
204
336
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
205
337
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
206
338
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -222,6 +354,7 @@ export class Frontend extends EventEmitter {
222
354
  }
223
355
  });
224
356
  ws.on('error', (error) => {
357
+ // istanbul ignore next
225
358
  this.log.error(`WebSocket client error: ${error}`);
226
359
  });
227
360
  });
@@ -235,6 +368,7 @@ export class Frontend extends EventEmitter {
235
368
  this.webSocketServer.on('error', (ws, error) => {
236
369
  this.log.error(`WebSocketServer error: ${error}`);
237
370
  });
371
+ // Subscribe to cli events
238
372
  cliEmitter.removeAllListeners();
239
373
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
240
374
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -245,6 +379,8 @@ export class Frontend extends EventEmitter {
245
379
  cliEmitter.on('cpu', (cpuUsage) => {
246
380
  this.wssSendCpuUpdate(cpuUsage);
247
381
  });
382
+ // Endpoint to validate login code
383
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
248
384
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
249
385
  const { password } = req.body;
250
386
  this.log.debug('The frontend sent /api/login', password);
@@ -263,23 +399,27 @@ export class Frontend extends EventEmitter {
263
399
  this.log.warn('/api/login error wrong password');
264
400
  res.json({ valid: false });
265
401
  }
402
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
266
403
  }
267
404
  catch (error) {
268
405
  this.log.error('/api/login error getting password');
269
406
  res.json({ valid: false });
270
407
  }
271
408
  });
409
+ // Endpoint to provide health check for docker
272
410
  this.expressApp.get('/health', (req, res) => {
273
411
  this.log.debug('Express received /health');
274
412
  const healthStatus = {
275
- status: 'ok',
276
- uptime: process.uptime(),
277
- timestamp: new Date().toISOString(),
413
+ status: 'ok', // Indicate service is healthy
414
+ uptime: process.uptime(), // Server uptime in seconds
415
+ timestamp: new Date().toISOString(), // Current timestamp
278
416
  };
279
417
  res.status(200).json(healthStatus);
280
418
  });
419
+ // Endpoint to provide memory usage details
281
420
  this.expressApp.get('/memory', async (req, res) => {
282
421
  this.log.debug('Express received /memory');
422
+ // Memory usage from process
283
423
  const memoryUsageRaw = process.memoryUsage();
284
424
  const memoryUsage = {
285
425
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -288,10 +428,13 @@ export class Frontend extends EventEmitter {
288
428
  external: this.formatMemoryUsage(memoryUsageRaw.external),
289
429
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
290
430
  };
431
+ // V8 heap statistics
291
432
  const { default: v8 } = await import('node:v8');
292
433
  const heapStatsRaw = v8.getHeapStatistics();
293
434
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
435
+ // Format heapStats
294
436
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
437
+ // Format heapSpaces
295
438
  const heapSpaces = heapSpacesRaw.map((space) => ({
296
439
  ...space,
297
440
  space_size: this.formatMemoryUsage(space.space_size),
@@ -309,19 +452,23 @@ export class Frontend extends EventEmitter {
309
452
  };
310
453
  res.status(200).json(memoryReport);
311
454
  });
455
+ // Endpoint to provide settings
312
456
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
313
457
  this.log.debug('The frontend sent /api/settings');
314
458
  res.json(await this.getApiSettings());
315
459
  });
460
+ // Endpoint to provide plugins
316
461
  this.expressApp.get('/api/plugins', async (req, res) => {
317
462
  this.log.debug('The frontend sent /api/plugins');
318
463
  res.json(this.getPlugins());
319
464
  });
465
+ // Endpoint to provide devices
320
466
  this.expressApp.get('/api/devices', async (req, res) => {
321
467
  this.log.debug('The frontend sent /api/devices');
322
468
  const devices = await this.getDevices();
323
469
  res.json(devices);
324
470
  });
471
+ // Endpoint to view the matterbridge log
325
472
  this.expressApp.get('/api/view-mblog', async (req, res) => {
326
473
  this.log.debug('The frontend sent /api/view-mblog');
327
474
  try {
@@ -334,6 +481,7 @@ export class Frontend extends EventEmitter {
334
481
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
335
482
  }
336
483
  });
484
+ // Endpoint to view the matter.js log
337
485
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
338
486
  this.log.debug('The frontend sent /api/view-mjlog');
339
487
  try {
@@ -346,6 +494,7 @@ export class Frontend extends EventEmitter {
346
494
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
347
495
  }
348
496
  });
497
+ // Endpoint to view the shelly log
349
498
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
350
499
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
351
500
  try {
@@ -358,9 +507,11 @@ export class Frontend extends EventEmitter {
358
507
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
359
508
  }
360
509
  });
510
+ // Endpoint to download the matterbridge log
361
511
  this.expressApp.get('/api/download-mblog', async (req, res) => {
362
512
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
363
513
  try {
514
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
364
515
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), fs.constants.F_OK);
365
516
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), 'utf8');
366
517
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), data, 'utf-8');
@@ -371,15 +522,18 @@ export class Frontend extends EventEmitter {
371
522
  }
372
523
  res.type('text/plain');
373
524
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), 'matterbridge.log', (error) => {
525
+ /* istanbul ignore if */
374
526
  if (error) {
375
527
  this.log.error(`Error downloading log file ${this.matterbridge.matterbridgeLoggerFile}: ${error instanceof Error ? error.message : error}`);
376
528
  res.status(500).send('Error downloading the matterbridge log file');
377
529
  }
378
530
  });
379
531
  });
532
+ // Endpoint to download the matter log
380
533
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
381
534
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
382
535
  try {
536
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
383
537
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
384
538
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
385
539
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
@@ -390,15 +544,18 @@ export class Frontend extends EventEmitter {
390
544
  }
391
545
  res.type('text/plain');
392
546
  res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
547
+ /* istanbul ignore if */
393
548
  if (error) {
394
549
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
395
550
  res.status(500).send('Error downloading the matter log file');
396
551
  }
397
552
  });
398
553
  });
554
+ // Endpoint to download the shelly log
399
555
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
400
556
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
401
557
  try {
558
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
402
559
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
403
560
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
404
561
  await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
@@ -409,74 +566,90 @@ export class Frontend extends EventEmitter {
409
566
  }
410
567
  res.type('text/plain');
411
568
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
569
+ /* istanbul ignore if */
412
570
  if (error) {
413
571
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
414
572
  res.status(500).send('Error downloading Shelly system log file');
415
573
  }
416
574
  });
417
575
  });
576
+ // Endpoint to download the matterbridge storage directory
418
577
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
419
578
  this.log.debug('The frontend sent /api/download-mbstorage');
420
579
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
421
580
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
581
+ /* istanbul ignore if */
422
582
  if (error) {
423
583
  this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
424
584
  res.status(500).send('Error downloading the matterbridge storage file');
425
585
  }
426
586
  });
427
587
  });
588
+ // Endpoint to download the matter storage file
428
589
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
429
590
  this.log.debug('The frontend sent /api/download-mjstorage');
430
591
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
431
592
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => {
593
+ /* istanbul ignore if */
432
594
  if (error) {
433
595
  this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`);
434
596
  res.status(500).send('Error downloading the matter storage zip file');
435
597
  }
436
598
  });
437
599
  });
600
+ // Endpoint to download the matterbridge plugin directory
438
601
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
439
602
  this.log.debug('The frontend sent /api/download-pluginstorage');
440
603
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
441
604
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
605
+ /* istanbul ignore if */
442
606
  if (error) {
443
607
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
444
608
  res.status(500).send('Error downloading the matterbridge plugin storage file');
445
609
  }
446
610
  });
447
611
  });
612
+ // Endpoint to download the matterbridge plugin config files
448
613
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
449
614
  this.log.debug('The frontend sent /api/download-pluginconfig');
450
615
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
451
616
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
617
+ /* istanbul ignore if */
452
618
  if (error) {
453
619
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
454
620
  res.status(500).send('Error downloading the matterbridge plugin config file');
455
621
  }
456
622
  });
457
623
  });
624
+ // Endpoint to download the matterbridge backup (created with the backup command)
458
625
  this.expressApp.get('/api/download-backup', async (req, res) => {
459
626
  this.log.debug('The frontend sent /api/download-backup');
460
627
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
628
+ /* istanbul ignore if */
461
629
  if (error) {
462
630
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
463
631
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
464
632
  }
465
633
  });
466
634
  });
635
+ // Endpoint to upload a package
467
636
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
468
637
  const { filename } = req.body;
469
638
  const file = req.file;
639
+ /* istanbul ignore if */
470
640
  if (!file || !filename) {
471
641
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
472
642
  res.status(400).send('Invalid request: file and filename are required');
473
643
  return;
474
644
  }
475
645
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
646
+ // Define the path where the plugin file will be saved
476
647
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
477
648
  try {
649
+ // Move the uploaded file to the specified path
478
650
  await fs.rename(file.path, filePath);
479
651
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
652
+ // Install the plugin package
480
653
  if (filename.endsWith('.tgz')) {
481
654
  const { spawnCommand } = await import('./utils/spawn.js');
482
655
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
@@ -496,6 +669,7 @@ export class Frontend extends EventEmitter {
496
669
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
497
670
  }
498
671
  });
672
+ // Fallback for routing (must be the last route)
499
673
  this.expressApp.use((req, res) => {
500
674
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
501
675
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -504,13 +678,16 @@ export class Frontend extends EventEmitter {
504
678
  }
505
679
  async stop() {
506
680
  this.log.debug('Stopping the frontend...');
681
+ // Remove listeners from the express app
507
682
  if (this.expressApp) {
508
683
  this.expressApp.removeAllListeners();
509
684
  this.expressApp = undefined;
510
685
  this.log.debug('Frontend app closed successfully');
511
686
  }
687
+ // Close the WebSocket server
512
688
  if (this.webSocketServer) {
513
689
  this.log.debug('Closing WebSocket server...');
690
+ // Close all active connections
514
691
  this.webSocketServer.clients.forEach((client) => {
515
692
  if (client.readyState === WebSocket.OPEN) {
516
693
  client.close();
@@ -519,6 +696,7 @@ export class Frontend extends EventEmitter {
519
696
  await withTimeout(new Promise((resolve) => {
520
697
  this.webSocketServer?.close((error) => {
521
698
  if (error) {
699
+ // istanbul ignore next
522
700
  this.log.error(`Error closing WebSocket server: ${error}`);
523
701
  }
524
702
  else {
@@ -531,11 +709,13 @@ export class Frontend extends EventEmitter {
531
709
  this.webSocketServer.removeAllListeners();
532
710
  this.webSocketServer = undefined;
533
711
  }
712
+ // Close the http server
534
713
  if (this.httpServer) {
535
714
  this.log.debug('Closing http server...');
536
715
  await withTimeout(new Promise((resolve) => {
537
716
  this.httpServer?.close((error) => {
538
717
  if (error) {
718
+ // istanbul ignore next
539
719
  this.log.error(`Error closing http server: ${error}`);
540
720
  }
541
721
  else {
@@ -549,11 +729,13 @@ export class Frontend extends EventEmitter {
549
729
  this.httpServer = undefined;
550
730
  this.log.debug('Frontend http server closed successfully');
551
731
  }
732
+ // Close the https server
552
733
  if (this.httpsServer) {
553
734
  this.log.debug('Closing https server...');
554
735
  await withTimeout(new Promise((resolve) => {
555
736
  this.httpsServer?.close((error) => {
556
737
  if (error) {
738
+ // istanbul ignore next
557
739
  this.log.error(`Error closing https server: ${error}`);
558
740
  }
559
741
  else {
@@ -569,6 +751,7 @@ export class Frontend extends EventEmitter {
569
751
  }
570
752
  this.log.debug('Frontend stopped successfully');
571
753
  }
754
+ // Function to format bytes to KB, MB, or GB
572
755
  formatMemoryUsage = (bytes) => {
573
756
  if (bytes >= 1024 ** 3) {
574
757
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -580,6 +763,7 @@ export class Frontend extends EventEmitter {
580
763
  return `${(bytes / 1024).toFixed(2)} KB`;
581
764
  }
582
765
  };
766
+ // Function to format system uptime with only the most significant unit
583
767
  formatOsUpTime = (seconds) => {
584
768
  if (seconds >= 86400) {
585
769
  const days = Math.floor(seconds / 86400);
@@ -595,7 +779,13 @@ export class Frontend extends EventEmitter {
595
779
  }
596
780
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
597
781
  };
782
+ /**
783
+ * Retrieves the api settings data.
784
+ *
785
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
786
+ */
598
787
  async getApiSettings() {
788
+ // Update the system information
599
789
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
600
790
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
601
791
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -604,6 +794,7 @@ export class Frontend extends EventEmitter {
604
794
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
605
795
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
606
796
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
797
+ // Update the matterbridge information
607
798
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
608
799
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
609
800
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
@@ -615,6 +806,7 @@ export class Frontend extends EventEmitter {
615
806
  this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
616
807
  this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
617
808
  this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
809
+ // Update the matterbridge information in bridge mode
618
810
  if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode && !this.matterbridge.hasCleanupStarted) {
619
811
  this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.serverNode.state.commissioning.commissioned;
620
812
  this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.qrPairingCode;
@@ -624,6 +816,12 @@ export class Frontend extends EventEmitter {
624
816
  }
625
817
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
626
818
  }
819
+ /**
820
+ * Retrieves the reachable attribute.
821
+ *
822
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
823
+ * @returns {boolean} The reachable attribute.
824
+ */
627
825
  getReachability(device) {
628
826
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
629
827
  return false;
@@ -635,6 +833,12 @@ export class Frontend extends EventEmitter {
635
833
  return true;
636
834
  return false;
637
835
  }
836
+ /**
837
+ * Retrieves the power source attribute.
838
+ *
839
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
840
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
841
+ */
638
842
  getPowerSource(endpoint) {
639
843
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
640
844
  return undefined;
@@ -650,13 +854,21 @@ export class Frontend extends EventEmitter {
650
854
  }
651
855
  return;
652
856
  };
857
+ // Root endpoint
653
858
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
654
859
  return powerSource(endpoint);
860
+ // Child endpoints
655
861
  for (const child of endpoint.getChildEndpoints()) {
656
862
  if (child.hasClusterServer(PowerSource.Cluster.id))
657
863
  return powerSource(child);
658
864
  }
659
865
  }
866
+ /**
867
+ * Retrieves the commissioned status, matter pairing codes, fabrics and sessions from a given device in server mode.
868
+ *
869
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the data from.
870
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
871
+ */
660
872
  getMatterDataFromDevice(device) {
661
873
  if (device.mode === 'server' && device.serverNode) {
662
874
  return {
@@ -668,6 +880,13 @@ export class Frontend extends EventEmitter {
668
880
  };
669
881
  }
670
882
  }
883
+ /**
884
+ * Retrieves the cluster text description from a given device.
885
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
886
+ *
887
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
888
+ * @returns {string} The attributes description of the cluster servers in the device.
889
+ */
671
890
  getClusterTextFromDevice(device) {
672
891
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
673
892
  return '';
@@ -678,6 +897,7 @@ export class Frontend extends EventEmitter {
678
897
  if (composed)
679
898
  return 'Composed: ' + composed.value;
680
899
  }
900
+ // istanbul ignore next cause is not reachable
681
901
  return '';
682
902
  };
683
903
  const getFixedLabel = (device) => {
@@ -687,11 +907,13 @@ export class Frontend extends EventEmitter {
687
907
  if (composed)
688
908
  return 'Composed: ' + composed.value;
689
909
  }
910
+ // istanbul ignore next cause is not reacheable
690
911
  return '';
691
912
  };
692
913
  let attributes = '';
693
914
  let supportedModes = [];
694
915
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
916
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
695
917
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
696
918
  return;
697
919
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -781,11 +1003,17 @@ export class Frontend extends EventEmitter {
781
1003
  if (clusterName === 'userLabel' && attributeName === 'labelList')
782
1004
  attributes += `${getUserLabel(device)} `;
783
1005
  });
1006
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
784
1007
  return attributes.trimStart().trimEnd();
785
1008
  }
1009
+ /**
1010
+ * Retrieves the registered plugins sanitized for res.json().
1011
+ *
1012
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1013
+ */
786
1014
  getPlugins() {
787
1015
  if (this.matterbridge.hasCleanupStarted)
788
- return [];
1016
+ return []; // Skip if cleanup has started
789
1017
  const baseRegisteredPlugins = [];
790
1018
  for (const plugin of this.matterbridge.plugins) {
791
1019
  baseRegisteredPlugins.push({
@@ -815,6 +1043,7 @@ export class Frontend extends EventEmitter {
815
1043
  schemaJson: plugin.schemaJson,
816
1044
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
817
1045
  hasBlackList: plugin.configJson?.blackList !== undefined,
1046
+ // Childbridge mode specific data
818
1047
  paired: plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.commissioned : undefined,
819
1048
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.qrPairingCode : undefined,
820
1049
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode && plugin.serverNode.lifecycle.isOnline ? plugin.serverNode.state.commissioning.pairingCodes.manualPairingCode : undefined,
@@ -824,13 +1053,21 @@ export class Frontend extends EventEmitter {
824
1053
  }
825
1054
  return baseRegisteredPlugins;
826
1055
  }
1056
+ /**
1057
+ * Retrieves the devices from Matterbridge.
1058
+ *
1059
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1060
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1061
+ */
827
1062
  async getDevices(pluginName) {
828
1063
  if (this.matterbridge.hasCleanupStarted)
829
- return [];
1064
+ return []; // Skip if cleanup has started
830
1065
  const devices = [];
831
1066
  for (const device of this.matterbridge.devices.array()) {
1067
+ // Filter by pluginName if provided
832
1068
  if (pluginName && pluginName !== device.plugin)
833
1069
  continue;
1070
+ // Check if the device has the required properties
834
1071
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
835
1072
  continue;
836
1073
  devices.push({
@@ -850,22 +1087,37 @@ export class Frontend extends EventEmitter {
850
1087
  }
851
1088
  return devices;
852
1089
  }
1090
+ /**
1091
+ * Retrieves the clusters from a given plugin and endpoint number.
1092
+ *
1093
+ * Response for /api/clusters
1094
+ *
1095
+ * @param {string} pluginName - The name of the plugin.
1096
+ * @param {number} endpointNumber - The endpoint number.
1097
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1098
+ */
853
1099
  getClusters(pluginName, endpointNumber) {
854
1100
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
855
1101
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
856
1102
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
857
1103
  return;
858
1104
  }
1105
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1106
+ // Get the device types from the main endpoint
859
1107
  const deviceTypes = [];
860
1108
  const clusters = [];
861
1109
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
862
1110
  deviceTypes.push(d.deviceType);
863
1111
  });
1112
+ // Get the clusters from the main endpoint
864
1113
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
865
1114
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
866
1115
  return;
867
1116
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
868
1117
  return;
1118
+ // console.log(
1119
+ // `${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}`,
1120
+ // );
869
1121
  clusters.push({
870
1122
  endpoint: endpoint.number.toString(),
871
1123
  id: 'main',
@@ -878,12 +1130,19 @@ export class Frontend extends EventEmitter {
878
1130
  attributeLocalValue: attributeValue,
879
1131
  });
880
1132
  });
1133
+ // Get the child endpoints
881
1134
  const childEndpoints = endpoint.getChildEndpoints();
1135
+ // if (childEndpoints.length === 0) {
1136
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1137
+ // }
882
1138
  childEndpoints.forEach((childEndpoint) => {
1139
+ // istanbul ignore if cause is not reachable: should never happen but ...
883
1140
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
884
1141
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
885
1142
  return;
886
1143
  }
1144
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1145
+ // Get the device types of the child endpoint
887
1146
  const deviceTypes = [];
888
1147
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
889
1148
  deviceTypes.push(d.deviceType);
@@ -893,9 +1152,12 @@ export class Frontend extends EventEmitter {
893
1152
  return;
894
1153
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
895
1154
  return;
1155
+ // console.log(
1156
+ // `${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}`,
1157
+ // );
896
1158
  clusters.push({
897
1159
  endpoint: childEndpoint.number.toString(),
898
- id: childEndpoint.maybeId ?? 'null',
1160
+ id: childEndpoint.maybeId ?? 'null', // Never happens
899
1161
  deviceTypes,
900
1162
  clusterName: capitalizeFirstLetter(clusterName),
901
1163
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -908,6 +1170,13 @@ export class Frontend extends EventEmitter {
908
1170
  });
909
1171
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
910
1172
  }
1173
+ /**
1174
+ * Handles incoming websocket messages for the Matterbridge frontend.
1175
+ *
1176
+ * @param {WebSocket} client - The websocket client that sent the message.
1177
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1178
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1179
+ */
911
1180
  async wsMessageHandler(client, message) {
912
1181
  let data;
913
1182
  try {
@@ -954,33 +1223,44 @@ export class Frontend extends EventEmitter {
954
1223
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
955
1224
  const packageName = data.params.packageName.replace(/@.*$/, '');
956
1225
  if (data.params.restart === false && packageName !== 'matterbridge') {
1226
+ // The install comes from InstallPlugins
957
1227
  this.matterbridge.plugins
958
1228
  .add(packageName)
959
1229
  .then((plugin) => {
960
1230
  if (plugin) {
1231
+ // The plugin is not registered
961
1232
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
962
1233
  this.matterbridge.plugins
963
1234
  .load(plugin, true, 'The plugin has been added', true)
1235
+ // eslint-disable-next-line promise/no-nesting
964
1236
  .then(() => {
965
1237
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
966
1238
  this.wssSendRefreshRequired('plugins');
967
1239
  return;
968
1240
  })
1241
+ // eslint-disable-next-line promise/no-nesting
969
1242
  .catch((_error) => {
1243
+ //
970
1244
  });
971
1245
  }
972
1246
  else {
1247
+ // The plugin is already registered
973
1248
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
974
1249
  this.wssSendRefreshRequired('plugins');
975
1250
  this.wssSendRestartRequired(true, true);
976
1251
  }
977
1252
  return;
978
1253
  })
1254
+ // eslint-disable-next-line promise/no-nesting
979
1255
  .catch((_error) => {
1256
+ //
980
1257
  });
981
1258
  }
982
1259
  else {
1260
+ // The package is matterbridge
1261
+ // istanbul ignore next
983
1262
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = true;
1263
+ // istanbul ignore next if
984
1264
  if (this.matterbridge.restartMode !== '') {
985
1265
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
986
1266
  this.matterbridge.shutdownProcess();
@@ -1002,6 +1282,7 @@ export class Frontend extends EventEmitter {
1002
1282
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
1003
1283
  return;
1004
1284
  }
1285
+ // The package is a plugin
1005
1286
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
1006
1287
  if (plugin) {
1007
1288
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -1010,6 +1291,7 @@ export class Frontend extends EventEmitter {
1010
1291
  this.wssSendRefreshRequired('plugins');
1011
1292
  this.wssSendRefreshRequired('devices');
1012
1293
  }
1294
+ // Uninstall the package
1013
1295
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
1014
1296
  const { spawnCommand } = await import('./utils/spawn.js');
1015
1297
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1050,6 +1332,7 @@ export class Frontend extends EventEmitter {
1050
1332
  return;
1051
1333
  })
1052
1334
  .catch((_error) => {
1335
+ //
1053
1336
  });
1054
1337
  }
1055
1338
  else {
@@ -1096,6 +1379,7 @@ export class Frontend extends EventEmitter {
1096
1379
  return;
1097
1380
  })
1098
1381
  .catch((_error) => {
1382
+ //
1099
1383
  });
1100
1384
  }
1101
1385
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1121,6 +1405,7 @@ export class Frontend extends EventEmitter {
1121
1405
  const plugin = this.matterbridge.plugins.get(data.params.pluginName);
1122
1406
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
1123
1407
  if (plugin.serverNode) {
1408
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1124
1409
  await this.matterbridge.stopServerNode(plugin.serverNode);
1125
1410
  plugin.serverNode = undefined;
1126
1411
  }
@@ -1131,15 +1416,16 @@ export class Frontend extends EventEmitter {
1131
1416
  }
1132
1417
  }
1133
1418
  await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
1134
- plugin.restartRequired = false;
1419
+ plugin.restartRequired = false; // Reset plugin restartRequired
1135
1420
  let needRestart = 0;
1136
1421
  for (const plugin of this.matterbridge.plugins) {
1137
1422
  if (plugin.restartRequired)
1138
1423
  needRestart++;
1139
1424
  }
1140
1425
  if (needRestart === 0) {
1141
- this.wssSendRestartNotRequired(true);
1426
+ this.wssSendRestartNotRequired(true); // Reset global restart required message
1142
1427
  }
1428
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1143
1429
  if (plugin.serverNode)
1144
1430
  await this.matterbridge.startServerNode(plugin.serverNode);
1145
1431
  this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
@@ -1245,6 +1531,8 @@ export class Frontend extends EventEmitter {
1245
1531
  else if (data.method === '/api/advertise') {
1246
1532
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1247
1533
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1534
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1535
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1248
1536
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1249
1537
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1250
1538
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1367,22 +1655,22 @@ export class Frontend extends EventEmitter {
1367
1655
  if (isValidString(data.params.value, 4)) {
1368
1656
  this.log.debug('Matterbridge logger level:', data.params.value);
1369
1657
  if (data.params.value === 'Debug') {
1370
- await this.matterbridge.setLogLevel("debug");
1658
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1371
1659
  }
1372
1660
  else if (data.params.value === 'Info') {
1373
- await this.matterbridge.setLogLevel("info");
1661
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1374
1662
  }
1375
1663
  else if (data.params.value === 'Notice') {
1376
- await this.matterbridge.setLogLevel("notice");
1664
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1377
1665
  }
1378
1666
  else if (data.params.value === 'Warn') {
1379
- await this.matterbridge.setLogLevel("warn");
1667
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1380
1668
  }
1381
1669
  else if (data.params.value === 'Error') {
1382
- await this.matterbridge.setLogLevel("error");
1670
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1383
1671
  }
1384
1672
  else if (data.params.value === 'Fatal') {
1385
- await this.matterbridge.setLogLevel("fatal");
1673
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1386
1674
  }
1387
1675
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1388
1676
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1393,6 +1681,7 @@ export class Frontend extends EventEmitter {
1393
1681
  this.log.debug('Matterbridge file log:', data.params.value);
1394
1682
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1395
1683
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1684
+ // Create the file logger for matterbridge
1396
1685
  if (data.params.value)
1397
1686
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1398
1687
  else
@@ -1439,6 +1728,7 @@ export class Frontend extends EventEmitter {
1439
1728
  });
1440
1729
  }
1441
1730
  catch (error) {
1731
+ /* istanbul ignore next */
1442
1732
  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}`);
1443
1733
  }
1444
1734
  }
@@ -1447,6 +1737,7 @@ export class Frontend extends EventEmitter {
1447
1737
  Logger.removeLogger('matterfilelogger');
1448
1738
  }
1449
1739
  catch (error) {
1740
+ /* istanbul ignore next */
1450
1741
  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}`);
1451
1742
  }
1452
1743
  }
@@ -1559,15 +1850,19 @@ export class Frontend extends EventEmitter {
1559
1850
  return;
1560
1851
  }
1561
1852
  const config = plugin.configJson;
1853
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1562
1854
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1855
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1563
1856
  if (select === 'serial')
1564
1857
  this.log.info(`Selected device serial ${data.params.serial}`);
1565
1858
  if (select === 'name')
1566
1859
  this.log.info(`Selected device name ${data.params.name}`);
1567
1860
  if (config && select && (select === 'serial' || select === 'name')) {
1861
+ // Remove postfix from the serial if it exists
1568
1862
  if (config.postfix) {
1569
1863
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1570
1864
  }
1865
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1571
1866
  if (isValidArray(config.whiteList, 1)) {
1572
1867
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1573
1868
  config.whiteList.push(data.params.serial);
@@ -1576,6 +1871,7 @@ export class Frontend extends EventEmitter {
1576
1871
  config.whiteList.push(data.params.name);
1577
1872
  }
1578
1873
  }
1874
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1579
1875
  if (isValidArray(config.blackList, 1)) {
1580
1876
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1581
1877
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1606,7 +1902,9 @@ export class Frontend extends EventEmitter {
1606
1902
  return;
1607
1903
  }
1608
1904
  const config = plugin.configJson;
1905
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1609
1906
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1907
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1610
1908
  if (select === 'serial')
1611
1909
  this.log.info(`Unselected device serial ${data.params.serial}`);
1612
1910
  if (select === 'name')
@@ -1615,6 +1913,7 @@ export class Frontend extends EventEmitter {
1615
1913
  if (config.postfix) {
1616
1914
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1617
1915
  }
1916
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1618
1917
  if (isValidArray(config.whiteList, 1)) {
1619
1918
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1620
1919
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1623,6 +1922,7 @@ export class Frontend extends EventEmitter {
1623
1922
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1624
1923
  }
1625
1924
  }
1925
+ // Add the serial to the blackList
1626
1926
  if (isValidArray(config.blackList)) {
1627
1927
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1628
1928
  config.blackList.push(data.params.serial);
@@ -1656,126 +1956,251 @@ export class Frontend extends EventEmitter {
1656
1956
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1657
1957
  }
1658
1958
  }
1959
+ /**
1960
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1961
+ *
1962
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1963
+ * @param {string} time - The time string of the message
1964
+ * @param {string} name - The logger name of the message
1965
+ * @param {string} message - The content of the message.
1966
+ *
1967
+ * @remarks
1968
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1969
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1970
+ * The function sends the message to all connected clients.
1971
+ */
1659
1972
  wssSendMessage(level, time, name, message) {
1660
1973
  if (!level || !time || !name || !message)
1661
1974
  return;
1975
+ // Remove ANSI escape codes from the message
1976
+ // eslint-disable-next-line no-control-regex
1662
1977
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1978
+ // Remove leading asterisks from the message
1663
1979
  message = message.replace(/^\*+/, '');
1980
+ // Replace all occurrences of \t and \n
1664
1981
  message = message.replace(/[\t\n]/g, '');
1982
+ // Remove non-printable characters
1983
+ // eslint-disable-next-line no-control-regex
1665
1984
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1985
+ // Replace all occurrences of \" with "
1666
1986
  message = message.replace(/\\"/g, '"');
1987
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1667
1988
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1989
+ // Define the maximum allowed length for continuous characters without a space
1668
1990
  const maxContinuousLength = 100;
1669
1991
  const keepStartLength = 20;
1670
1992
  const keepEndLength = 20;
1993
+ // Split the message into words
1671
1994
  message = message
1672
1995
  .split(' ')
1673
1996
  .map((word) => {
1997
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1674
1998
  if (word.length > maxContinuousLength) {
1675
1999
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1676
2000
  }
1677
2001
  return word;
1678
2002
  })
1679
2003
  .join(' ');
2004
+ // Send the message to all connected clients
1680
2005
  this.webSocketServer?.clients.forEach((client) => {
1681
2006
  if (client.readyState === WebSocket.OPEN) {
1682
2007
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1683
2008
  }
1684
2009
  });
1685
2010
  }
2011
+ /**
2012
+ * Sends a need to refresh WebSocket message to all connected clients.
2013
+ *
2014
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
2015
+ * possible values:
2016
+ * - 'matterbridgeLatestVersion'
2017
+ * - 'matterbridgeDevVersion'
2018
+ * - 'matterbridgeAdvertise'
2019
+ * - 'online'
2020
+ * - 'offline'
2021
+ * - 'reachability'
2022
+ * - 'settings'
2023
+ * - 'plugins'
2024
+ * - 'pluginsRestart'
2025
+ * - 'devices'
2026
+ * - 'fabrics'
2027
+ * - 'sessions'
2028
+ */
1686
2029
  wssSendRefreshRequired(changed = null) {
1687
2030
  this.log.debug('Sending a refresh required message to all connected clients');
2031
+ // Send the message to all connected clients
1688
2032
  this.webSocketServer?.clients.forEach((client) => {
1689
2033
  if (client.readyState === WebSocket.OPEN) {
1690
2034
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1691
2035
  }
1692
2036
  });
1693
2037
  }
2038
+ /**
2039
+ * Sends a need to restart WebSocket message to all connected clients.
2040
+ *
2041
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
2042
+ * @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
2043
+ */
1694
2044
  wssSendRestartRequired(snackbar = true, fixed = false) {
1695
2045
  this.log.debug('Sending a restart required message to all connected clients');
1696
2046
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1697
2047
  this.matterbridge.matterbridgeInformation.fixedRestartRequired = fixed;
1698
2048
  if (snackbar === true)
1699
2049
  this.wssSendSnackbarMessage(`Restart required`, 0);
2050
+ // Send the message to all connected clients
1700
2051
  this.webSocketServer?.clients.forEach((client) => {
1701
2052
  if (client.readyState === WebSocket.OPEN) {
1702
2053
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: { fixed } }));
1703
2054
  }
1704
2055
  });
1705
2056
  }
2057
+ /**
2058
+ * Sends a no need to restart WebSocket message to all connected clients.
2059
+ *
2060
+ * @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients.
2061
+ */
1706
2062
  wssSendRestartNotRequired(snackbar = true) {
1707
2063
  this.log.debug('Sending a restart not required message to all connected clients');
1708
2064
  this.matterbridge.matterbridgeInformation.restartRequired = false;
1709
2065
  if (snackbar === true)
1710
2066
  this.wssSendCloseSnackbarMessage(`Restart required`);
2067
+ // Send the message to all connected clients
1711
2068
  this.webSocketServer?.clients.forEach((client) => {
1712
2069
  if (client.readyState === WebSocket.OPEN) {
1713
2070
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', params: {} }));
1714
2071
  }
1715
2072
  });
1716
2073
  }
2074
+ /**
2075
+ * Sends a need to update WebSocket message to all connected clients.
2076
+ *
2077
+ * @param {boolean} devVersion - If true, the update is for a development version.
2078
+ */
1717
2079
  wssSendUpdateRequired(devVersion = false) {
1718
2080
  this.log.debug('Sending an update required message to all connected clients');
1719
2081
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2082
+ // Send the message to all connected clients
1720
2083
  this.webSocketServer?.clients.forEach((client) => {
1721
2084
  if (client.readyState === WebSocket.OPEN) {
1722
2085
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: devVersion ? 'update_required_dev' : 'update_required', params: {} }));
1723
2086
  }
1724
2087
  });
1725
2088
  }
2089
+ /**
2090
+ * Sends a cpu update message to all connected clients.
2091
+ *
2092
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2093
+ */
1726
2094
  wssSendCpuUpdate(cpuUsage) {
1727
2095
  if (hasParameter('debug'))
1728
2096
  this.log.debug('Sending a cpu update message to all connected clients');
2097
+ // Send the message to all connected clients
1729
2098
  this.webSocketServer?.clients.forEach((client) => {
1730
2099
  if (client.readyState === WebSocket.OPEN) {
1731
2100
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1732
2101
  }
1733
2102
  });
1734
2103
  }
2104
+ /**
2105
+ * Sends a memory update message to all connected clients.
2106
+ *
2107
+ * @param {string} totalMemory - The total memory in bytes.
2108
+ * @param {string} freeMemory - The free memory in bytes.
2109
+ * @param {string} rss - The resident set size in bytes.
2110
+ * @param {string} heapTotal - The total heap memory in bytes.
2111
+ * @param {string} heapUsed - The used heap memory in bytes.
2112
+ * @param {string} external - The external memory in bytes.
2113
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2114
+ */
1735
2115
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1736
2116
  if (hasParameter('debug'))
1737
2117
  this.log.debug('Sending a memory update message to all connected clients');
2118
+ // Send the message to all connected clients
1738
2119
  this.webSocketServer?.clients.forEach((client) => {
1739
2120
  if (client.readyState === WebSocket.OPEN) {
1740
2121
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1741
2122
  }
1742
2123
  });
1743
2124
  }
2125
+ /**
2126
+ * Sends an uptime update message to all connected clients.
2127
+ *
2128
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2129
+ * @param {string} processUptime - The process uptime in a human-readable format.
2130
+ */
1744
2131
  wssSendUptimeUpdate(systemUptime, processUptime) {
1745
2132
  if (hasParameter('debug'))
1746
2133
  this.log.debug('Sending a uptime update message to all connected clients');
2134
+ // Send the message to all connected clients
1747
2135
  this.webSocketServer?.clients.forEach((client) => {
1748
2136
  if (client.readyState === WebSocket.OPEN) {
1749
2137
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1750
2138
  }
1751
2139
  });
1752
2140
  }
2141
+ /**
2142
+ * Sends an open snackbar message to all connected clients.
2143
+ *
2144
+ * @param {string} message - The message to send.
2145
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2146
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2147
+ */
1753
2148
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1754
2149
  this.log.debug('Sending a snackbar message to all connected clients');
2150
+ // Send the message to all connected clients
1755
2151
  this.webSocketServer?.clients.forEach((client) => {
1756
2152
  if (client.readyState === WebSocket.OPEN) {
1757
2153
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1758
2154
  }
1759
2155
  });
1760
2156
  }
2157
+ /**
2158
+ * Sends a close snackbar message to all connected clients.
2159
+ *
2160
+ * @param {string} message - The message to send.
2161
+ */
1761
2162
  wssSendCloseSnackbarMessage(message) {
1762
2163
  this.log.debug('Sending a close snackbar message to all connected clients');
2164
+ // Send the message to all connected clients
1763
2165
  this.webSocketServer?.clients.forEach((client) => {
1764
2166
  if (client.readyState === WebSocket.OPEN) {
1765
2167
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1766
2168
  }
1767
2169
  });
1768
2170
  }
2171
+ /**
2172
+ * Sends an attribute update message to all connected WebSocket clients.
2173
+ *
2174
+ * @param {string | undefined} plugin - The name of the plugin.
2175
+ * @param {string | undefined} serialNumber - The serial number of the device.
2176
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2177
+ * @param {string} cluster - The cluster name where the attribute belongs.
2178
+ * @param {string} attribute - The name of the attribute that changed.
2179
+ * @param {number | string | boolean} value - The new value of the attribute.
2180
+ *
2181
+ * @remarks
2182
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2183
+ * with the updated attribute information.
2184
+ */
1769
2185
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1770
2186
  this.log.debug('Sending an attribute update message to all connected clients');
2187
+ // Send the message to all connected clients
1771
2188
  this.webSocketServer?.clients.forEach((client) => {
1772
2189
  if (client.readyState === WebSocket.OPEN) {
1773
2190
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1774
2191
  }
1775
2192
  });
1776
2193
  }
2194
+ /**
2195
+ * Sends a message to all connected clients.
2196
+ *
2197
+ * @param {number} id - The message id.
2198
+ * @param {string} method - The message method.
2199
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2200
+ */
1777
2201
  wssBroadcastMessage(id, method, params) {
1778
2202
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2203
+ // Send the message to all connected clients
1779
2204
  this.webSocketServer?.clients.forEach((client) => {
1780
2205
  if (client.readyState === WebSocket.OPEN) {
1781
2206
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1783,3 +2208,4 @@ export class Frontend extends EventEmitter {
1783
2208
  });
1784
2209
  }
1785
2210
  }
2211
+ //# sourceMappingURL=frontend.js.map