matterbridge 3.1.4-dev-20250715-075e722 → 3.1.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 (208) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/README.md +25 -2
  3. package/dist/cli.d.ts +26 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +91 -2
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cliEmitter.d.ts +34 -0
  8. package/dist/cliEmitter.d.ts.map +1 -0
  9. package/dist/cliEmitter.js +30 -0
  10. package/dist/cliEmitter.js.map +1 -0
  11. package/dist/clusters/export.d.ts +2 -0
  12. package/dist/clusters/export.d.ts.map +1 -0
  13. package/dist/clusters/export.js +2 -0
  14. package/dist/clusters/export.js.map +1 -0
  15. package/dist/defaultConfigSchema.d.ts +28 -0
  16. package/dist/defaultConfigSchema.d.ts.map +1 -0
  17. package/dist/defaultConfigSchema.js +24 -0
  18. package/dist/defaultConfigSchema.js.map +1 -0
  19. package/dist/deviceManager.d.ts +112 -0
  20. package/dist/deviceManager.d.ts.map +1 -0
  21. package/dist/deviceManager.js +94 -1
  22. package/dist/deviceManager.js.map +1 -0
  23. package/dist/devices/batteryStorage.d.ts +48 -0
  24. package/dist/devices/batteryStorage.d.ts.map +1 -0
  25. package/dist/devices/batteryStorage.js +48 -1
  26. package/dist/devices/batteryStorage.js.map +1 -0
  27. package/dist/devices/evse.d.ts +75 -0
  28. package/dist/devices/evse.d.ts.map +1 -0
  29. package/dist/devices/evse.js +74 -10
  30. package/dist/devices/evse.js.map +1 -0
  31. package/dist/devices/export.d.ts +9 -0
  32. package/dist/devices/export.d.ts.map +1 -0
  33. package/dist/devices/export.js +2 -0
  34. package/dist/devices/export.js.map +1 -0
  35. package/dist/devices/heatPump.d.ts +47 -0
  36. package/dist/devices/heatPump.d.ts.map +1 -0
  37. package/dist/devices/heatPump.js +50 -2
  38. package/dist/devices/heatPump.js.map +1 -0
  39. package/dist/devices/laundryDryer.d.ts +87 -0
  40. package/dist/devices/laundryDryer.d.ts.map +1 -0
  41. package/dist/devices/laundryDryer.js +83 -6
  42. package/dist/devices/laundryDryer.js.map +1 -0
  43. package/dist/devices/laundryWasher.d.ts +242 -0
  44. package/dist/devices/laundryWasher.d.ts.map +1 -0
  45. package/dist/devices/laundryWasher.js +91 -7
  46. package/dist/devices/laundryWasher.js.map +1 -0
  47. package/dist/devices/roboticVacuumCleaner.d.ts +110 -0
  48. package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
  49. package/dist/devices/roboticVacuumCleaner.js +89 -6
  50. package/dist/devices/roboticVacuumCleaner.js.map +1 -0
  51. package/dist/devices/solarPower.d.ts +40 -0
  52. package/dist/devices/solarPower.d.ts.map +1 -0
  53. package/dist/devices/solarPower.js +38 -0
  54. package/dist/devices/solarPower.js.map +1 -0
  55. package/dist/devices/waterHeater.d.ts +111 -0
  56. package/dist/devices/waterHeater.d.ts.map +1 -0
  57. package/dist/devices/waterHeater.js +82 -2
  58. package/dist/devices/waterHeater.js.map +1 -0
  59. package/dist/frontend.d.ts +304 -0
  60. package/dist/frontend.d.ts.map +1 -0
  61. package/dist/frontend.js +491 -49
  62. package/dist/frontend.js.map +1 -0
  63. package/dist/globalMatterbridge.d.ts +59 -0
  64. package/dist/globalMatterbridge.d.ts.map +1 -0
  65. package/dist/globalMatterbridge.js +47 -0
  66. package/dist/globalMatterbridge.js.map +1 -0
  67. package/dist/helpers.d.ts +48 -0
  68. package/dist/helpers.d.ts.map +1 -0
  69. package/dist/helpers.js +53 -0
  70. package/dist/helpers.js.map +1 -0
  71. package/dist/index.d.ts +33 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +30 -1
  74. package/dist/index.js.map +1 -0
  75. package/dist/logger/export.d.ts +2 -0
  76. package/dist/logger/export.d.ts.map +1 -0
  77. package/dist/logger/export.js +1 -0
  78. package/dist/logger/export.js.map +1 -0
  79. package/dist/matter/behaviors.d.ts +2 -0
  80. package/dist/matter/behaviors.d.ts.map +1 -0
  81. package/dist/matter/behaviors.js +2 -0
  82. package/dist/matter/behaviors.js.map +1 -0
  83. package/dist/matter/clusters.d.ts +2 -0
  84. package/dist/matter/clusters.d.ts.map +1 -0
  85. package/dist/matter/clusters.js +2 -0
  86. package/dist/matter/clusters.js.map +1 -0
  87. package/dist/matter/devices.d.ts +2 -0
  88. package/dist/matter/devices.d.ts.map +1 -0
  89. package/dist/matter/devices.js +2 -0
  90. package/dist/matter/devices.js.map +1 -0
  91. package/dist/matter/endpoints.d.ts +2 -0
  92. package/dist/matter/endpoints.d.ts.map +1 -0
  93. package/dist/matter/endpoints.js +2 -0
  94. package/dist/matter/endpoints.js.map +1 -0
  95. package/dist/matter/export.d.ts +5 -0
  96. package/dist/matter/export.d.ts.map +1 -0
  97. package/dist/matter/export.js +3 -0
  98. package/dist/matter/export.js.map +1 -0
  99. package/dist/matter/types.d.ts +3 -0
  100. package/dist/matter/types.d.ts.map +1 -0
  101. package/dist/matter/types.js +3 -0
  102. package/dist/matter/types.js.map +1 -0
  103. package/dist/matterbridge.d.ts +444 -0
  104. package/dist/matterbridge.d.ts.map +1 -0
  105. package/dist/matterbridge.js +785 -51
  106. package/dist/matterbridge.js.map +1 -0
  107. package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
  108. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  109. package/dist/matterbridgeAccessoryPlatform.js +36 -0
  110. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  111. package/dist/matterbridgeBehaviors.d.ts +1340 -0
  112. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  113. package/dist/matterbridgeBehaviors.js +61 -1
  114. package/dist/matterbridgeBehaviors.js.map +1 -0
  115. package/dist/matterbridgeDeviceTypes.d.ts +709 -0
  116. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  117. package/dist/matterbridgeDeviceTypes.js +579 -15
  118. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  119. package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
  120. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  121. package/dist/matterbridgeDynamicPlatform.js +36 -0
  122. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  123. package/dist/matterbridgeEndpoint.d.ts +1250 -0
  124. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  125. package/dist/matterbridgeEndpoint.js +1106 -42
  126. package/dist/matterbridgeEndpoint.js.map +1 -0
  127. package/dist/matterbridgeEndpointHelpers.d.ts +3198 -0
  128. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  129. package/dist/matterbridgeEndpointHelpers.js +322 -12
  130. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  131. package/dist/matterbridgePlatform.d.ts +310 -0
  132. package/dist/matterbridgePlatform.d.ts.map +1 -0
  133. package/dist/matterbridgePlatform.js +233 -0
  134. package/dist/matterbridgePlatform.js.map +1 -0
  135. package/dist/matterbridgeTypes.d.ts +195 -0
  136. package/dist/matterbridgeTypes.d.ts.map +1 -0
  137. package/dist/matterbridgeTypes.js +25 -0
  138. package/dist/matterbridgeTypes.js.map +1 -0
  139. package/dist/pluginManager.d.ts +291 -0
  140. package/dist/pluginManager.d.ts.map +1 -0
  141. package/dist/pluginManager.js +269 -3
  142. package/dist/pluginManager.js.map +1 -0
  143. package/dist/shelly.d.ts +174 -0
  144. package/dist/shelly.d.ts.map +1 -0
  145. package/dist/shelly.js +168 -7
  146. package/dist/shelly.js.map +1 -0
  147. package/dist/storage/export.d.ts +2 -0
  148. package/dist/storage/export.d.ts.map +1 -0
  149. package/dist/storage/export.js +1 -0
  150. package/dist/storage/export.js.map +1 -0
  151. package/dist/update.d.ts +59 -0
  152. package/dist/update.d.ts.map +1 -0
  153. package/dist/update.js +54 -0
  154. package/dist/update.js.map +1 -0
  155. package/dist/utils/colorUtils.d.ts +117 -0
  156. package/dist/utils/colorUtils.d.ts.map +1 -0
  157. package/dist/utils/colorUtils.js +263 -2
  158. package/dist/utils/colorUtils.js.map +1 -0
  159. package/dist/utils/commandLine.d.ts +59 -0
  160. package/dist/utils/commandLine.d.ts.map +1 -0
  161. package/dist/utils/commandLine.js +54 -0
  162. package/dist/utils/commandLine.js.map +1 -0
  163. package/dist/utils/copyDirectory.d.ts +33 -0
  164. package/dist/utils/copyDirectory.d.ts.map +1 -0
  165. package/dist/utils/copyDirectory.js +38 -1
  166. package/dist/utils/copyDirectory.js.map +1 -0
  167. package/dist/utils/createDirectory.d.ts +34 -0
  168. package/dist/utils/createDirectory.d.ts.map +1 -0
  169. package/dist/utils/createDirectory.js +33 -0
  170. package/dist/utils/createDirectory.js.map +1 -0
  171. package/dist/utils/createZip.d.ts +39 -0
  172. package/dist/utils/createZip.d.ts.map +1 -0
  173. package/dist/utils/createZip.js +47 -2
  174. package/dist/utils/createZip.js.map +1 -0
  175. package/dist/utils/deepCopy.d.ts +32 -0
  176. package/dist/utils/deepCopy.d.ts.map +1 -0
  177. package/dist/utils/deepCopy.js +39 -0
  178. package/dist/utils/deepCopy.js.map +1 -0
  179. package/dist/utils/deepEqual.d.ts +54 -0
  180. package/dist/utils/deepEqual.d.ts.map +1 -0
  181. package/dist/utils/deepEqual.js +72 -1
  182. package/dist/utils/deepEqual.js.map +1 -0
  183. package/dist/utils/export.d.ts +12 -0
  184. package/dist/utils/export.d.ts.map +1 -0
  185. package/dist/utils/export.js +1 -0
  186. package/dist/utils/export.js.map +1 -0
  187. package/dist/utils/hex.d.ts +49 -0
  188. package/dist/utils/hex.d.ts.map +1 -0
  189. package/dist/utils/hex.js +58 -0
  190. package/dist/utils/hex.js.map +1 -0
  191. package/dist/utils/isvalid.d.ts +103 -0
  192. package/dist/utils/isvalid.d.ts.map +1 -0
  193. package/dist/utils/isvalid.js +101 -0
  194. package/dist/utils/isvalid.js.map +1 -0
  195. package/dist/utils/network.d.ts +74 -0
  196. package/dist/utils/network.d.ts.map +1 -0
  197. package/dist/utils/network.js +81 -5
  198. package/dist/utils/network.js.map +1 -0
  199. package/dist/utils/spawn.d.ts +11 -0
  200. package/dist/utils/spawn.d.ts.map +1 -0
  201. package/dist/utils/spawn.js +18 -0
  202. package/dist/utils/spawn.js.map +1 -0
  203. package/dist/utils/wait.d.ts +56 -0
  204. package/dist/utils/wait.d.ts.map +1 -0
  205. package/dist/utils/wait.js +62 -9
  206. package/dist/utils/wait.js.map +1 -0
  207. package/npm-shrinkwrap.json +2 -2
  208. package/package.json +2 -1
package/dist/frontend.js CHANGED
@@ -1,36 +1,131 @@
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
- import { promises as fs } from 'node:fs';
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;
31
127
  log;
32
128
  port = 8283;
33
- initializeError = false;
34
129
  expressApp;
35
130
  httpServer;
36
131
  httpsServer;
@@ -38,7 +133,7 @@ export class Frontend extends EventEmitter {
38
133
  constructor(matterbridge) {
39
134
  super();
40
135
  this.matterbridge = matterbridge;
41
- 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 */ });
42
137
  }
43
138
  set logLevel(logLevel) {
44
139
  this.log.logLevel = logLevel;
@@ -46,13 +141,43 @@ export class Frontend extends EventEmitter {
46
141
  async start(port = 8283) {
47
142
  this.port = port;
48
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
49
145
  const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
50
146
  await fs.mkdir(uploadDir, { recursive: true });
51
147
  const upload = multer({ dest: uploadDir });
148
+ // Create the express app that serves the frontend
52
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
53
176
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
54
177
  if (!hasParameter('ssl')) {
178
+ // Create an HTTP server and attach the express app
55
179
  try {
180
+ this.log.debug(`Creating HTTP server...`);
56
181
  this.httpServer = createServer(this.expressApp);
57
182
  }
58
183
  catch (error) {
@@ -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}`);
@@ -85,49 +211,86 @@ export class Frontend extends EventEmitter {
85
211
  this.log.error(`Port ${this.port} is already in use`);
86
212
  break;
87
213
  }
88
- this.initializeError = true;
89
214
  this.emit('server_error', error);
90
215
  return;
91
216
  });
92
217
  }
93
218
  else {
94
219
  let cert;
95
- try {
96
- cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
97
- this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
98
- }
99
- catch (error) {
100
- this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
101
- this.emit('server_error', error);
102
- return;
103
- }
104
220
  let key;
105
- try {
106
- key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
107
- this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`);
108
- }
109
- catch (error) {
110
- this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
111
- this.emit('server_error', error);
112
- return;
113
- }
114
221
  let ca;
115
- try {
116
- ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
117
- this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`);
222
+ let fullChain;
223
+ let pfx;
224
+ let passphrase;
225
+ let httpsServerOptions = {};
226
+ if (existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
227
+ // Load the p12 certificate and the passphrase
228
+ try {
229
+ pfx = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
230
+ this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
231
+ }
232
+ catch (error) {
233
+ this.log.error(`Error reading p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}: ${error}`);
234
+ this.emit('server_error', error);
235
+ return;
236
+ }
237
+ try {
238
+ passphrase = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
239
+ passphrase = passphrase.trim(); // Ensure no extra characters
240
+ this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
241
+ }
242
+ catch (error) {
243
+ this.log.error(`Error reading p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}: ${error}`);
244
+ this.emit('server_error', error);
245
+ return;
246
+ }
247
+ httpsServerOptions = { pfx, passphrase };
118
248
  }
119
- catch (error) {
120
- this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
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.
251
+ try {
252
+ cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
253
+ this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
254
+ }
255
+ catch (error) {
256
+ this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
257
+ this.emit('server_error', error);
258
+ return;
259
+ }
260
+ try {
261
+ key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
262
+ this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`);
263
+ }
264
+ catch (error) {
265
+ this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
266
+ this.emit('server_error', error);
267
+ return;
268
+ }
269
+ try {
270
+ ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
271
+ fullChain = `${cert}\n${ca}`;
272
+ this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`);
273
+ }
274
+ catch (error) {
275
+ this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
276
+ }
277
+ httpsServerOptions = { cert: fullChain ?? cert, key, ca };
121
278
  }
122
- const serverOptions = { cert, key, ca };
279
+ if (hasParameter('mtls')) {
280
+ httpsServerOptions.requestCert = true; // Request client certificate
281
+ httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
282
+ }
283
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
123
284
  try {
124
- this.httpsServer = https.createServer(serverOptions, this.expressApp);
285
+ this.log.debug(`Creating HTTPS server...`);
286
+ this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
125
287
  }
126
288
  catch (error) {
127
289
  this.log.error(`Failed to create HTTPS server: ${error}`);
128
290
  this.emit('server_error', error);
129
291
  return;
130
292
  }
293
+ // Listen on the specified port
131
294
  if (hasParameter('ingress')) {
132
295
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
133
296
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -153,23 +316,23 @@ export class Frontend extends EventEmitter {
153
316
  this.log.error(`Port ${this.port} is already in use`);
154
317
  break;
155
318
  }
156
- this.initializeError = true;
157
319
  this.emit('server_error', error);
158
320
  return;
159
321
  });
160
322
  }
161
- if (this.initializeError)
162
- return;
323
+ // Create a WebSocket server and attach it to the http or https server
163
324
  const wssPort = this.port;
164
325
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
326
+ this.log.debug(`Creating WebSocketServer on host ${CYAN}${wssHost}${db}...`);
165
327
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
166
328
  this.webSocketServer.on('connection', (ws, request) => {
167
329
  const clientIp = request.socket.remoteAddress;
168
- let callbackLogLevel = "notice";
169
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
170
- callbackLogLevel = "info";
171
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
172
- 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 */;
173
336
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
174
337
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
175
338
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -191,6 +354,7 @@ export class Frontend extends EventEmitter {
191
354
  }
192
355
  });
193
356
  ws.on('error', (error) => {
357
+ // istanbul ignore next
194
358
  this.log.error(`WebSocket client error: ${error}`);
195
359
  });
196
360
  });
@@ -204,6 +368,7 @@ export class Frontend extends EventEmitter {
204
368
  this.webSocketServer.on('error', (ws, error) => {
205
369
  this.log.error(`WebSocketServer error: ${error}`);
206
370
  });
371
+ // Subscribe to cli events
207
372
  cliEmitter.removeAllListeners();
208
373
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
209
374
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -214,6 +379,8 @@ export class Frontend extends EventEmitter {
214
379
  cliEmitter.on('cpu', (cpuUsage) => {
215
380
  this.wssSendCpuUpdate(cpuUsage);
216
381
  });
382
+ // Endpoint to validate login code
383
+ // curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
217
384
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
218
385
  const { password } = req.body;
219
386
  this.log.debug('The frontend sent /api/login', password);
@@ -232,23 +399,27 @@ export class Frontend extends EventEmitter {
232
399
  this.log.warn('/api/login error wrong password');
233
400
  res.json({ valid: false });
234
401
  }
402
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
235
403
  }
236
404
  catch (error) {
237
405
  this.log.error('/api/login error getting password');
238
406
  res.json({ valid: false });
239
407
  }
240
408
  });
409
+ // Endpoint to provide health check for docker
241
410
  this.expressApp.get('/health', (req, res) => {
242
411
  this.log.debug('Express received /health');
243
412
  const healthStatus = {
244
- status: 'ok',
245
- uptime: process.uptime(),
246
- 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
247
416
  };
248
417
  res.status(200).json(healthStatus);
249
418
  });
419
+ // Endpoint to provide memory usage details
250
420
  this.expressApp.get('/memory', async (req, res) => {
251
421
  this.log.debug('Express received /memory');
422
+ // Memory usage from process
252
423
  const memoryUsageRaw = process.memoryUsage();
253
424
  const memoryUsage = {
254
425
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -257,10 +428,13 @@ export class Frontend extends EventEmitter {
257
428
  external: this.formatMemoryUsage(memoryUsageRaw.external),
258
429
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
259
430
  };
431
+ // V8 heap statistics
260
432
  const { default: v8 } = await import('node:v8');
261
433
  const heapStatsRaw = v8.getHeapStatistics();
262
434
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
435
+ // Format heapStats
263
436
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
437
+ // Format heapSpaces
264
438
  const heapSpaces = heapSpacesRaw.map((space) => ({
265
439
  ...space,
266
440
  space_size: this.formatMemoryUsage(space.space_size),
@@ -278,19 +452,23 @@ export class Frontend extends EventEmitter {
278
452
  };
279
453
  res.status(200).json(memoryReport);
280
454
  });
455
+ // Endpoint to provide settings
281
456
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
282
457
  this.log.debug('The frontend sent /api/settings');
283
458
  res.json(await this.getApiSettings());
284
459
  });
460
+ // Endpoint to provide plugins
285
461
  this.expressApp.get('/api/plugins', async (req, res) => {
286
462
  this.log.debug('The frontend sent /api/plugins');
287
463
  res.json(this.getBaseRegisteredPlugins());
288
464
  });
465
+ // Endpoint to provide devices
289
466
  this.expressApp.get('/api/devices', async (req, res) => {
290
467
  this.log.debug('The frontend sent /api/devices');
291
468
  const devices = await this.getDevices();
292
469
  res.json(devices);
293
470
  });
471
+ // Endpoint to view the matterbridge log
294
472
  this.expressApp.get('/api/view-mblog', async (req, res) => {
295
473
  this.log.debug('The frontend sent /api/view-mblog');
296
474
  try {
@@ -303,6 +481,7 @@ export class Frontend extends EventEmitter {
303
481
  res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
304
482
  }
305
483
  });
484
+ // Endpoint to view the matter.js log
306
485
  this.expressApp.get('/api/view-mjlog', async (req, res) => {
307
486
  this.log.debug('The frontend sent /api/view-mjlog');
308
487
  try {
@@ -315,6 +494,7 @@ export class Frontend extends EventEmitter {
315
494
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
316
495
  }
317
496
  });
497
+ // Endpoint to view the shelly log
318
498
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
319
499
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
320
500
  try {
@@ -327,9 +507,11 @@ export class Frontend extends EventEmitter {
327
507
  res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
328
508
  }
329
509
  });
510
+ // Endpoint to download the matterbridge log
330
511
  this.expressApp.get('/api/download-mblog', async (req, res) => {
331
512
  this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
332
513
  try {
514
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
333
515
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), fs.constants.F_OK);
334
516
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), 'utf8');
335
517
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), data, 'utf-8');
@@ -340,15 +522,18 @@ export class Frontend extends EventEmitter {
340
522
  }
341
523
  res.type('text/plain');
342
524
  res.download(path.join(os.tmpdir(), this.matterbridge.matterbridgeLoggerFile), 'matterbridge.log', (error) => {
525
+ /* istanbul ignore if */
343
526
  if (error) {
344
527
  this.log.error(`Error downloading log file ${this.matterbridge.matterbridgeLoggerFile}: ${error instanceof Error ? error.message : error}`);
345
528
  res.status(500).send('Error downloading the matterbridge log file');
346
529
  }
347
530
  });
348
531
  });
532
+ // Endpoint to download the matter log
349
533
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
350
534
  this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile)}`);
351
535
  try {
536
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
352
537
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
353
538
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
354
539
  await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
@@ -359,15 +544,18 @@ export class Frontend extends EventEmitter {
359
544
  }
360
545
  res.type('text/plain');
361
546
  res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
547
+ /* istanbul ignore if */
362
548
  if (error) {
363
549
  this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
364
550
  res.status(500).send('Error downloading the matter log file');
365
551
  }
366
552
  });
367
553
  });
554
+ // Endpoint to download the shelly log
368
555
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
369
556
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
370
557
  try {
558
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
371
559
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
372
560
  const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
373
561
  await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
@@ -378,74 +566,90 @@ export class Frontend extends EventEmitter {
378
566
  }
379
567
  res.type('text/plain');
380
568
  res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
569
+ /* istanbul ignore if */
381
570
  if (error) {
382
571
  this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
383
572
  res.status(500).send('Error downloading Shelly system log file');
384
573
  }
385
574
  });
386
575
  });
576
+ // Endpoint to download the matterbridge storage directory
387
577
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
388
578
  this.log.debug('The frontend sent /api/download-mbstorage');
389
579
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
390
580
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
581
+ /* istanbul ignore if */
391
582
  if (error) {
392
583
  this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
393
584
  res.status(500).send('Error downloading the matterbridge storage file');
394
585
  }
395
586
  });
396
587
  });
588
+ // Endpoint to download the matter storage file
397
589
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
398
590
  this.log.debug('The frontend sent /api/download-mjstorage');
399
591
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
400
592
  res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => {
593
+ /* istanbul ignore if */
401
594
  if (error) {
402
595
  this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`);
403
596
  res.status(500).send('Error downloading the matter storage zip file');
404
597
  }
405
598
  });
406
599
  });
600
+ // Endpoint to download the matterbridge plugin directory
407
601
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
408
602
  this.log.debug('The frontend sent /api/download-pluginstorage');
409
603
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
410
604
  res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
605
+ /* istanbul ignore if */
411
606
  if (error) {
412
607
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
413
608
  res.status(500).send('Error downloading the matterbridge plugin storage file');
414
609
  }
415
610
  });
416
611
  });
612
+ // Endpoint to download the matterbridge plugin config files
417
613
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
418
614
  this.log.debug('The frontend sent /api/download-pluginconfig');
419
615
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
420
616
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
617
+ /* istanbul ignore if */
421
618
  if (error) {
422
619
  this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
423
620
  res.status(500).send('Error downloading the matterbridge plugin config file');
424
621
  }
425
622
  });
426
623
  });
624
+ // Endpoint to download the matterbridge backup (created with the backup command)
427
625
  this.expressApp.get('/api/download-backup', async (req, res) => {
428
626
  this.log.debug('The frontend sent /api/download-backup');
429
627
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
628
+ /* istanbul ignore if */
430
629
  if (error) {
431
630
  this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
432
631
  res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
433
632
  }
434
633
  });
435
634
  });
635
+ // Endpoint to upload a package
436
636
  this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
437
637
  const { filename } = req.body;
438
638
  const file = req.file;
639
+ /* istanbul ignore if */
439
640
  if (!file || !filename) {
440
641
  this.log.error(`uploadpackage: invalid request: file and filename are required`);
441
642
  res.status(400).send('Invalid request: file and filename are required');
442
643
  return;
443
644
  }
444
645
  this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
646
+ // Define the path where the plugin file will be saved
445
647
  const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
446
648
  try {
649
+ // Move the uploaded file to the specified path
447
650
  await fs.rename(file.path, filePath);
448
651
  this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
652
+ // Install the plugin package
449
653
  if (filename.endsWith('.tgz')) {
450
654
  const { spawnCommand } = await import('./utils/spawn.js');
451
655
  await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
@@ -465,6 +669,7 @@ export class Frontend extends EventEmitter {
465
669
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
466
670
  }
467
671
  });
672
+ // Fallback for routing (must be the last route)
468
673
  this.expressApp.use((req, res) => {
469
674
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
470
675
  res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -473,13 +678,16 @@ export class Frontend extends EventEmitter {
473
678
  }
474
679
  async stop() {
475
680
  this.log.debug('Stopping the frontend...');
681
+ // Remove listeners from the express app
476
682
  if (this.expressApp) {
477
683
  this.expressApp.removeAllListeners();
478
684
  this.expressApp = undefined;
479
685
  this.log.debug('Frontend app closed successfully');
480
686
  }
687
+ // Close the WebSocket server
481
688
  if (this.webSocketServer) {
482
689
  this.log.debug('Closing WebSocket server...');
690
+ // Close all active connections
483
691
  this.webSocketServer.clients.forEach((client) => {
484
692
  if (client.readyState === WebSocket.OPEN) {
485
693
  client.close();
@@ -492,6 +700,7 @@ export class Frontend extends EventEmitter {
492
700
  }
493
701
  else {
494
702
  this.log.debug('WebSocket server closed successfully');
703
+ this.emit('websocket_server_stopped');
495
704
  }
496
705
  resolve();
497
706
  });
@@ -499,6 +708,7 @@ export class Frontend extends EventEmitter {
499
708
  this.webSocketServer.removeAllListeners();
500
709
  this.webSocketServer = undefined;
501
710
  }
711
+ // Close the http server
502
712
  if (this.httpServer) {
503
713
  this.log.debug('Closing http server...');
504
714
  await withTimeout(new Promise((resolve) => {
@@ -508,6 +718,7 @@ export class Frontend extends EventEmitter {
508
718
  }
509
719
  else {
510
720
  this.log.debug('Http server closed successfully');
721
+ this.emit('server_stopped');
511
722
  }
512
723
  resolve();
513
724
  });
@@ -516,6 +727,7 @@ export class Frontend extends EventEmitter {
516
727
  this.httpServer = undefined;
517
728
  this.log.debug('Frontend http server closed successfully');
518
729
  }
730
+ // Close the https server
519
731
  if (this.httpsServer) {
520
732
  this.log.debug('Closing https server...');
521
733
  await withTimeout(new Promise((resolve) => {
@@ -525,6 +737,7 @@ export class Frontend extends EventEmitter {
525
737
  }
526
738
  else {
527
739
  this.log.debug('Https server closed successfully');
740
+ this.emit('server_stopped');
528
741
  }
529
742
  resolve();
530
743
  });
@@ -535,6 +748,7 @@ export class Frontend extends EventEmitter {
535
748
  }
536
749
  this.log.debug('Frontend stopped successfully');
537
750
  }
751
+ // Function to format bytes to KB, MB, or GB
538
752
  formatMemoryUsage = (bytes) => {
539
753
  if (bytes >= 1024 ** 3) {
540
754
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -546,6 +760,7 @@ export class Frontend extends EventEmitter {
546
760
  return `${(bytes / 1024).toFixed(2)} KB`;
547
761
  }
548
762
  };
763
+ // Function to format system uptime with only the most significant unit
549
764
  formatOsUpTime = (seconds) => {
550
765
  if (seconds >= 86400) {
551
766
  const days = Math.floor(seconds / 86400);
@@ -561,7 +776,13 @@ export class Frontend extends EventEmitter {
561
776
  }
562
777
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
563
778
  };
779
+ /**
780
+ * Retrieves the api settings data.
781
+ *
782
+ * @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
783
+ */
564
784
  async getApiSettings() {
785
+ // Update the system information
565
786
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
566
787
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
567
788
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -570,6 +791,7 @@ export class Frontend extends EventEmitter {
570
791
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
571
792
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
572
793
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
794
+ // Update the matterbridge information
573
795
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
574
796
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
575
797
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
@@ -581,6 +803,7 @@ export class Frontend extends EventEmitter {
581
803
  this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
582
804
  this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
583
805
  this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
806
+ // Update the matterbridge information in bridge mode
584
807
  if (this.matterbridge.bridgeMode === 'bridge' && this.matterbridge.serverNode) {
585
808
  this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.serverNode.state.commissioning.commissioned;
586
809
  this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : this.matterbridge.serverNode.state.commissioning.pairingCodes.qrPairingCode;
@@ -590,6 +813,12 @@ export class Frontend extends EventEmitter {
590
813
  }
591
814
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
592
815
  }
816
+ /**
817
+ * Retrieves the reachable attribute.
818
+ *
819
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
820
+ * @returns {boolean} The reachable attribute.
821
+ */
593
822
  getReachability(device) {
594
823
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
595
824
  return false;
@@ -601,6 +830,12 @@ export class Frontend extends EventEmitter {
601
830
  return true;
602
831
  return false;
603
832
  }
833
+ /**
834
+ * Retrieves the power source attribute.
835
+ *
836
+ * @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
837
+ * @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
838
+ */
604
839
  getPowerSource(endpoint) {
605
840
  if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
606
841
  return undefined;
@@ -616,13 +851,21 @@ export class Frontend extends EventEmitter {
616
851
  }
617
852
  return;
618
853
  };
854
+ // Root endpoint
619
855
  if (endpoint.hasClusterServer(PowerSource.Cluster.id))
620
856
  return powerSource(endpoint);
857
+ // Child endpoints
621
858
  for (const child of endpoint.getChildEndpoints()) {
622
859
  if (child.hasClusterServer(PowerSource.Cluster.id))
623
860
  return powerSource(child);
624
861
  }
625
862
  }
863
+ /**
864
+ * Retrieves the commissioned status, matter pairing codes, fabrics and sessions from a given device in server mode.
865
+ *
866
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the data from.
867
+ * @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
868
+ */
626
869
  getMatterDataFromDevice(device) {
627
870
  if (device.mode === 'server' && device.serverNode) {
628
871
  return {
@@ -634,6 +877,13 @@ export class Frontend extends EventEmitter {
634
877
  };
635
878
  }
636
879
  }
880
+ /**
881
+ * Retrieves the cluster text description from a given device.
882
+ * The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
883
+ *
884
+ * @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
885
+ * @returns {string} The attributes description of the cluster servers in the device.
886
+ */
637
887
  getClusterTextFromDevice(device) {
638
888
  if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
639
889
  return '';
@@ -658,6 +908,7 @@ export class Frontend extends EventEmitter {
658
908
  let attributes = '';
659
909
  let supportedModes = [];
660
910
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
911
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
661
912
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
662
913
  return;
663
914
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -747,11 +998,17 @@ export class Frontend extends EventEmitter {
747
998
  if (clusterName === 'userLabel' && attributeName === 'labelList')
748
999
  attributes += `${getUserLabel(device)} `;
749
1000
  });
1001
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
750
1002
  return attributes.trimStart().trimEnd();
751
1003
  }
1004
+ /**
1005
+ * Retrieves the base registered plugins sanitized for res.json().
1006
+ *
1007
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1008
+ */
752
1009
  getBaseRegisteredPlugins() {
753
1010
  if (this.matterbridge.hasCleanupStarted)
754
- return [];
1011
+ return []; // Skip if cleanup has started
755
1012
  const baseRegisteredPlugins = [];
756
1013
  for (const plugin of this.matterbridge.plugins) {
757
1014
  baseRegisteredPlugins.push({
@@ -780,6 +1037,7 @@ export class Frontend extends EventEmitter {
780
1037
  schemaJson: plugin.schemaJson,
781
1038
  hasWhiteList: plugin.configJson?.whiteList !== undefined,
782
1039
  hasBlackList: plugin.configJson?.blackList !== undefined,
1040
+ // Childbridge mode specific data
783
1041
  paired: plugin.serverNode?.state.commissioning.commissioned,
784
1042
  qrPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.qrPairingCode,
785
1043
  manualPairingCode: this.matterbridge.matterbridgeInformation.matterbridgeEndAdvertise ? undefined : plugin.serverNode?.state.commissioning.pairingCodes.manualPairingCode,
@@ -789,13 +1047,21 @@ export class Frontend extends EventEmitter {
789
1047
  }
790
1048
  return baseRegisteredPlugins;
791
1049
  }
1050
+ /**
1051
+ * Retrieves the devices from Matterbridge.
1052
+ *
1053
+ * @param {string} [pluginName] - The name of the plugin to filter devices by.
1054
+ * @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
1055
+ */
792
1056
  async getDevices(pluginName) {
793
1057
  if (this.matterbridge.hasCleanupStarted)
794
- return [];
1058
+ return []; // Skip if cleanup has started
795
1059
  const devices = [];
796
1060
  for (const device of this.matterbridge.devices.array()) {
1061
+ // Filter by pluginName if provided
797
1062
  if (pluginName && pluginName !== device.plugin)
798
1063
  continue;
1064
+ // Check if the device has the required properties
799
1065
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
800
1066
  continue;
801
1067
  devices.push({
@@ -815,22 +1081,37 @@ export class Frontend extends EventEmitter {
815
1081
  }
816
1082
  return devices;
817
1083
  }
1084
+ /**
1085
+ * Retrieves the clusters from a given plugin and endpoint number.
1086
+ *
1087
+ * Response for /api/clusters
1088
+ *
1089
+ * @param {string} pluginName - The name of the plugin.
1090
+ * @param {number} endpointNumber - The endpoint number.
1091
+ * @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
1092
+ */
818
1093
  getClusters(pluginName, endpointNumber) {
819
1094
  const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
820
1095
  if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
821
1096
  this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
822
1097
  return;
823
1098
  }
1099
+ // this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
1100
+ // Get the device types from the main endpoint
824
1101
  const deviceTypes = [];
825
1102
  const clusters = [];
826
1103
  endpoint.state.descriptor.deviceTypeList.forEach((d) => {
827
1104
  deviceTypes.push(d.deviceType);
828
1105
  });
1106
+ // Get the clusters from the main endpoint
829
1107
  endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
830
1108
  if (typeof attributeValue === 'undefined' || attributeValue === undefined)
831
1109
  return;
832
1110
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
833
1111
  return;
1112
+ // console.log(
1113
+ // `${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}`,
1114
+ // );
834
1115
  clusters.push({
835
1116
  endpoint: endpoint.number.toString(),
836
1117
  id: 'main',
@@ -843,12 +1124,18 @@ export class Frontend extends EventEmitter {
843
1124
  attributeLocalValue: attributeValue,
844
1125
  });
845
1126
  });
1127
+ // Get the child endpoints
846
1128
  const childEndpoints = endpoint.getChildEndpoints();
1129
+ // if (childEndpoints.length === 0) {
1130
+ // this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
1131
+ // }
847
1132
  childEndpoints.forEach((childEndpoint) => {
848
1133
  if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
849
1134
  this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
850
1135
  return;
851
1136
  }
1137
+ // this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
1138
+ // Get the device types of the child endpoint
852
1139
  const deviceTypes = [];
853
1140
  childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
854
1141
  deviceTypes.push(d.deviceType);
@@ -858,9 +1145,12 @@ export class Frontend extends EventEmitter {
858
1145
  return;
859
1146
  if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
860
1147
  return;
1148
+ // console.log(
1149
+ // `${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}`,
1150
+ // );
861
1151
  clusters.push({
862
1152
  endpoint: childEndpoint.number.toString(),
863
- id: childEndpoint.maybeId ?? 'null',
1153
+ id: childEndpoint.maybeId ?? 'null', // Never happens
864
1154
  deviceTypes,
865
1155
  clusterName: capitalizeFirstLetter(clusterName),
866
1156
  clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
@@ -873,6 +1163,13 @@ export class Frontend extends EventEmitter {
873
1163
  });
874
1164
  return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
875
1165
  }
1166
+ /**
1167
+ * Handles incoming websocket messages for the Matterbridge frontend.
1168
+ *
1169
+ * @param {WebSocket} client - The websocket client that sent the message.
1170
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1171
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1172
+ */
876
1173
  async wsMessageHandler(client, message) {
877
1174
  let data;
878
1175
  try {
@@ -919,32 +1216,42 @@ export class Frontend extends EventEmitter {
919
1216
  this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
920
1217
  const packageName = data.params.packageName.replace(/@.*$/, '');
921
1218
  if (data.params.restart === false && packageName !== 'matterbridge') {
1219
+ // The install comes from InstallPlugins
922
1220
  this.matterbridge.plugins
923
1221
  .add(packageName)
924
1222
  .then((plugin) => {
925
1223
  if (plugin) {
1224
+ // The plugin is not registered
926
1225
  this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
927
1226
  this.matterbridge.plugins
928
1227
  .load(plugin, true, 'The plugin has been added', true)
1228
+ // eslint-disable-next-line promise/no-nesting
929
1229
  .then(() => {
930
1230
  this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
931
1231
  this.wssSendRefreshRequired('plugins');
932
1232
  return;
933
1233
  })
1234
+ // eslint-disable-next-line promise/no-nesting
934
1235
  .catch((_error) => {
1236
+ //
935
1237
  });
936
1238
  }
937
1239
  else {
1240
+ // The plugin is already registered
938
1241
  this.wssSendSnackbarMessage(`Restart required`, 0);
939
1242
  this.wssSendRefreshRequired('plugins');
940
1243
  this.wssSendRestartRequired();
941
1244
  }
942
1245
  return;
943
1246
  })
1247
+ // eslint-disable-next-line promise/no-nesting
944
1248
  .catch((_error) => {
1249
+ //
945
1250
  });
946
1251
  }
947
1252
  else {
1253
+ // The package is matterbridge
1254
+ // istanbul ignore next if
948
1255
  if (this.matterbridge.restartMode !== '') {
949
1256
  this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
950
1257
  this.matterbridge.shutdownProcess();
@@ -967,6 +1274,7 @@ export class Frontend extends EventEmitter {
967
1274
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
968
1275
  return;
969
1276
  }
1277
+ // The package is a plugin
970
1278
  const plugin = this.matterbridge.plugins.get(data.params.packageName);
971
1279
  if (plugin) {
972
1280
  await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
@@ -975,6 +1283,7 @@ export class Frontend extends EventEmitter {
975
1283
  this.wssSendRefreshRequired('plugins');
976
1284
  this.wssSendRefreshRequired('devices');
977
1285
  }
1286
+ // Uninstall the package
978
1287
  this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
979
1288
  const { spawnCommand } = await import('./utils/spawn.js');
980
1289
  spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
@@ -1015,6 +1324,7 @@ export class Frontend extends EventEmitter {
1015
1324
  return;
1016
1325
  })
1017
1326
  .catch((_error) => {
1327
+ //
1018
1328
  });
1019
1329
  }
1020
1330
  else {
@@ -1061,6 +1371,7 @@ export class Frontend extends EventEmitter {
1061
1371
  return;
1062
1372
  })
1063
1373
  .catch((_error) => {
1374
+ //
1064
1375
  });
1065
1376
  }
1066
1377
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1171,6 +1482,8 @@ export class Frontend extends EventEmitter {
1171
1482
  else if (data.method === '/api/advertise') {
1172
1483
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
1173
1484
  this.matterbridge.matterbridgeInformation.matterbridgeAdvertise = true;
1485
+ // this.matterbridge.matterbridgeQrPairingCode = pairingCodes?.qrPairingCode;
1486
+ // this.matterbridge.matterbridgeManualPairingCode = pairingCodes?.manualPairingCode;
1174
1487
  this.wssSendRefreshRequired('matterbridgeAdvertise');
1175
1488
  this.wssSendSnackbarMessage(`Started fabrics share`, 0);
1176
1489
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: pairingCodes, success: true }));
@@ -1293,22 +1606,22 @@ export class Frontend extends EventEmitter {
1293
1606
  if (isValidString(data.params.value, 4)) {
1294
1607
  this.log.debug('Matterbridge logger level:', data.params.value);
1295
1608
  if (data.params.value === 'Debug') {
1296
- await this.matterbridge.setLogLevel("debug");
1609
+ await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
1297
1610
  }
1298
1611
  else if (data.params.value === 'Info') {
1299
- await this.matterbridge.setLogLevel("info");
1612
+ await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
1300
1613
  }
1301
1614
  else if (data.params.value === 'Notice') {
1302
- await this.matterbridge.setLogLevel("notice");
1615
+ await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
1303
1616
  }
1304
1617
  else if (data.params.value === 'Warn') {
1305
- await this.matterbridge.setLogLevel("warn");
1618
+ await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
1306
1619
  }
1307
1620
  else if (data.params.value === 'Error') {
1308
- await this.matterbridge.setLogLevel("error");
1621
+ await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
1309
1622
  }
1310
1623
  else if (data.params.value === 'Fatal') {
1311
- await this.matterbridge.setLogLevel("fatal");
1624
+ await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
1312
1625
  }
1313
1626
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
1314
1627
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
@@ -1319,6 +1632,7 @@ export class Frontend extends EventEmitter {
1319
1632
  this.log.debug('Matterbridge file log:', data.params.value);
1320
1633
  this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
1321
1634
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
1635
+ // Create the file logger for matterbridge
1322
1636
  if (data.params.value)
1323
1637
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbridgeLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
1324
1638
  else
@@ -1365,6 +1679,7 @@ export class Frontend extends EventEmitter {
1365
1679
  });
1366
1680
  }
1367
1681
  catch (error) {
1682
+ /* istanbul ignore next */
1368
1683
  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}`);
1369
1684
  }
1370
1685
  }
@@ -1373,6 +1688,7 @@ export class Frontend extends EventEmitter {
1373
1688
  Logger.removeLogger('matterfilelogger');
1374
1689
  }
1375
1690
  catch (error) {
1691
+ /* istanbul ignore next */
1376
1692
  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}`);
1377
1693
  }
1378
1694
  }
@@ -1485,15 +1801,19 @@ export class Frontend extends EventEmitter {
1485
1801
  return;
1486
1802
  }
1487
1803
  const config = plugin.configJson;
1804
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1488
1805
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1806
+ // this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1489
1807
  if (select === 'serial')
1490
1808
  this.log.info(`Selected device serial ${data.params.serial}`);
1491
1809
  if (select === 'name')
1492
1810
  this.log.info(`Selected device name ${data.params.name}`);
1493
1811
  if (config && select && (select === 'serial' || select === 'name')) {
1812
+ // Remove postfix from the serial if it exists
1494
1813
  if (config.postfix) {
1495
1814
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1496
1815
  }
1816
+ // Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
1497
1817
  if (isValidArray(config.whiteList, 1)) {
1498
1818
  if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
1499
1819
  config.whiteList.push(data.params.serial);
@@ -1502,6 +1822,7 @@ export class Frontend extends EventEmitter {
1502
1822
  config.whiteList.push(data.params.name);
1503
1823
  }
1504
1824
  }
1825
+ // Remove the serial from the blackList if the blackList exists and the serial or name is in it
1505
1826
  if (isValidArray(config.blackList, 1)) {
1506
1827
  if (select === 'serial' && config.blackList.includes(data.params.serial)) {
1507
1828
  config.blackList = config.blackList.filter((item) => item !== data.params.serial);
@@ -1532,7 +1853,9 @@ export class Frontend extends EventEmitter {
1532
1853
  return;
1533
1854
  }
1534
1855
  const config = plugin.configJson;
1856
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1535
1857
  const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
1858
+ // this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
1536
1859
  if (select === 'serial')
1537
1860
  this.log.info(`Unselected device serial ${data.params.serial}`);
1538
1861
  if (select === 'name')
@@ -1541,6 +1864,7 @@ export class Frontend extends EventEmitter {
1541
1864
  if (config.postfix) {
1542
1865
  data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1543
1866
  }
1867
+ // Remove the serial from the whiteList if the whiteList exists and the serial is in it
1544
1868
  if (isValidArray(config.whiteList, 1)) {
1545
1869
  if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
1546
1870
  config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
@@ -1549,6 +1873,7 @@ export class Frontend extends EventEmitter {
1549
1873
  config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
1550
1874
  }
1551
1875
  }
1876
+ // Add the serial to the blackList
1552
1877
  if (isValidArray(config.blackList)) {
1553
1878
  if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
1554
1879
  config.blackList.push(data.params.serial);
@@ -1582,114 +1907,230 @@ export class Frontend extends EventEmitter {
1582
1907
  this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1583
1908
  }
1584
1909
  }
1910
+ /**
1911
+ * Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1912
+ *
1913
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1914
+ * @param {string} time - The time string of the message
1915
+ * @param {string} name - The logger name of the message
1916
+ * @param {string} message - The content of the message.
1917
+ *
1918
+ * @remarks
1919
+ * The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
1920
+ * It also replaces all occurrences of \" with " and angle-brackets with &lt; and &gt;.
1921
+ * The function sends the message to all connected clients.
1922
+ */
1585
1923
  wssSendMessage(level, time, name, message) {
1586
1924
  if (!level || !time || !name || !message)
1587
1925
  return;
1926
+ // Remove ANSI escape codes from the message
1927
+ // eslint-disable-next-line no-control-regex
1588
1928
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1929
+ // Remove leading asterisks from the message
1589
1930
  message = message.replace(/^\*+/, '');
1931
+ // Replace all occurrences of \t and \n
1590
1932
  message = message.replace(/[\t\n]/g, '');
1933
+ // Remove non-printable characters
1934
+ // eslint-disable-next-line no-control-regex
1591
1935
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1936
+ // Replace all occurrences of \" with "
1592
1937
  message = message.replace(/\\"/g, '"');
1938
+ // Replace all occurrences of angle-brackets with &lt; and &gt;"
1593
1939
  message = message.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1940
+ // Define the maximum allowed length for continuous characters without a space
1594
1941
  const maxContinuousLength = 100;
1595
1942
  const keepStartLength = 20;
1596
1943
  const keepEndLength = 20;
1944
+ // Split the message into words
1597
1945
  message = message
1598
1946
  .split(' ')
1599
1947
  .map((word) => {
1948
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1600
1949
  if (word.length > maxContinuousLength) {
1601
1950
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1602
1951
  }
1603
1952
  return word;
1604
1953
  })
1605
1954
  .join(' ');
1955
+ // Send the message to all connected clients
1606
1956
  this.webSocketServer?.clients.forEach((client) => {
1607
1957
  if (client.readyState === WebSocket.OPEN) {
1608
1958
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1609
1959
  }
1610
1960
  });
1611
1961
  }
1962
+ /**
1963
+ * Sends a need to refresh WebSocket message to all connected clients.
1964
+ *
1965
+ * @param {string} changed - The changed value. If null, the whole page will be refreshed.
1966
+ * possible values:
1967
+ * - 'matterbridgeLatestVersion'
1968
+ * - 'matterbridgeAdvertise'
1969
+ * - 'online'
1970
+ * - 'offline'
1971
+ * - 'reachability'
1972
+ * - 'settings'
1973
+ * - 'plugins'
1974
+ * - 'pluginsRestart'
1975
+ * - 'devices'
1976
+ * - 'fabrics'
1977
+ * - 'sessions'
1978
+ */
1612
1979
  wssSendRefreshRequired(changed = null) {
1613
1980
  this.log.debug('Sending a refresh required message to all connected clients');
1981
+ // Send the message to all connected clients
1614
1982
  this.webSocketServer?.clients.forEach((client) => {
1615
1983
  if (client.readyState === WebSocket.OPEN) {
1616
1984
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1617
1985
  }
1618
1986
  });
1619
1987
  }
1988
+ /**
1989
+ * Sends a need to restart WebSocket message to all connected clients.
1990
+ *
1991
+ * @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients.
1992
+ */
1620
1993
  wssSendRestartRequired(snackbar = true) {
1621
1994
  this.log.debug('Sending a restart required message to all connected clients');
1622
1995
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1623
1996
  if (snackbar === true)
1624
1997
  this.wssSendSnackbarMessage(`Restart required`, 0);
1998
+ // Send the message to all connected clients
1625
1999
  this.webSocketServer?.clients.forEach((client) => {
1626
2000
  if (client.readyState === WebSocket.OPEN) {
1627
2001
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1628
2002
  }
1629
2003
  });
1630
2004
  }
2005
+ /**
2006
+ * Sends a need to update WebSocket message to all connected clients.
2007
+ *
2008
+ */
1631
2009
  wssSendUpdateRequired() {
1632
2010
  this.log.debug('Sending an update required message to all connected clients');
1633
2011
  this.matterbridge.matterbridgeInformation.updateRequired = true;
2012
+ // Send the message to all connected clients
1634
2013
  this.webSocketServer?.clients.forEach((client) => {
1635
2014
  if (client.readyState === WebSocket.OPEN) {
1636
2015
  client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
1637
2016
  }
1638
2017
  });
1639
2018
  }
2019
+ /**
2020
+ * Sends a cpu update message to all connected clients.
2021
+ *
2022
+ * @param {number} cpuUsage - The CPU usage percentage to send.
2023
+ */
1640
2024
  wssSendCpuUpdate(cpuUsage) {
1641
2025
  if (hasParameter('debug'))
1642
2026
  this.log.debug('Sending a cpu update message to all connected clients');
2027
+ // Send the message to all connected clients
1643
2028
  this.webSocketServer?.clients.forEach((client) => {
1644
2029
  if (client.readyState === WebSocket.OPEN) {
1645
2030
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1646
2031
  }
1647
2032
  });
1648
2033
  }
2034
+ /**
2035
+ * Sends a memory update message to all connected clients.
2036
+ *
2037
+ * @param {string} totalMemory - The total memory in bytes.
2038
+ * @param {string} freeMemory - The free memory in bytes.
2039
+ * @param {string} rss - The resident set size in bytes.
2040
+ * @param {string} heapTotal - The total heap memory in bytes.
2041
+ * @param {string} heapUsed - The used heap memory in bytes.
2042
+ * @param {string} external - The external memory in bytes.
2043
+ * @param {string} arrayBuffers - The array buffers memory in bytes.
2044
+ */
1649
2045
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1650
2046
  if (hasParameter('debug'))
1651
2047
  this.log.debug('Sending a memory update message to all connected clients');
2048
+ // Send the message to all connected clients
1652
2049
  this.webSocketServer?.clients.forEach((client) => {
1653
2050
  if (client.readyState === WebSocket.OPEN) {
1654
2051
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1655
2052
  }
1656
2053
  });
1657
2054
  }
2055
+ /**
2056
+ * Sends an uptime update message to all connected clients.
2057
+ *
2058
+ * @param {string} systemUptime - The system uptime in a human-readable format.
2059
+ * @param {string} processUptime - The process uptime in a human-readable format.
2060
+ */
1658
2061
  wssSendUptimeUpdate(systemUptime, processUptime) {
1659
2062
  if (hasParameter('debug'))
1660
2063
  this.log.debug('Sending a uptime update message to all connected clients');
2064
+ // Send the message to all connected clients
1661
2065
  this.webSocketServer?.clients.forEach((client) => {
1662
2066
  if (client.readyState === WebSocket.OPEN) {
1663
2067
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1664
2068
  }
1665
2069
  });
1666
2070
  }
2071
+ /**
2072
+ * Sends an open snackbar message to all connected clients.
2073
+ *
2074
+ * @param {string} message - The message to send.
2075
+ * @param {number} timeout - The timeout in seconds for the snackbar message.
2076
+ * @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
2077
+ */
1667
2078
  wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
1668
2079
  this.log.debug('Sending a snackbar message to all connected clients');
2080
+ // Send the message to all connected clients
1669
2081
  this.webSocketServer?.clients.forEach((client) => {
1670
2082
  if (client.readyState === WebSocket.OPEN) {
1671
2083
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
1672
2084
  }
1673
2085
  });
1674
2086
  }
2087
+ /**
2088
+ * Sends a close snackbar message to all connected clients.
2089
+ *
2090
+ * @param {string} message - The message to send.
2091
+ */
1675
2092
  wssSendCloseSnackbarMessage(message) {
1676
2093
  this.log.debug('Sending a close snackbar message to all connected clients');
2094
+ // Send the message to all connected clients
1677
2095
  this.webSocketServer?.clients.forEach((client) => {
1678
2096
  if (client.readyState === WebSocket.OPEN) {
1679
2097
  client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
1680
2098
  }
1681
2099
  });
1682
2100
  }
2101
+ /**
2102
+ * Sends an attribute update message to all connected WebSocket clients.
2103
+ *
2104
+ * @param {string | undefined} plugin - The name of the plugin.
2105
+ * @param {string | undefined} serialNumber - The serial number of the device.
2106
+ * @param {string | undefined} uniqueId - The unique identifier of the device.
2107
+ * @param {string} cluster - The cluster name where the attribute belongs.
2108
+ * @param {string} attribute - The name of the attribute that changed.
2109
+ * @param {number | string | boolean} value - The new value of the attribute.
2110
+ *
2111
+ * @remarks
2112
+ * This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
2113
+ * with the updated attribute information.
2114
+ */
1683
2115
  wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
1684
2116
  this.log.debug('Sending an attribute update message to all connected clients');
2117
+ // Send the message to all connected clients
1685
2118
  this.webSocketServer?.clients.forEach((client) => {
1686
2119
  if (client.readyState === WebSocket.OPEN) {
1687
2120
  client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
1688
2121
  }
1689
2122
  });
1690
2123
  }
2124
+ /**
2125
+ * Sends a message to all connected clients.
2126
+ *
2127
+ * @param {number} id - The message id.
2128
+ * @param {string} method - The message method.
2129
+ * @param {Record<string, string | number | boolean>} params - The message parameters.
2130
+ */
1691
2131
  wssBroadcastMessage(id, method, params) {
1692
2132
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
2133
+ // Send the message to all connected clients
1693
2134
  this.webSocketServer?.clients.forEach((client) => {
1694
2135
  if (client.readyState === WebSocket.OPEN) {
1695
2136
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1697,3 +2138,4 @@ export class Frontend extends EventEmitter {
1697
2138
  });
1698
2139
  }
1699
2140
  }
2141
+ //# sourceMappingURL=frontend.js.map