matterbridge 2.2.0-dev.9 → 2.2.1

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 (155) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +2 -2
  3. package/dist/cli.d.ts +29 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +37 -2
  6. package/dist/cli.js.map +1 -0
  7. package/dist/cluster/export.d.ts +2 -0
  8. package/dist/cluster/export.d.ts.map +1 -0
  9. package/dist/cluster/export.js +2 -0
  10. package/dist/cluster/export.js.map +1 -0
  11. package/dist/defaultConfigSchema.d.ts +27 -0
  12. package/dist/defaultConfigSchema.d.ts.map +1 -0
  13. package/dist/defaultConfigSchema.js +23 -0
  14. package/dist/defaultConfigSchema.js.map +1 -0
  15. package/dist/deviceManager.d.ts +114 -0
  16. package/dist/deviceManager.d.ts.map +1 -0
  17. package/dist/deviceManager.js +94 -1
  18. package/dist/deviceManager.js.map +1 -0
  19. package/dist/frontend.d.ts +178 -0
  20. package/dist/frontend.d.ts.map +1 -0
  21. package/dist/frontend.js +356 -27
  22. package/dist/frontend.js.map +1 -0
  23. package/dist/index.d.ts +35 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +28 -1
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger/export.d.ts +2 -0
  28. package/dist/logger/export.d.ts.map +1 -0
  29. package/dist/logger/export.js +1 -0
  30. package/dist/logger/export.js.map +1 -0
  31. package/dist/matter/behaviors.d.ts +2 -0
  32. package/dist/matter/behaviors.d.ts.map +1 -0
  33. package/dist/matter/behaviors.js +2 -0
  34. package/dist/matter/behaviors.js.map +1 -0
  35. package/dist/matter/clusters.d.ts +2 -0
  36. package/dist/matter/clusters.d.ts.map +1 -0
  37. package/dist/matter/clusters.js +2 -0
  38. package/dist/matter/clusters.js.map +1 -0
  39. package/dist/matter/devices.d.ts +2 -0
  40. package/dist/matter/devices.d.ts.map +1 -0
  41. package/dist/matter/devices.js +2 -0
  42. package/dist/matter/devices.js.map +1 -0
  43. package/dist/matter/endpoints.d.ts +2 -0
  44. package/dist/matter/endpoints.d.ts.map +1 -0
  45. package/dist/matter/endpoints.js +2 -0
  46. package/dist/matter/endpoints.js.map +1 -0
  47. package/dist/matter/export.d.ts +5 -0
  48. package/dist/matter/export.d.ts.map +1 -0
  49. package/dist/matter/export.js +2 -0
  50. package/dist/matter/export.js.map +1 -0
  51. package/dist/matter/types.d.ts +3 -0
  52. package/dist/matter/types.d.ts.map +1 -0
  53. package/dist/matter/types.js +2 -0
  54. package/dist/matter/types.js.map +1 -0
  55. package/dist/matterbridge.d.ts +411 -0
  56. package/dist/matterbridge.d.ts.map +1 -0
  57. package/dist/matterbridge.js +720 -48
  58. package/dist/matterbridge.js.map +1 -0
  59. package/dist/matterbridgeAccessoryPlatform.d.ts +39 -0
  60. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  61. package/dist/matterbridgeAccessoryPlatform.js +33 -0
  62. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  63. package/dist/matterbridgeBehaviors.d.ts +1056 -0
  64. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  65. package/dist/matterbridgeBehaviors.js +32 -1
  66. package/dist/matterbridgeBehaviors.js.map +1 -0
  67. package/dist/matterbridgeDeviceTypes.d.ts +177 -0
  68. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  69. package/dist/matterbridgeDeviceTypes.js +112 -11
  70. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  71. package/dist/matterbridgeDynamicPlatform.d.ts +39 -0
  72. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  73. package/dist/matterbridgeDynamicPlatform.js +33 -0
  74. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  75. package/dist/matterbridgeEndpoint.d.ts +835 -0
  76. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  77. package/dist/matterbridgeEndpoint.js +692 -6
  78. package/dist/matterbridgeEndpoint.js.map +1 -0
  79. package/dist/matterbridgeEndpointHelpers.d.ts +2275 -0
  80. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  81. package/dist/matterbridgeEndpointHelpers.js +118 -9
  82. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  83. package/dist/matterbridgePlatform.d.ts +251 -0
  84. package/dist/matterbridgePlatform.d.ts.map +1 -0
  85. package/dist/matterbridgePlatform.js +245 -20
  86. package/dist/matterbridgePlatform.js.map +1 -0
  87. package/dist/matterbridgeTypes.d.ts +177 -0
  88. package/dist/matterbridgeTypes.d.ts.map +1 -0
  89. package/dist/matterbridgeTypes.js +24 -0
  90. package/dist/matterbridgeTypes.js.map +1 -0
  91. package/dist/pluginManager.d.ts +236 -0
  92. package/dist/pluginManager.d.ts.map +1 -0
  93. package/dist/pluginManager.js +232 -3
  94. package/dist/pluginManager.js.map +1 -0
  95. package/dist/shelly.d.ts +77 -0
  96. package/dist/shelly.d.ts.map +1 -0
  97. package/dist/shelly.js +122 -7
  98. package/dist/shelly.js.map +1 -0
  99. package/dist/storage/export.d.ts +2 -0
  100. package/dist/storage/export.d.ts.map +1 -0
  101. package/dist/storage/export.js +1 -0
  102. package/dist/storage/export.js.map +1 -0
  103. package/dist/update.d.ts +32 -0
  104. package/dist/update.d.ts.map +1 -0
  105. package/dist/update.js +45 -0
  106. package/dist/update.js.map +1 -0
  107. package/dist/utils/colorUtils.d.ts +61 -0
  108. package/dist/utils/colorUtils.d.ts.map +1 -0
  109. package/dist/utils/colorUtils.js +205 -2
  110. package/dist/utils/colorUtils.js.map +1 -0
  111. package/dist/utils/copyDirectory.d.ts +32 -0
  112. package/dist/utils/copyDirectory.d.ts.map +1 -0
  113. package/dist/utils/copyDirectory.js +37 -1
  114. package/dist/utils/copyDirectory.js.map +1 -0
  115. package/dist/utils/createZip.d.ts +38 -0
  116. package/dist/utils/createZip.d.ts.map +1 -0
  117. package/dist/utils/createZip.js +42 -2
  118. package/dist/utils/createZip.js.map +1 -0
  119. package/dist/utils/deepCopy.d.ts +31 -0
  120. package/dist/utils/deepCopy.d.ts.map +1 -0
  121. package/dist/utils/deepCopy.js +40 -0
  122. package/dist/utils/deepCopy.js.map +1 -0
  123. package/dist/utils/deepEqual.d.ts +53 -0
  124. package/dist/utils/deepEqual.d.ts.map +1 -0
  125. package/dist/utils/deepEqual.js +65 -1
  126. package/dist/utils/deepEqual.js.map +1 -0
  127. package/dist/utils/export.d.ts +10 -0
  128. package/dist/utils/export.d.ts.map +1 -0
  129. package/dist/utils/export.js +1 -0
  130. package/dist/utils/export.js.map +1 -0
  131. package/dist/utils/isvalid.d.ts +87 -0
  132. package/dist/utils/isvalid.d.ts.map +1 -0
  133. package/dist/utils/isvalid.js +86 -0
  134. package/dist/utils/isvalid.js.map +1 -0
  135. package/dist/utils/network.d.ts +70 -0
  136. package/dist/utils/network.d.ts.map +1 -0
  137. package/dist/utils/network.js +77 -5
  138. package/dist/utils/network.js.map +1 -0
  139. package/dist/utils/parameter.d.ts +44 -0
  140. package/dist/utils/parameter.d.ts.map +1 -0
  141. package/dist/utils/parameter.js +41 -0
  142. package/dist/utils/parameter.js.map +1 -0
  143. package/dist/utils/wait.d.ts +43 -0
  144. package/dist/utils/wait.d.ts.map +1 -0
  145. package/dist/utils/wait.js +48 -5
  146. package/dist/utils/wait.js.map +1 -0
  147. package/frontend/build/asset-manifest.json +3 -3
  148. package/frontend/build/index.html +1 -1
  149. package/frontend/build/static/js/main.025c65d2.js +115 -0
  150. package/frontend/build/static/js/main.025c65d2.js.map +1 -0
  151. package/npm-shrinkwrap.json +44 -44
  152. package/package.json +3 -2
  153. package/frontend/build/static/js/main.8240902c.js +0 -115
  154. package/frontend/build/static/js/main.8240902c.js.map +0 -1
  155. /package/frontend/build/static/js/{main.8240902c.js.LICENSE.txt → main.025c65d2.js.LICENSE.txt} +0 -0
package/dist/frontend.js CHANGED
@@ -1,4 +1,28 @@
1
- import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat } from '@matter/main';
1
+ /**
2
+ * This file contains the class Frontend.
3
+ *
4
+ * @file frontend.ts
5
+ * @author Luca Liguori
6
+ * @date 2025-01-13
7
+ * @version 1.0.2
8
+ *
9
+ * Copyright 2025, 2026, 2027 Luca Liguori.
10
+ *
11
+ * Licensed under the Apache License, Version 2.0 (the "License");
12
+ * you may not use this file except in compliance with the License.
13
+ * You may obtain a copy of the License at
14
+ *
15
+ * http://www.apache.org/licenses/LICENSE-2.0
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License. *
22
+ */
23
+ // @matter
24
+ import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
25
+ // Node modules
2
26
  import { createServer } from 'node:http';
3
27
  import https from 'https';
4
28
  import express from 'express';
@@ -6,19 +30,71 @@ import WebSocket, { WebSocketServer } from 'ws';
6
30
  import os from 'node:os';
7
31
  import path from 'node:path';
8
32
  import { promises as fs } from 'node:fs';
33
+ // AnsiLogger module
9
34
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from './logger/export.js';
10
- import { createZip, deepCopy, isValidNumber, isValidObject, isValidString } from './utils/export.js';
35
+ // Matterbridge
36
+ import { createZip, deepCopy, isValidArray, isValidNumber, isValidObject, isValidString } from './utils/export.js';
11
37
  import { plg } from './matterbridgeTypes.js';
12
38
  import { hasParameter } from './utils/export.js';
39
+ import { BridgedDeviceBasicInformation } from '@matter/main/clusters';
40
+ /**
41
+ * Websocket message ID for logging.
42
+ * @constant {number}
43
+ */
13
44
  export const WS_ID_LOG = 0;
45
+ /**
46
+ * Websocket message ID indicating a refresh is needed.
47
+ * @constant {number}
48
+ */
14
49
  export const WS_ID_REFRESH_NEEDED = 1;
50
+ /**
51
+ * Websocket message ID indicating a restart is needed.
52
+ * @constant {number}
53
+ */
15
54
  export const WS_ID_RESTART_NEEDED = 2;
55
+ /**
56
+ * Websocket message ID indicating a cpu update.
57
+ * @constant {number}
58
+ */
16
59
  export const WS_ID_CPU_UPDATE = 3;
60
+ /**
61
+ * Websocket message ID indicating a memory update.
62
+ * @constant {number}
63
+ */
17
64
  export const WS_ID_MEMORY_UPDATE = 4;
65
+ /**
66
+ * Websocket message ID indicating an uptime update.
67
+ * @constant {number}
68
+ */
18
69
  export const WS_ID_UPTIME_UPDATE = 5;
70
+ /**
71
+ * Websocket message ID indicating a memory update.
72
+ * @constant {number}
73
+ */
19
74
  export const WS_ID_SNACKBAR = 6;
75
+ /**
76
+ * Websocket message ID indicating a shelly system update.
77
+ * check:
78
+ * curl -k http://127.0.0.1:8101/api/updates/sys/check
79
+ * perform:
80
+ * curl -k http://127.0.0.1:8101/api/updates/sys/perform
81
+ * @constant {number}
82
+ */
20
83
  export const WS_ID_SHELLY_SYS_UPDATE = 100;
84
+ /**
85
+ * Websocket message ID indicating a shelly main update.
86
+ * check:
87
+ * curl -k http://127.0.0.1:8101/api/updates/main/check
88
+ * perform:
89
+ * curl -k http://127.0.0.1:8101/api/updates/main/perform
90
+ * @constant {number}
91
+ */
21
92
  export const WS_ID_SHELLY_MAIN_UPDATE = 101;
93
+ /**
94
+ * Initializes the frontend of Matterbridge.
95
+ *
96
+ * @param port The port number to run the frontend server on. Default is 8283.
97
+ */
22
98
  export class Frontend {
23
99
  matterbridge;
24
100
  log;
@@ -35,7 +111,7 @@ export class Frontend {
35
111
  memoryTimeout;
36
112
  constructor(matterbridge) {
37
113
  this.matterbridge = matterbridge;
38
- this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
114
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
39
115
  }
40
116
  set logLevel(logLevel) {
41
117
  this.log.logLevel = logLevel;
@@ -43,10 +119,21 @@ export class Frontend {
43
119
  async start(port = 8283) {
44
120
  this.port = port;
45
121
  this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
122
+ // Create the express app that serves the frontend
46
123
  this.expressApp = express();
124
+ // Log all requests to the server for debugging
125
+ /*
126
+ this.expressApp.use((req, res, next) => {
127
+ this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`);
128
+ next();
129
+ });
130
+ */
131
+ // Serve static files from '/static' endpoint
47
132
  this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
48
133
  if (!hasParameter('ssl')) {
134
+ // Create an HTTP server and attach the express app
49
135
  this.httpServer = createServer(this.expressApp);
136
+ // Listen on the specified port
50
137
  if (hasParameter('ingress')) {
51
138
  this.httpServer.listen(this.port, '0.0.0.0', () => {
52
139
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -60,6 +147,7 @@ export class Frontend {
60
147
  this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
61
148
  });
62
149
  }
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
151
  this.httpServer.on('error', (error) => {
64
152
  this.log.error(`Frontend http server error listening on ${this.port}`);
65
153
  switch (error.code) {
@@ -75,6 +163,7 @@ export class Frontend {
75
163
  });
76
164
  }
77
165
  else {
166
+ // Load the SSL certificate, the private key and optionally the CA certificate
78
167
  let cert;
79
168
  try {
80
169
  cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
@@ -102,7 +191,9 @@ export class Frontend {
102
191
  this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
103
192
  }
104
193
  const serverOptions = { cert, key, ca };
194
+ // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
105
195
  this.httpsServer = https.createServer(serverOptions, this.expressApp);
196
+ // Listen on the specified port
106
197
  if (hasParameter('ingress')) {
107
198
  this.httpsServer.listen(this.port, '0.0.0.0', () => {
108
199
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
@@ -116,6 +207,7 @@ export class Frontend {
116
207
  this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
117
208
  });
118
209
  }
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
211
  this.httpsServer.on('error', (error) => {
120
212
  this.log.error(`Frontend https server error listening on ${this.port}`);
121
213
  switch (error.code) {
@@ -132,16 +224,18 @@ export class Frontend {
132
224
  }
133
225
  if (this.initializeError)
134
226
  return;
227
+ // Create a WebSocket server and attach it to the http or https server
135
228
  const wssPort = this.port;
136
229
  const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
137
230
  this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
138
231
  this.webSocketServer.on('connection', (ws, request) => {
139
232
  const clientIp = request.socket.remoteAddress;
140
- let callbackLogLevel = "notice";
141
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
142
- callbackLogLevel = "info";
143
- if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
144
- callbackLogLevel = "debug";
233
+ // Set the global logger callback for the WebSocketServer
234
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
235
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
236
+ callbackLogLevel = "info" /* LogLevel.INFO */;
237
+ if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
238
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
145
239
  AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
146
240
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
147
241
  this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
@@ -175,6 +269,7 @@ export class Frontend {
175
269
  this.webSocketServer.on('error', (ws, error) => {
176
270
  this.log.error(`WebSocketServer error: ${error}`);
177
271
  });
272
+ // Subscribe to cli events
178
273
  const { cliEmitter } = await import('./cli.js');
179
274
  cliEmitter.on('uptime', (systemUptime, processUptime) => {
180
275
  this.wssSendUptimeUpdate(systemUptime, processUptime);
@@ -185,6 +280,7 @@ export class Frontend {
185
280
  cliEmitter.on('cpu', (cpuUsage) => {
186
281
  this.wssSendCpuUpdate(cpuUsage);
187
282
  });
283
+ // Endpoint to validate login code
188
284
  this.expressApp.post('/api/login', express.json(), async (req, res) => {
189
285
  const { password } = req.body;
190
286
  this.log.debug('The frontend sent /api/login', password);
@@ -203,23 +299,27 @@ export class Frontend {
203
299
  this.log.warn('/api/login error wrong password');
204
300
  res.json({ valid: false });
205
301
  }
302
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
206
303
  }
207
304
  catch (error) {
208
305
  this.log.error('/api/login error getting password');
209
306
  res.json({ valid: false });
210
307
  }
211
308
  });
309
+ // Endpoint to provide health check
212
310
  this.expressApp.get('/health', (req, res) => {
213
311
  this.log.debug('Express received /health');
214
312
  const healthStatus = {
215
- status: 'ok',
216
- uptime: process.uptime(),
217
- timestamp: new Date().toISOString(),
313
+ status: 'ok', // Indicate service is healthy
314
+ uptime: process.uptime(), // Server uptime in seconds
315
+ timestamp: new Date().toISOString(), // Current timestamp
218
316
  };
219
317
  res.status(200).json(healthStatus);
220
318
  });
319
+ // Endpoint to provide memory usage details
221
320
  this.expressApp.get('/memory', async (req, res) => {
222
321
  this.log.debug('Express received /memory');
322
+ // Memory usage from process
223
323
  const memoryUsageRaw = process.memoryUsage();
224
324
  const memoryUsage = {
225
325
  rss: this.formatMemoryUsage(memoryUsageRaw.rss),
@@ -228,10 +328,13 @@ export class Frontend {
228
328
  external: this.formatMemoryUsage(memoryUsageRaw.external),
229
329
  arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
230
330
  };
331
+ // V8 heap statistics
231
332
  const { default: v8 } = await import('node:v8');
232
333
  const heapStatsRaw = v8.getHeapStatistics();
233
334
  const heapSpacesRaw = v8.getHeapSpaceStatistics();
335
+ // Format heapStats
234
336
  const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
337
+ // Format heapSpaces
235
338
  const heapSpaces = heapSpacesRaw.map((space) => ({
236
339
  ...space,
237
340
  space_size: this.formatMemoryUsage(space.space_size),
@@ -249,6 +352,7 @@ export class Frontend {
249
352
  };
250
353
  res.status(200).json(memoryReport);
251
354
  });
355
+ // Endpoint to start advertising the server node
252
356
  this.expressApp.get('/api/advertise', express.json(), async (req, res) => {
253
357
  const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
254
358
  if (pairingCodes) {
@@ -259,18 +363,22 @@ export class Frontend {
259
363
  res.status(500).json({ error: 'Failed to generate pairing codes' });
260
364
  }
261
365
  });
366
+ // Endpoint to provide settings
262
367
  this.expressApp.get('/api/settings', express.json(), async (req, res) => {
263
368
  this.log.debug('The frontend sent /api/settings');
264
369
  res.json(await this.getApiSettings());
265
370
  });
371
+ // Endpoint to provide plugins
266
372
  this.expressApp.get('/api/plugins', async (req, res) => {
267
373
  this.log.debug('The frontend sent /api/plugins');
268
374
  res.json(this.getBaseRegisteredPlugins());
269
375
  });
376
+ // Endpoint to provide devices
270
377
  this.expressApp.get('/api/devices', (req, res) => {
271
378
  this.log.debug('The frontend sent /api/devices');
272
379
  const devices = [];
273
380
  this.matterbridge.devices.forEach(async (device) => {
381
+ // Check if the device has the required properties
274
382
  if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
275
383
  return;
276
384
  const cluster = this.getClusterTextFromDevice(device);
@@ -283,11 +391,13 @@ export class Frontend {
283
391
  productUrl: device.productUrl,
284
392
  configUrl: device.configUrl,
285
393
  uniqueId: device.uniqueId,
394
+ reachable: this.getReachability(device),
286
395
  cluster: cluster,
287
396
  });
288
397
  });
289
398
  res.json(devices);
290
399
  });
400
+ // Endpoint to provide the cluster servers of the devices
291
401
  this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
292
402
  const selectedPluginName = req.params.selectedPluginName;
293
403
  const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
@@ -360,6 +470,7 @@ export class Frontend {
360
470
  });
361
471
  res.json(data);
362
472
  });
473
+ // Endpoint to view the log
363
474
  this.expressApp.get('/api/view-log', async (req, res) => {
364
475
  this.log.debug('The frontend sent /api/log');
365
476
  try {
@@ -372,10 +483,12 @@ export class Frontend {
372
483
  res.status(500).send('Error reading log file');
373
484
  }
374
485
  });
486
+ // Endpoint to download the matterbridge log
375
487
  this.expressApp.get('/api/download-mblog', async (req, res) => {
376
488
  this.log.debug('The frontend sent /api/download-mblog');
377
489
  try {
378
490
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
491
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
379
492
  }
380
493
  catch (error) {
381
494
  fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
@@ -387,10 +500,12 @@ export class Frontend {
387
500
  }
388
501
  });
389
502
  });
503
+ // Endpoint to download the matter log
390
504
  this.expressApp.get('/api/download-mjlog', async (req, res) => {
391
505
  this.log.debug('The frontend sent /api/download-mjlog');
392
506
  try {
393
507
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
508
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
394
509
  }
395
510
  catch (error) {
396
511
  fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
@@ -402,10 +517,12 @@ export class Frontend {
402
517
  }
403
518
  });
404
519
  });
520
+ // Endpoint to download the matter log
405
521
  this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
406
522
  this.log.debug('The frontend sent /api/shellydownloadsystemlog');
407
523
  try {
408
524
  await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
525
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
409
526
  }
410
527
  catch (error) {
411
528
  fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'Create the Shelly system log before downloading it.');
@@ -417,6 +534,7 @@ export class Frontend {
417
534
  }
418
535
  });
419
536
  });
537
+ // Endpoint to download the matter storage file
420
538
  this.expressApp.get('/api/download-mjstorage', async (req, res) => {
421
539
  this.log.debug('The frontend sent /api/download-mjstorage');
422
540
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
@@ -427,6 +545,7 @@ export class Frontend {
427
545
  }
428
546
  });
429
547
  });
548
+ // Endpoint to download the matterbridge storage directory
430
549
  this.expressApp.get('/api/download-mbstorage', async (req, res) => {
431
550
  this.log.debug('The frontend sent /api/download-mbstorage');
432
551
  await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
@@ -437,6 +556,7 @@ export class Frontend {
437
556
  }
438
557
  });
439
558
  });
559
+ // Endpoint to download the matterbridge plugin directory
440
560
  this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
441
561
  this.log.debug('The frontend sent /api/download-pluginstorage');
442
562
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
@@ -447,9 +567,11 @@ export class Frontend {
447
567
  }
448
568
  });
449
569
  });
570
+ // Endpoint to download the matterbridge plugin config files
450
571
  this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
451
572
  this.log.debug('The frontend sent /api/download-pluginconfig');
452
573
  await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
574
+ // await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
453
575
  res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
454
576
  if (error) {
455
577
  this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
@@ -457,6 +579,7 @@ export class Frontend {
457
579
  }
458
580
  });
459
581
  });
582
+ // Endpoint to download the matterbridge plugin config files
460
583
  this.expressApp.get('/api/download-backup', async (req, res) => {
461
584
  this.log.debug('The frontend sent /api/download-backup');
462
585
  res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
@@ -466,6 +589,7 @@ export class Frontend {
466
589
  }
467
590
  });
468
591
  });
592
+ // Endpoint to receive commands
469
593
  this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
470
594
  const command = req.params.command;
471
595
  let param = req.params.param;
@@ -475,13 +599,15 @@ export class Frontend {
475
599
  return;
476
600
  }
477
601
  this.log.debug(`Received frontend command: ${command}:${param}`);
602
+ // Handle the command setpassword from Settings
478
603
  if (command === 'setpassword') {
479
- const password = param.slice(1, -1);
604
+ const password = param.slice(1, -1); // Remove the first and last characters
480
605
  this.log.debug('setpassword', param, password);
481
606
  await this.matterbridge.nodeContext?.set('password', password);
482
607
  res.json({ message: 'Command received' });
483
608
  return;
484
609
  }
610
+ // Handle the command setbridgemode from Settings
485
611
  if (command === 'setbridgemode') {
486
612
  this.log.debug(`setbridgemode: ${param}`);
487
613
  this.wssSendRestartRequired();
@@ -489,6 +615,7 @@ export class Frontend {
489
615
  res.json({ message: 'Command received' });
490
616
  return;
491
617
  }
618
+ // Handle the command backup from Settings
492
619
  if (command === 'backup') {
493
620
  this.log.notice(`Prepairing the backup...`);
494
621
  await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
@@ -497,31 +624,33 @@ export class Frontend {
497
624
  res.json({ message: 'Command received' });
498
625
  return;
499
626
  }
627
+ // Handle the command setmbloglevel from Settings
500
628
  if (command === 'setmbloglevel') {
501
629
  this.log.debug('Matterbridge log level:', param);
502
630
  if (param === 'Debug') {
503
- this.log.logLevel = "debug";
631
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
504
632
  }
505
633
  else if (param === 'Info') {
506
- this.log.logLevel = "info";
634
+ this.log.logLevel = "info" /* LogLevel.INFO */;
507
635
  }
508
636
  else if (param === 'Notice') {
509
- this.log.logLevel = "notice";
637
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
510
638
  }
511
639
  else if (param === 'Warn') {
512
- this.log.logLevel = "warn";
640
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
513
641
  }
514
642
  else if (param === 'Error') {
515
- this.log.logLevel = "error";
643
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
516
644
  }
517
645
  else if (param === 'Fatal') {
518
- this.log.logLevel = "fatal";
646
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
519
647
  }
520
648
  await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
521
649
  await this.matterbridge.setLogLevel(this.log.logLevel);
522
650
  res.json({ message: 'Command received' });
523
651
  return;
524
652
  }
653
+ // Handle the command setmbloglevel from Settings
525
654
  if (command === 'setmjloglevel') {
526
655
  this.log.debug('Matter.js log level:', param);
527
656
  if (param === 'Debug') {
@@ -546,30 +675,34 @@ export class Frontend {
546
675
  res.json({ message: 'Command received' });
547
676
  return;
548
677
  }
678
+ // Handle the command setmdnsinterface from Settings
549
679
  if (command === 'setmdnsinterface') {
550
- param = param.slice(1, -1);
680
+ param = param.slice(1, -1); // Remove the first and last characters *mdns*
551
681
  this.matterbridge.matterbridgeInformation.mattermdnsinterface = param;
552
682
  this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
553
683
  await this.matterbridge.nodeContext?.set('mattermdnsinterface', param);
554
684
  res.json({ message: 'Command received' });
555
685
  return;
556
686
  }
687
+ // Handle the command setipv4address from Settings
557
688
  if (command === 'setipv4address') {
558
- param = param.slice(1, -1);
689
+ param = param.slice(1, -1); // Remove the first and last characters *ip*
559
690
  this.matterbridge.matterbridgeInformation.matteripv4address = param;
560
691
  this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
561
692
  await this.matterbridge.nodeContext?.set('matteripv4address', param);
562
693
  res.json({ message: 'Command received' });
563
694
  return;
564
695
  }
696
+ // Handle the command setipv6address from Settings
565
697
  if (command === 'setipv6address') {
566
- param = param.slice(1, -1);
698
+ param = param.slice(1, -1); // Remove the first and last characters *ip*
567
699
  this.matterbridge.matterbridgeInformation.matteripv6address = param;
568
700
  this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
569
701
  await this.matterbridge.nodeContext?.set('matteripv6address', param);
570
702
  res.json({ message: 'Command received' });
571
703
  return;
572
704
  }
705
+ // Handle the command setmatterport from Settings
573
706
  if (command === 'setmatterport') {
574
707
  const port = Math.min(Math.max(parseInt(param), 5540), 5560);
575
708
  this.matterbridge.matterbridgeInformation.matterPort = port;
@@ -578,6 +711,7 @@ export class Frontend {
578
711
  res.json({ message: 'Command received' });
579
712
  return;
580
713
  }
714
+ // Handle the command setmatterdiscriminator from Settings
581
715
  if (command === 'setmatterdiscriminator') {
582
716
  const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
583
717
  this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
@@ -586,6 +720,7 @@ export class Frontend {
586
720
  res.json({ message: 'Command received' });
587
721
  return;
588
722
  }
723
+ // Handle the command setmatterpasscode from Settings
589
724
  if (command === 'setmatterpasscode') {
590
725
  const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
591
726
  this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
@@ -594,17 +729,20 @@ export class Frontend {
594
729
  res.json({ message: 'Command received' });
595
730
  return;
596
731
  }
732
+ // Handle the command setmbloglevel from Settings
597
733
  if (command === 'setmblogfile') {
598
734
  this.log.debug('Matterbridge file log:', param);
599
735
  this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
600
736
  await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
737
+ // Create the file logger for matterbridge
601
738
  if (param === 'true')
602
- AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug", true);
739
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug" /* LogLevel.DEBUG */, true);
603
740
  else
604
741
  AnsiLogger.setGlobalLogfile(undefined);
605
742
  res.json({ message: 'Command received' });
606
743
  return;
607
744
  }
745
+ // Handle the command setmbloglevel from Settings
608
746
  if (command === 'setmjlogfile') {
609
747
  this.log.debug('Matter file log:', param);
610
748
  this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
@@ -631,40 +769,48 @@ export class Frontend {
631
769
  res.json({ message: 'Command received' });
632
770
  return;
633
771
  }
772
+ // Handle the command unregister from Settings
634
773
  if (command === 'unregister') {
635
774
  await this.matterbridge.unregisterAndShutdownProcess();
636
775
  res.json({ message: 'Command received' });
637
776
  return;
638
777
  }
778
+ // Handle the command reset from Settings
639
779
  if (command === 'reset') {
640
780
  await this.matterbridge.shutdownProcessAndReset();
641
781
  res.json({ message: 'Command received' });
642
782
  return;
643
783
  }
784
+ // Handle the command factoryreset from Settings
644
785
  if (command === 'factoryreset') {
645
786
  await this.matterbridge.shutdownProcessAndFactoryReset();
646
787
  res.json({ message: 'Command received' });
647
788
  return;
648
789
  }
790
+ // Handle the command shutdown from Header
649
791
  if (command === 'shutdown') {
650
792
  await this.matterbridge.shutdownProcess();
651
793
  res.json({ message: 'Command received' });
652
794
  return;
653
795
  }
796
+ // Handle the command restart from Header
654
797
  if (command === 'restart') {
655
798
  await this.matterbridge.restartProcess();
656
799
  res.json({ message: 'Command received' });
657
800
  return;
658
801
  }
802
+ // Handle the command update from Header
659
803
  if (command === 'update') {
660
804
  await this.matterbridge.updateProcess();
661
805
  this.wssSendRestartRequired();
662
806
  res.json({ message: 'Command received' });
663
807
  return;
664
808
  }
809
+ // Handle the command saveconfig from Home
665
810
  if (command === 'saveconfig') {
666
811
  param = param.replace(/\*/g, '\\');
667
812
  this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
813
+ // console.log('Req.body:', JSON.stringify(req.body, null, 2));
668
814
  if (!this.matterbridge.plugins.has(param)) {
669
815
  this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
670
816
  }
@@ -679,6 +825,7 @@ export class Frontend {
679
825
  res.json({ message: 'Command received' });
680
826
  return;
681
827
  }
828
+ // Handle the command installplugin from Home
682
829
  if (command === 'installplugin') {
683
830
  param = param.replace(/\*/g, '\\');
684
831
  this.log.info(`Installing plugin ${plg}${param}${nf}...`);
@@ -687,24 +834,30 @@ export class Frontend {
687
834
  await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
688
835
  this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
689
836
  this.wssSendSnackbarMessage(`Installed package ${param}`);
837
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
690
838
  }
691
839
  catch (error) {
692
840
  this.log.error(`Error installing plugin ${plg}${param}${er}`);
693
841
  this.wssSendSnackbarMessage(`Package ${param} not installed`);
694
842
  }
695
- this.wssSendSnackbarMessage(`Restart required`, 0);
696
843
  this.wssSendRestartRequired();
697
844
  param = param.split('@')[0];
845
+ // Also add the plugin to matterbridge so no return!
698
846
  if (param === 'matterbridge') {
847
+ // If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
699
848
  res.json({ message: 'Command received' });
700
849
  return;
701
850
  }
702
851
  }
852
+ // Handle the command addplugin from Home
703
853
  if (command === 'addplugin' || command === 'installplugin') {
704
854
  param = param.replace(/\*/g, '\\');
705
855
  const plugin = await this.matterbridge.plugins.add(param);
706
856
  if (plugin) {
857
+ this.wssSendSnackbarMessage(`Added plugin ${param}`);
707
858
  if (this.matterbridge.bridgeMode === 'childbridge') {
859
+ // We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node
860
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
708
861
  this.matterbridge.createDynamicPlugin(plugin, true);
709
862
  }
710
863
  this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
@@ -714,19 +867,22 @@ export class Frontend {
714
867
  res.json({ message: 'Command received' });
715
868
  return;
716
869
  }
870
+ // Handle the command removeplugin from Home
717
871
  if (command === 'removeplugin') {
718
872
  if (!this.matterbridge.plugins.has(param)) {
719
873
  this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
720
874
  }
721
875
  else {
722
876
  const plugin = this.matterbridge.plugins.get(param);
723
- await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
877
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode
724
878
  await this.matterbridge.plugins.remove(param);
879
+ this.wssSendSnackbarMessage(`Removed plugin ${param}`);
725
880
  }
726
881
  res.json({ message: 'Command received' });
727
882
  this.wssSendRefreshRequired();
728
883
  return;
729
884
  }
885
+ // Handle the command enableplugin from Home
730
886
  if (command === 'enableplugin') {
731
887
  if (!this.matterbridge.plugins.has(param)) {
732
888
  this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
@@ -743,7 +899,9 @@ export class Frontend {
743
899
  plugin.registeredDevices = undefined;
744
900
  plugin.addedDevices = undefined;
745
901
  await this.matterbridge.plugins.enable(param);
902
+ this.wssSendSnackbarMessage(`Enabled plugin ${param}`);
746
903
  if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
904
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
747
905
  this.matterbridge.createDynamicPlugin(plugin, true);
748
906
  }
749
907
  this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true).then(() => {
@@ -755,6 +913,7 @@ export class Frontend {
755
913
  this.wssSendRefreshRequired();
756
914
  return;
757
915
  }
916
+ // Handle the command disableplugin from Home
758
917
  if (command === 'disableplugin') {
759
918
  if (!this.matterbridge.plugins.has(param)) {
760
919
  this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
@@ -762,8 +921,9 @@ export class Frontend {
762
921
  else {
763
922
  const plugin = this.matterbridge.plugins.get(param);
764
923
  if (plugin && plugin.enabled) {
765
- await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
924
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true); // This will also close the server node in childbridge mode
766
925
  await this.matterbridge.plugins.disable(param);
926
+ this.wssSendSnackbarMessage(`Disabled plugin ${param}`);
767
927
  }
768
928
  }
769
929
  res.json({ message: 'Command received' });
@@ -771,6 +931,7 @@ export class Frontend {
771
931
  return;
772
932
  }
773
933
  });
934
+ // Fallback for routing (must be the last route)
774
935
  this.expressApp.get('*', (req, res) => {
775
936
  this.log.debug('The frontend sent:', req.url);
776
937
  this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
@@ -779,24 +940,29 @@ export class Frontend {
779
940
  this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
780
941
  }
781
942
  async stop() {
943
+ // Close the http server
782
944
  if (this.httpServer) {
783
945
  this.httpServer.close();
784
946
  this.httpServer.removeAllListeners();
785
947
  this.httpServer = undefined;
786
948
  this.log.debug('Frontend http server closed successfully');
787
949
  }
950
+ // Close the https server
788
951
  if (this.httpsServer) {
789
952
  this.httpsServer.close();
790
953
  this.httpsServer.removeAllListeners();
791
954
  this.httpsServer = undefined;
792
955
  this.log.debug('Frontend https server closed successfully');
793
956
  }
957
+ // Remove listeners from the express app
794
958
  if (this.expressApp) {
795
959
  this.expressApp.removeAllListeners();
796
960
  this.expressApp = undefined;
797
961
  this.log.debug('Frontend app closed successfully');
798
962
  }
963
+ // Close the WebSocket server
799
964
  if (this.webSocketServer) {
965
+ // Close all active connections
800
966
  this.webSocketServer.clients.forEach((client) => {
801
967
  if (client.readyState === WebSocket.OPEN) {
802
968
  client.close();
@@ -813,6 +979,7 @@ export class Frontend {
813
979
  this.webSocketServer = undefined;
814
980
  }
815
981
  }
982
+ // Function to format bytes to KB, MB, or GB
816
983
  formatMemoryUsage = (bytes) => {
817
984
  if (bytes >= 1024 ** 3) {
818
985
  return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
@@ -824,6 +991,7 @@ export class Frontend {
824
991
  return `${(bytes / 1024).toFixed(2)} KB`;
825
992
  }
826
993
  };
994
+ // Function to format system uptime with only the most significant unit
827
995
  formatOsUpTime = (seconds) => {
828
996
  if (seconds >= 86400) {
829
997
  const days = Math.floor(seconds / 86400);
@@ -839,8 +1007,13 @@ export class Frontend {
839
1007
  }
840
1008
  return `${seconds} second${seconds !== 1 ? 's' : ''}`;
841
1009
  };
1010
+ /**
1011
+ * Retrieves the api settings data.
1012
+ * @returns {Promise<object>} A promise that resolve in the api settings object.
1013
+ */
842
1014
  async getApiSettings() {
843
1015
  const { lastCpuUsage } = await import('./cli.js');
1016
+ // Update the system information
844
1017
  this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
845
1018
  this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
846
1019
  this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
@@ -849,6 +1022,7 @@ export class Frontend {
849
1022
  this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
850
1023
  this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
851
1024
  this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
1025
+ // Update the matterbridge information
852
1026
  this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
853
1027
  this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
854
1028
  this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
@@ -867,7 +1041,28 @@ export class Frontend {
867
1041
  this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
868
1042
  return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
869
1043
  }
1044
+ /**
1045
+ * Retrieves the reachable attribute.
1046
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
1047
+ * @returns {boolean} The reachable attribute.
1048
+ */
1049
+ getReachability(device) {
1050
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
1051
+ return false;
1052
+ if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
1053
+ return device.getAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable');
1054
+ if (this.matterbridge.bridgeMode === 'childbridge')
1055
+ return true;
1056
+ return false;
1057
+ }
1058
+ /**
1059
+ * Retrieves the cluster text description from a given device.
1060
+ * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
1061
+ * @returns {string} The attributes description of the cluster servers in the device.
1062
+ */
870
1063
  getClusterTextFromDevice(device) {
1064
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
1065
+ return '';
871
1066
  const getAttribute = (device, cluster, attribute) => {
872
1067
  let value = undefined;
873
1068
  Object.entries(device.state)
@@ -905,6 +1100,7 @@ export class Frontend {
905
1100
  };
906
1101
  let attributes = '';
907
1102
  device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1103
+ // console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
908
1104
  if (typeof attributeValue === 'undefined')
909
1105
  return;
910
1106
  if (clusterName === 'onOff' && attributeName === 'onOff')
@@ -982,8 +1178,13 @@ export class Frontend {
982
1178
  if (clusterName === 'userLabel' && attributeName === 'labelList')
983
1179
  attributes += `${getUserLabel(device)} `;
984
1180
  });
1181
+ // console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
985
1182
  return attributes.trimStart().trimEnd();
986
1183
  }
1184
+ /**
1185
+ * Retrieves the base registered plugins sanitized for res.json().
1186
+ * @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
1187
+ */
987
1188
  getBaseRegisteredPlugins() {
988
1189
  const baseRegisteredPlugins = [];
989
1190
  for (const plugin of this.matterbridge.plugins) {
@@ -1010,10 +1211,19 @@ export class Frontend {
1010
1211
  manualPairingCode: plugin.manualPairingCode,
1011
1212
  configJson: plugin.configJson,
1012
1213
  schemaJson: plugin.schemaJson,
1214
+ hasWhiteList: plugin.configJson?.whiteList !== undefined,
1215
+ hasBlackList: plugin.configJson?.blackList !== undefined,
1013
1216
  });
1014
1217
  }
1015
1218
  return baseRegisteredPlugins;
1016
1219
  }
1220
+ /**
1221
+ * Handles incoming websocket messages for the Matterbridge frontend.
1222
+ *
1223
+ * @param {WebSocket} client - The websocket client that sent the message.
1224
+ * @param {WebSocket.RawData} message - The raw data of the message received from the client.
1225
+ * @returns {Promise<void>} A promise that resolves when the message has been handled.
1226
+ */
1017
1227
  async wsMessageHandler(client, message) {
1018
1228
  let data;
1019
1229
  try {
@@ -1159,8 +1369,10 @@ export class Frontend {
1159
1369
  else if (data.method === '/api/devices') {
1160
1370
  const devices = [];
1161
1371
  this.matterbridge.devices.forEach(async (device) => {
1372
+ // Filter by pluginName if provided
1162
1373
  if (data.params.pluginName && data.params.pluginName !== device.plugin)
1163
1374
  return;
1375
+ // Check if the device has the required properties
1164
1376
  if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
1165
1377
  return;
1166
1378
  const cluster = this.getClusterTextFromDevice(device);
@@ -1173,6 +1385,7 @@ export class Frontend {
1173
1385
  productUrl: device.productUrl,
1174
1386
  configUrl: device.configUrl,
1175
1387
  uniqueId: device.uniqueId,
1388
+ reachable: this.getReachability(device),
1176
1389
  cluster: cluster,
1177
1390
  });
1178
1391
  });
@@ -1243,6 +1456,7 @@ export class Frontend {
1243
1456
  });
1244
1457
  endpointServer.getChildEndpoints().forEach((childEndpoint) => {
1245
1458
  deviceTypes = [];
1459
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1246
1460
  const name = childEndpoint.endpoint?.id;
1247
1461
  const clusterServers = childEndpoint.getAllClusterServers();
1248
1462
  clusterServers.forEach((clusterServer) => {
@@ -1296,7 +1510,8 @@ export class Frontend {
1296
1510
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select' }));
1297
1511
  return;
1298
1512
  }
1299
- const selectDeviceValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectDevice.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1513
+ // const selectDeviceValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectDevice.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1514
+ const selectDeviceValues = plugin.platform?.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1300
1515
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectDeviceValues }));
1301
1516
  return;
1302
1517
  }
@@ -1310,10 +1525,69 @@ export class Frontend {
1310
1525
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' }));
1311
1526
  return;
1312
1527
  }
1313
- const selectEntityValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectEntity.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1528
+ // const selectEntityValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectEntity.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1529
+ const selectEntityValues = plugin.platform?.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
1314
1530
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectEntityValues }));
1315
1531
  return;
1316
1532
  }
1533
+ else if (data.method === '/api/command') {
1534
+ if (!isValidString(data.params.command, 5)) {
1535
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter command in /api/command' }));
1536
+ return;
1537
+ }
1538
+ if (data.params.command === 'selectdevice' && isValidString(data.params.plugin, 10) && isValidString(data.params.serial, 1)) {
1539
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1540
+ if (!plugin) {
1541
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' }));
1542
+ return;
1543
+ }
1544
+ const config = plugin.configJson;
1545
+ if (config) {
1546
+ if (config.postfix) {
1547
+ data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1548
+ }
1549
+ // Add the serial to the whiteList if the whiteList exists and the serial is not already in it
1550
+ if (isValidArray(config.whiteList, 1)) {
1551
+ if (!config.whiteList.includes(data.params.serial)) {
1552
+ config.whiteList.push(data.params.serial);
1553
+ }
1554
+ }
1555
+ // Remove the serial from the blackList if the blackList exists and the serial is in it
1556
+ if (isValidArray(config.blackList, 1)) {
1557
+ if (config.blackList.includes(data.params.serial)) {
1558
+ config.blackList = config.blackList.filter((serial) => serial !== data.params.serial);
1559
+ }
1560
+ }
1561
+ if (plugin.platform)
1562
+ plugin.platform.config = config;
1563
+ await this.matterbridge.plugins.saveConfigFromPlugin(plugin);
1564
+ this.wssSendRestartRequired();
1565
+ }
1566
+ }
1567
+ else if (data.params.command === 'unselectdevice' && isValidString(data.params.plugin, 10) && isValidString(data.params.serial, 1)) {
1568
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1569
+ if (!plugin) {
1570
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' }));
1571
+ return;
1572
+ }
1573
+ const config = plugin.configJson;
1574
+ if (config) {
1575
+ if (config.postfix) {
1576
+ data.params.serial = data.params.serial.replace('-' + config.postfix, '');
1577
+ }
1578
+ // Add the serial to the blackList
1579
+ if (isValidArray(config.blackList)) {
1580
+ if (!config.blackList.includes(data.params.serial)) {
1581
+ config.blackList.push(data.params.serial);
1582
+ }
1583
+ }
1584
+ if (plugin.platform)
1585
+ plugin.platform.config = config;
1586
+ await this.matterbridge.plugins.saveConfigFromPlugin(plugin);
1587
+ this.wssSendRestartRequired();
1588
+ }
1589
+ }
1590
+ }
1317
1591
  else {
1318
1592
  this.log.error(`Invalid method from websocket client: ${debugStringify(data)}`);
1319
1593
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid method' }));
@@ -1325,85 +1599,139 @@ export class Frontend {
1325
1599
  return;
1326
1600
  }
1327
1601
  }
1602
+ /**
1603
+ * Sends a WebSocket message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
1604
+ *
1605
+ * @param {string} level - The logger level of the message: debug info notice warn error fatal...
1606
+ * @param {string} time - The time string of the message
1607
+ * @param {string} name - The logger name of the message
1608
+ * @param {string} message - The content of the message.
1609
+ */
1328
1610
  wssSendMessage(level, time, name, message) {
1329
1611
  if (!level || !time || !name || !message)
1330
1612
  return;
1613
+ // Remove ANSI escape codes from the message
1614
+ // eslint-disable-next-line no-control-regex
1331
1615
  message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1616
+ // Remove leading asterisks from the message
1332
1617
  message = message.replace(/^\*+/, '');
1618
+ // Replace all occurrences of \t and \n
1333
1619
  message = message.replace(/[\t\n]/g, '');
1620
+ // Remove non-printable characters
1621
+ // eslint-disable-next-line no-control-regex
1334
1622
  message = message.replace(/[\x00-\x1F\x7F]/g, '');
1623
+ // Replace all occurrences of \" with "
1335
1624
  message = message.replace(/\\"/g, '"');
1625
+ // Define the maximum allowed length for continuous characters without a space
1336
1626
  const maxContinuousLength = 100;
1337
1627
  const keepStartLength = 20;
1338
1628
  const keepEndLength = 20;
1629
+ // Split the message into words
1339
1630
  message = message
1340
1631
  .split(' ')
1341
1632
  .map((word) => {
1633
+ // If the word length exceeds the max continuous length, insert spaces and truncate
1342
1634
  if (word.length > maxContinuousLength) {
1343
1635
  return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1344
1636
  }
1345
1637
  return word;
1346
1638
  })
1347
1639
  .join(' ');
1640
+ // Send the message to all connected clients
1348
1641
  this.webSocketServer?.clients.forEach((client) => {
1349
1642
  if (client.readyState === WebSocket.OPEN) {
1350
1643
  client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1351
1644
  }
1352
1645
  });
1353
1646
  }
1647
+ /**
1648
+ * Sends a need to refresh WebSocket message to all connected clients.
1649
+ *
1650
+ */
1354
1651
  wssSendRefreshRequired() {
1355
1652
  this.log.debug('Sending a refresh required message to all connected clients');
1356
1653
  this.matterbridge.matterbridgeInformation.refreshRequired = true;
1654
+ // Send the message to all connected clients
1357
1655
  this.webSocketServer?.clients.forEach((client) => {
1358
1656
  if (client.readyState === WebSocket.OPEN) {
1359
1657
  client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
1360
1658
  }
1361
1659
  });
1362
1660
  }
1661
+ /**
1662
+ * Sends a need to restart WebSocket message to all connected clients.
1663
+ *
1664
+ */
1363
1665
  wssSendRestartRequired() {
1364
1666
  this.log.debug('Sending a restart required message to all connected clients');
1365
1667
  this.matterbridge.matterbridgeInformation.restartRequired = true;
1366
1668
  this.wssSendSnackbarMessage(`Restart required`, 0);
1669
+ // Send the message to all connected clients
1367
1670
  this.webSocketServer?.clients.forEach((client) => {
1368
1671
  if (client.readyState === WebSocket.OPEN) {
1369
1672
  client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1370
1673
  }
1371
1674
  });
1372
1675
  }
1676
+ /**
1677
+ * Sends a memory update message to all connected clients.
1678
+ *
1679
+ */
1373
1680
  wssSendCpuUpdate(cpuUsage) {
1374
1681
  this.log.debug('Sending a cpu update message to all connected clients');
1682
+ // Send the message to all connected clients
1375
1683
  this.webSocketServer?.clients.forEach((client) => {
1376
1684
  if (client.readyState === WebSocket.OPEN) {
1377
1685
  client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
1378
1686
  }
1379
1687
  });
1380
1688
  }
1689
+ /**
1690
+ * Sends a cpu update message to all connected clients.
1691
+ *
1692
+ */
1381
1693
  wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
1382
1694
  this.log.debug('Sending a memory update message to all connected clients');
1695
+ // Send the message to all connected clients
1383
1696
  this.webSocketServer?.clients.forEach((client) => {
1384
1697
  if (client.readyState === WebSocket.OPEN) {
1385
1698
  client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
1386
1699
  }
1387
1700
  });
1388
1701
  }
1702
+ /**
1703
+ * Sends a memory update message to all connected clients.
1704
+ *
1705
+ */
1389
1706
  wssSendUptimeUpdate(systemUptime, processUptime) {
1390
1707
  this.log.debug('Sending a uptime update message to all connected clients');
1708
+ // Send the message to all connected clients
1391
1709
  this.webSocketServer?.clients.forEach((client) => {
1392
1710
  if (client.readyState === WebSocket.OPEN) {
1393
1711
  client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
1394
1712
  }
1395
1713
  });
1396
1714
  }
1715
+ /**
1716
+ * Sends a cpu update message to all connected clients.
1717
+ *
1718
+ */
1397
1719
  wssSendSnackbarMessage(message, timeout = 5) {
1398
1720
  this.log.debug('Sending a snackbar message to all connected clients');
1721
+ // Send the message to all connected clients
1399
1722
  this.webSocketServer?.clients.forEach((client) => {
1400
1723
  if (client.readyState === WebSocket.OPEN) {
1401
1724
  client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { message, timeout } }));
1402
1725
  }
1403
1726
  });
1404
1727
  }
1728
+ /**
1729
+ * Sends a message to all connected clients.
1730
+ *
1731
+ */
1405
1732
  wssBroadcastMessage(id, method, params) {
1406
1733
  this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
1734
+ // Send the message to all connected clients
1407
1735
  this.webSocketServer?.clients.forEach((client) => {
1408
1736
  if (client.readyState === WebSocket.OPEN) {
1409
1737
  client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
@@ -1411,3 +1739,4 @@ export class Frontend {
1411
1739
  });
1412
1740
  }
1413
1741
  }
1742
+ //# sourceMappingURL=frontend.js.map