matterbridge 1.7.3 → 2.0.0-edge.2

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 (102) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/cli.js +3 -39
  3. package/dist/cluster/export.js +0 -2
  4. package/dist/defaultConfigSchema.js +0 -23
  5. package/dist/deviceManager.js +1 -26
  6. package/dist/frontend.js +1203 -0
  7. package/dist/index.js +1 -36
  8. package/dist/logger/export.js +0 -1
  9. package/dist/matter/export.js +0 -5
  10. package/dist/matterbridge.js +516 -2678
  11. package/dist/matterbridgeAccessoryPlatform.js +0 -33
  12. package/dist/matterbridgeBehaviors.js +23 -31
  13. package/dist/matterbridgeDeviceTypes.js +11 -82
  14. package/dist/matterbridgeDynamicPlatform.js +0 -33
  15. package/dist/matterbridgeEndpoint.js +45 -1145
  16. package/dist/matterbridgePlatform.js +14 -145
  17. package/dist/matterbridgeTypes.js +3 -24
  18. package/dist/pluginManager.js +5 -246
  19. package/dist/storage/export.js +0 -1
  20. package/dist/utils/colorUtils.js +2 -205
  21. package/dist/utils/export.js +0 -1
  22. package/dist/utils/utils.js +7 -252
  23. package/frontend/build/asset-manifest.json +3 -3
  24. package/frontend/build/index.html +1 -1
  25. package/frontend/build/static/js/{main.6bbd1772.js → main.6204ae54.js} +3 -3
  26. package/frontend/build/static/js/main.6204ae54.js.map +1 -0
  27. package/npm-shrinkwrap.json +9 -9
  28. package/package.json +2 -3
  29. package/dist/cli.d.ts +0 -25
  30. package/dist/cli.d.ts.map +0 -1
  31. package/dist/cli.js.map +0 -1
  32. package/dist/cluster/export.d.ts +0 -2
  33. package/dist/cluster/export.d.ts.map +0 -1
  34. package/dist/cluster/export.js.map +0 -1
  35. package/dist/defaultConfigSchema.d.ts +0 -27
  36. package/dist/defaultConfigSchema.d.ts.map +0 -1
  37. package/dist/defaultConfigSchema.js.map +0 -1
  38. package/dist/deviceManager.d.ts +0 -46
  39. package/dist/deviceManager.d.ts.map +0 -1
  40. package/dist/deviceManager.js.map +0 -1
  41. package/dist/index.d.ts +0 -40
  42. package/dist/index.d.ts.map +0 -1
  43. package/dist/index.js.map +0 -1
  44. package/dist/logger/export.d.ts +0 -2
  45. package/dist/logger/export.d.ts.map +0 -1
  46. package/dist/logger/export.js.map +0 -1
  47. package/dist/matter/export.d.ts +0 -11
  48. package/dist/matter/export.d.ts.map +0 -1
  49. package/dist/matter/export.js.map +0 -1
  50. package/dist/matterbridge.d.ts +0 -483
  51. package/dist/matterbridge.d.ts.map +0 -1
  52. package/dist/matterbridge.js.map +0 -1
  53. package/dist/matterbridgeAccessoryPlatform.d.ts +0 -39
  54. package/dist/matterbridgeAccessoryPlatform.d.ts.map +0 -1
  55. package/dist/matterbridgeAccessoryPlatform.js.map +0 -1
  56. package/dist/matterbridgeBehaviors.d.ts +0 -942
  57. package/dist/matterbridgeBehaviors.d.ts.map +0 -1
  58. package/dist/matterbridgeBehaviors.js.map +0 -1
  59. package/dist/matterbridgeDevice.d.ts +0 -7077
  60. package/dist/matterbridgeDevice.d.ts.map +0 -1
  61. package/dist/matterbridgeDevice.js +0 -2736
  62. package/dist/matterbridgeDevice.js.map +0 -1
  63. package/dist/matterbridgeDeviceTypes.d.ts +0 -109
  64. package/dist/matterbridgeDeviceTypes.d.ts.map +0 -1
  65. package/dist/matterbridgeDeviceTypes.js.map +0 -1
  66. package/dist/matterbridgeDynamicPlatform.d.ts +0 -39
  67. package/dist/matterbridgeDynamicPlatform.d.ts.map +0 -1
  68. package/dist/matterbridgeDynamicPlatform.js.map +0 -1
  69. package/dist/matterbridgeEdge.d.ts +0 -91
  70. package/dist/matterbridgeEdge.d.ts.map +0 -1
  71. package/dist/matterbridgeEdge.js +0 -1077
  72. package/dist/matterbridgeEdge.js.map +0 -1
  73. package/dist/matterbridgeEndpoint.d.ts +0 -10156
  74. package/dist/matterbridgeEndpoint.d.ts.map +0 -1
  75. package/dist/matterbridgeEndpoint.js.map +0 -1
  76. package/dist/matterbridgePlatform.d.ts +0 -168
  77. package/dist/matterbridgePlatform.d.ts.map +0 -1
  78. package/dist/matterbridgePlatform.js.map +0 -1
  79. package/dist/matterbridgeTypes.d.ts +0 -172
  80. package/dist/matterbridgeTypes.d.ts.map +0 -1
  81. package/dist/matterbridgeTypes.js.map +0 -1
  82. package/dist/matterbridgeWebsocket.d.ts +0 -49
  83. package/dist/matterbridgeWebsocket.d.ts.map +0 -1
  84. package/dist/matterbridgeWebsocket.js +0 -325
  85. package/dist/matterbridgeWebsocket.js.map +0 -1
  86. package/dist/pluginManager.d.ts +0 -238
  87. package/dist/pluginManager.d.ts.map +0 -1
  88. package/dist/pluginManager.js.map +0 -1
  89. package/dist/storage/export.d.ts +0 -2
  90. package/dist/storage/export.d.ts.map +0 -1
  91. package/dist/storage/export.js.map +0 -1
  92. package/dist/utils/colorUtils.d.ts +0 -61
  93. package/dist/utils/colorUtils.d.ts.map +0 -1
  94. package/dist/utils/colorUtils.js.map +0 -1
  95. package/dist/utils/export.d.ts +0 -3
  96. package/dist/utils/export.d.ts.map +0 -1
  97. package/dist/utils/export.js.map +0 -1
  98. package/dist/utils/utils.d.ts +0 -221
  99. package/dist/utils/utils.d.ts.map +0 -1
  100. package/dist/utils/utils.js.map +0 -1
  101. package/frontend/build/static/js/main.6bbd1772.js.map +0 -1
  102. /package/frontend/build/static/js/{main.6bbd1772.js.LICENSE.txt → main.6204ae54.js.LICENSE.txt} +0 -0
@@ -0,0 +1,1203 @@
1
+ import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat } from '@matter/main';
2
+ import { createServer } from 'http';
3
+ import https from 'https';
4
+ import express from 'express';
5
+ import WebSocket, { WebSocketServer } from 'ws';
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { promises as fs } from 'fs';
9
+ import { AnsiLogger, CYAN, db, debugStringify, er, nf, rs, stringify, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
10
+ import { createZip, hasParameter, isValidNumber, isValidObject, isValidString } from './utils/utils.js';
11
+ import { plg } from './matterbridgeTypes.js';
12
+ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
13
+ export const WS_ID_LOG = 0;
14
+ export const WS_ID_REFRESH_NEEDED = 1;
15
+ export const WS_ID_RESTART_NEEDED = 2;
16
+ export class Frontend {
17
+ matterbridge;
18
+ log;
19
+ port = 8283;
20
+ initializeError = false;
21
+ expressApp;
22
+ httpServer;
23
+ httpsServer;
24
+ webSocketServer;
25
+ constructor(matterbridge) {
26
+ this.matterbridge = matterbridge;
27
+ this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
28
+ }
29
+ async start(port = 8283) {
30
+ this.port = port;
31
+ this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
32
+ this.expressApp = express();
33
+ this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
34
+ if (!hasParameter('ssl')) {
35
+ this.httpServer = createServer(this.expressApp);
36
+ if (hasParameter('ingress')) {
37
+ this.httpServer.listen(this.port, '0.0.0.0', () => {
38
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
39
+ });
40
+ }
41
+ else {
42
+ this.httpServer.listen(this.port, () => {
43
+ if (this.matterbridge.systemInformation.ipv4Address !== '')
44
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
45
+ if (this.matterbridge.systemInformation.ipv6Address !== '')
46
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
47
+ });
48
+ }
49
+ this.httpServer.on('error', (error) => {
50
+ this.log.error(`Frontend http server error listening on ${this.port}`);
51
+ switch (error.code) {
52
+ case 'EACCES':
53
+ this.log.error(`Port ${this.port} requires elevated privileges`);
54
+ break;
55
+ case 'EADDRINUSE':
56
+ this.log.error(`Port ${this.port} is already in use`);
57
+ break;
58
+ }
59
+ this.initializeError = true;
60
+ return;
61
+ });
62
+ }
63
+ else {
64
+ let cert;
65
+ try {
66
+ cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
67
+ this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
68
+ }
69
+ catch (error) {
70
+ this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
71
+ return;
72
+ }
73
+ let key;
74
+ try {
75
+ key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
76
+ this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`);
77
+ }
78
+ catch (error) {
79
+ this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
80
+ return;
81
+ }
82
+ let ca;
83
+ try {
84
+ ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
85
+ this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`);
86
+ }
87
+ catch (error) {
88
+ this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
89
+ }
90
+ const serverOptions = { cert, key, ca };
91
+ this.httpsServer = https.createServer(serverOptions, this.expressApp);
92
+ if (hasParameter('ingress')) {
93
+ this.httpsServer.listen(this.port, '0.0.0.0', () => {
94
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
95
+ });
96
+ }
97
+ else {
98
+ this.httpsServer.listen(this.port, () => {
99
+ if (this.matterbridge.systemInformation.ipv4Address !== '')
100
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
101
+ if (this.matterbridge.systemInformation.ipv6Address !== '')
102
+ this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
103
+ });
104
+ }
105
+ this.httpsServer.on('error', (error) => {
106
+ this.log.error(`Frontend https server error listening on ${this.port}`);
107
+ switch (error.code) {
108
+ case 'EACCES':
109
+ this.log.error(`Port ${this.port} requires elevated privileges`);
110
+ break;
111
+ case 'EADDRINUSE':
112
+ this.log.error(`Port ${this.port} is already in use`);
113
+ break;
114
+ }
115
+ this.initializeError = true;
116
+ return;
117
+ });
118
+ }
119
+ if (this.initializeError)
120
+ return;
121
+ const wssPort = this.port;
122
+ const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
123
+ this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
124
+ this.webSocketServer.on('connection', (ws, request) => {
125
+ const clientIp = request.socket.remoteAddress;
126
+ AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), "debug");
127
+ this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
128
+ ws.on('message', (message) => {
129
+ this.wsMessageHandler(ws, message);
130
+ });
131
+ ws.on('ping', () => {
132
+ this.log.debug('WebSocket client ping');
133
+ ws.pong();
134
+ });
135
+ ws.on('pong', () => {
136
+ this.log.debug('WebSocket client pong');
137
+ });
138
+ ws.on('close', () => {
139
+ this.log.info('WebSocket client disconnected');
140
+ if (this.webSocketServer?.clients.size === 0) {
141
+ AnsiLogger.setGlobalCallback(undefined);
142
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
143
+ }
144
+ });
145
+ ws.on('error', (error) => {
146
+ this.log.error(`WebSocket client error: ${error}`);
147
+ });
148
+ });
149
+ this.webSocketServer.on('close', () => {
150
+ this.log.debug(`WebSocketServer closed`);
151
+ });
152
+ this.webSocketServer.on('listening', () => {
153
+ this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
154
+ });
155
+ this.webSocketServer.on('error', (ws, error) => {
156
+ this.log.error(`WebSocketServer error: ${error}`);
157
+ });
158
+ this.expressApp.post('/api/login', express.json(), async (req, res) => {
159
+ const { password } = req.body;
160
+ this.log.debug('The frontend sent /api/login', password);
161
+ if (!this.matterbridge.nodeContext) {
162
+ this.log.error('/api/login nodeContext not found');
163
+ res.json({ valid: false });
164
+ return;
165
+ }
166
+ try {
167
+ const storedPassword = await this.matterbridge.nodeContext.get('password', '');
168
+ if (storedPassword === '' || password === storedPassword) {
169
+ this.log.debug('/api/login password valid');
170
+ res.json({ valid: true });
171
+ }
172
+ else {
173
+ this.log.warn('/api/login error wrong password');
174
+ res.json({ valid: false });
175
+ }
176
+ }
177
+ catch (error) {
178
+ this.log.error('/api/login error getting password');
179
+ res.json({ valid: false });
180
+ }
181
+ });
182
+ this.expressApp.get('/health', (req, res) => {
183
+ this.log.debug('Express received /health');
184
+ const healthStatus = {
185
+ status: 'ok',
186
+ uptime: process.uptime(),
187
+ timestamp: new Date().toISOString(),
188
+ };
189
+ res.status(200).json(healthStatus);
190
+ });
191
+ this.expressApp.get('/api/settings', express.json(), async (req, res) => {
192
+ this.log.debug('The frontend sent /api/settings');
193
+ this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
194
+ this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
195
+ this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
196
+ this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
197
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
198
+ this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
199
+ this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
200
+ this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
201
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
202
+ this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
203
+ this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
204
+ this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
205
+ this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
206
+ this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
207
+ this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
208
+ this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
209
+ this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
210
+ const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
211
+ res.json(response);
212
+ });
213
+ this.expressApp.get('/api/plugins', async (req, res) => {
214
+ this.log.debug('The frontend sent /api/plugins');
215
+ const response = await this.getBaseRegisteredPlugins();
216
+ res.json(response);
217
+ });
218
+ this.expressApp.get('/api/devices', (req, res) => {
219
+ this.log.debug('The frontend sent /api/devices');
220
+ const devices = [];
221
+ this.matterbridge.devices.forEach(async (device) => {
222
+ if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
223
+ return;
224
+ const cluster = this.getClusterTextFromDevice(device);
225
+ devices.push({
226
+ pluginName: device.plugin,
227
+ type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
228
+ endpoint: device.number,
229
+ name: device.deviceName,
230
+ serial: device.serialNumber,
231
+ productUrl: device.productUrl,
232
+ configUrl: device.configUrl,
233
+ uniqueId: device.uniqueId,
234
+ cluster: cluster,
235
+ });
236
+ });
237
+ res.json(devices);
238
+ });
239
+ this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
240
+ const selectedPluginName = req.params.selectedPluginName;
241
+ const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
242
+ this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
243
+ if (selectedPluginName === 'none') {
244
+ res.json([]);
245
+ return;
246
+ }
247
+ const data = [];
248
+ this.matterbridge.devices.forEach(async (device) => {
249
+ const pluginName = device.plugin;
250
+ if (pluginName === selectedPluginName && device.number === selectedDeviceEndpoint) {
251
+ const endpointServer = EndpointServer.forEndpoint(device);
252
+ const clusterServers = endpointServer.getAllClusterServers();
253
+ clusterServers.forEach((clusterServer) => {
254
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
255
+ if (clusterServer.name === 'EveHistory')
256
+ return;
257
+ let attributeValue;
258
+ try {
259
+ if (typeof value.getLocal() === 'object')
260
+ attributeValue = stringify(value.getLocal());
261
+ else
262
+ attributeValue = value.getLocal().toString();
263
+ }
264
+ catch (error) {
265
+ attributeValue = 'Fabric-Scoped';
266
+ this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
267
+ }
268
+ data.push({
269
+ endpoint: device.number ? device.number.toString() : '...',
270
+ clusterName: clusterServer.name,
271
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
272
+ attributeName: key,
273
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
274
+ attributeValue,
275
+ });
276
+ });
277
+ });
278
+ endpointServer.getChildEndpoints().forEach((childEndpoint) => {
279
+ const name = childEndpoint.name;
280
+ const clusterServers = childEndpoint.getAllClusterServers();
281
+ clusterServers.forEach((clusterServer) => {
282
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
283
+ if (clusterServer.name === 'EveHistory')
284
+ return;
285
+ let attributeValue;
286
+ try {
287
+ if (typeof value.getLocal() === 'object')
288
+ attributeValue = stringify(value.getLocal());
289
+ else
290
+ attributeValue = value.getLocal().toString();
291
+ }
292
+ catch (error) {
293
+ attributeValue = 'Fabric-Scoped';
294
+ this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
295
+ }
296
+ data.push({
297
+ endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''),
298
+ clusterName: clusterServer.name,
299
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
300
+ attributeName: key,
301
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
302
+ attributeValue,
303
+ });
304
+ });
305
+ });
306
+ });
307
+ }
308
+ });
309
+ res.json(data);
310
+ });
311
+ this.expressApp.get('/api/view-log', async (req, res) => {
312
+ this.log.debug('The frontend sent /api/log');
313
+ try {
314
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
315
+ res.type('text/plain');
316
+ res.send(data);
317
+ }
318
+ catch (error) {
319
+ this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
320
+ res.status(500).send('Error reading log file');
321
+ }
322
+ });
323
+ this.expressApp.get('/api/download-mblog', async (req, res) => {
324
+ this.log.debug('The frontend sent /api/download-mblog');
325
+ try {
326
+ await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
327
+ }
328
+ catch (error) {
329
+ fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
330
+ }
331
+ res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
332
+ if (error) {
333
+ this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
334
+ res.status(500).send('Error downloading the matterbridge log file');
335
+ }
336
+ });
337
+ });
338
+ this.expressApp.get('/api/download-mjlog', async (req, res) => {
339
+ this.log.debug('The frontend sent /api/download-mjlog');
340
+ try {
341
+ await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
342
+ }
343
+ catch (error) {
344
+ fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
345
+ }
346
+ res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
347
+ if (error) {
348
+ this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
349
+ res.status(500).send('Error downloading the matter log file');
350
+ }
351
+ });
352
+ });
353
+ this.expressApp.get('/api/download-mjstorage', async (req, res) => {
354
+ this.log.debug('The frontend sent /api/download-mjstorage');
355
+ await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
356
+ res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), `matterbridge.${this.matterbridge.matterStorageName}.zip`, (error) => {
357
+ if (error) {
358
+ this.log.error(`Error downloading the matter storage matterbridge.${this.matterbridge.matterStorageName}.zip: ${error instanceof Error ? error.message : error}`);
359
+ res.status(500).send('Error downloading the matter storage zip file');
360
+ }
361
+ });
362
+ });
363
+ this.expressApp.get('/api/download-mbstorage', async (req, res) => {
364
+ this.log.debug('The frontend sent /api/download-mbstorage');
365
+ await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
366
+ res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
367
+ if (error) {
368
+ this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
369
+ res.status(500).send('Error downloading the matterbridge storage file');
370
+ }
371
+ });
372
+ });
373
+ this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
374
+ this.log.debug('The frontend sent /api/download-pluginstorage');
375
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
376
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
377
+ if (error) {
378
+ this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
379
+ res.status(500).send('Error downloading the matterbridge plugin storage file');
380
+ }
381
+ });
382
+ });
383
+ this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
384
+ this.log.debug('The frontend sent /api/download-pluginconfig');
385
+ await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
386
+ res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
387
+ if (error) {
388
+ this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
389
+ res.status(500).send('Error downloading the matterbridge plugin storage file');
390
+ }
391
+ });
392
+ });
393
+ this.expressApp.get('/api/download-backup', async (req, res) => {
394
+ this.log.debug('The frontend sent /api/download-backup');
395
+ res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
396
+ if (error) {
397
+ this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
398
+ res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
399
+ }
400
+ });
401
+ });
402
+ this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
403
+ const command = req.params.command;
404
+ let param = req.params.param;
405
+ this.log.debug(`The frontend sent /api/command/${command}/${param}`);
406
+ if (!command) {
407
+ res.status(400).json({ error: 'No command provided' });
408
+ return;
409
+ }
410
+ this.log.debug(`Received frontend command: ${command}:${param}`);
411
+ if (command === 'setpassword') {
412
+ const password = param.slice(1, -1);
413
+ this.log.debug('setpassword', param, password);
414
+ await this.matterbridge.nodeContext?.set('password', password);
415
+ res.json({ message: 'Command received' });
416
+ return;
417
+ }
418
+ if (command === 'setbridgemode') {
419
+ this.log.debug(`setbridgemode: ${param}`);
420
+ this.wssSendRestartRequired();
421
+ await this.matterbridge.nodeContext?.set('bridgeMode', param);
422
+ res.json({ message: 'Command received' });
423
+ return;
424
+ }
425
+ if (command === 'backup') {
426
+ this.log.notice(`Prepairing the backup...`);
427
+ await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
428
+ this.log.notice(`Backup ready to be downloaded.`);
429
+ res.json({ message: 'Command received' });
430
+ return;
431
+ }
432
+ if (command === 'setmbloglevel') {
433
+ this.log.debug('Matterbridge log level:', param);
434
+ if (param === 'Debug') {
435
+ this.log.logLevel = "debug";
436
+ }
437
+ else if (param === 'Info') {
438
+ this.log.logLevel = "info";
439
+ }
440
+ else if (param === 'Notice') {
441
+ this.log.logLevel = "notice";
442
+ }
443
+ else if (param === 'Warn') {
444
+ this.log.logLevel = "warn";
445
+ }
446
+ else if (param === 'Error') {
447
+ this.log.logLevel = "error";
448
+ }
449
+ else if (param === 'Fatal') {
450
+ this.log.logLevel = "fatal";
451
+ }
452
+ await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
453
+ MatterbridgeEndpoint.logLevel = this.log.logLevel;
454
+ this.matterbridge.plugins.logLevel = this.log.logLevel;
455
+ for (const plugin of this.matterbridge.plugins) {
456
+ if (!plugin.platform || !plugin.platform.config)
457
+ continue;
458
+ plugin.platform.log.logLevel = plugin.platform.config.debug ? "debug" : this.log.logLevel;
459
+ await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? "debug" : this.log.logLevel);
460
+ }
461
+ res.json({ message: 'Command received' });
462
+ return;
463
+ }
464
+ if (command === 'setmjloglevel') {
465
+ this.log.debug('Matter.js log level:', param);
466
+ if (param === 'Debug') {
467
+ Logger.defaultLogLevel = MatterLogLevel.DEBUG;
468
+ }
469
+ else if (param === 'Info') {
470
+ Logger.defaultLogLevel = MatterLogLevel.INFO;
471
+ }
472
+ else if (param === 'Notice') {
473
+ Logger.defaultLogLevel = MatterLogLevel.NOTICE;
474
+ }
475
+ else if (param === 'Warn') {
476
+ Logger.defaultLogLevel = MatterLogLevel.WARN;
477
+ }
478
+ else if (param === 'Error') {
479
+ Logger.defaultLogLevel = MatterLogLevel.ERROR;
480
+ }
481
+ else if (param === 'Fatal') {
482
+ Logger.defaultLogLevel = MatterLogLevel.FATAL;
483
+ }
484
+ await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel);
485
+ res.json({ message: 'Command received' });
486
+ return;
487
+ }
488
+ if (command === 'setmdnsinterface') {
489
+ param = param.slice(1, -1);
490
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = param;
491
+ this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
492
+ await this.matterbridge.nodeContext?.set('mattermdnsinterface', param);
493
+ res.json({ message: 'Command received' });
494
+ return;
495
+ }
496
+ if (command === 'setipv4address') {
497
+ param = param.slice(1, -1);
498
+ this.matterbridge.matterbridgeInformation.matteripv4address = param;
499
+ this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
500
+ await this.matterbridge.nodeContext?.set('matteripv4address', param);
501
+ res.json({ message: 'Command received' });
502
+ return;
503
+ }
504
+ if (command === 'setipv6address') {
505
+ param = param.slice(1, -1);
506
+ this.matterbridge.matterbridgeInformation.matteripv6address = param;
507
+ this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
508
+ await this.matterbridge.nodeContext?.set('matteripv6address', param);
509
+ res.json({ message: 'Command received' });
510
+ return;
511
+ }
512
+ if (command === 'setmatterport') {
513
+ const port = Math.min(Math.max(parseInt(param), 5540), 5560);
514
+ this.matterbridge.matterbridgeInformation.matterPort = port;
515
+ this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
516
+ await this.matterbridge.nodeContext?.set('matterport', port);
517
+ res.json({ message: 'Command received' });
518
+ return;
519
+ }
520
+ if (command === 'setmatterdiscriminator') {
521
+ const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
522
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
523
+ this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
524
+ await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator);
525
+ res.json({ message: 'Command received' });
526
+ return;
527
+ }
528
+ if (command === 'setmatterpasscode') {
529
+ const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
530
+ this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
531
+ this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
532
+ await this.matterbridge.nodeContext?.set('matterpasscode', passcode);
533
+ res.json({ message: 'Command received' });
534
+ return;
535
+ }
536
+ if (command === 'setmblogfile') {
537
+ this.log.debug('Matterbridge file log:', param);
538
+ this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
539
+ await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
540
+ if (param === 'true')
541
+ AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug", true);
542
+ else
543
+ AnsiLogger.setGlobalLogfile(undefined);
544
+ res.json({ message: 'Command received' });
545
+ return;
546
+ }
547
+ if (command === 'setmjlogfile') {
548
+ this.log.debug('Matter file log:', param);
549
+ this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
550
+ await this.matterbridge.nodeContext?.set('matterFileLog', param === 'true');
551
+ if (param === 'true') {
552
+ try {
553
+ Logger.addLogger('matterfilelogger', await this.matterbridge.createMatterFileLogger(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), true), {
554
+ defaultLogLevel: MatterLogLevel.DEBUG,
555
+ logFormat: MatterLogFormat.PLAIN,
556
+ });
557
+ }
558
+ catch (error) {
559
+ 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}`);
560
+ }
561
+ }
562
+ else {
563
+ try {
564
+ Logger.removeLogger('matterfilelogger');
565
+ }
566
+ catch (error) {
567
+ 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}`);
568
+ }
569
+ }
570
+ res.json({ message: 'Command received' });
571
+ return;
572
+ }
573
+ if (command === 'unregister') {
574
+ await this.matterbridge.unregisterAndShutdownProcess();
575
+ res.json({ message: 'Command received' });
576
+ return;
577
+ }
578
+ if (command === 'reset') {
579
+ await this.matterbridge.shutdownProcessAndReset();
580
+ res.json({ message: 'Command received' });
581
+ return;
582
+ }
583
+ if (command === 'factoryreset') {
584
+ await this.matterbridge.shutdownProcessAndFactoryReset();
585
+ res.json({ message: 'Command received' });
586
+ return;
587
+ }
588
+ if (command === 'shutdown') {
589
+ await this.matterbridge.shutdownProcess();
590
+ res.json({ message: 'Command received' });
591
+ return;
592
+ }
593
+ if (command === 'restart') {
594
+ await this.matterbridge.restartProcess();
595
+ res.json({ message: 'Command received' });
596
+ return;
597
+ }
598
+ if (command === 'update') {
599
+ this.log.info('Updating matterbridge...');
600
+ try {
601
+ await this.matterbridge.spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
602
+ this.log.info('Matterbridge has been updated. Full restart required.');
603
+ }
604
+ catch (error) {
605
+ this.log.error('Error updating matterbridge');
606
+ }
607
+ await this.matterbridge.updateProcess();
608
+ this.wssSendRestartRequired();
609
+ res.json({ message: 'Command received' });
610
+ return;
611
+ }
612
+ if (command === 'saveconfig') {
613
+ param = param.replace(/\*/g, '\\');
614
+ this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
615
+ if (!this.matterbridge.plugins.has(param)) {
616
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
617
+ }
618
+ else {
619
+ const plugin = this.matterbridge.plugins.get(param);
620
+ if (!plugin)
621
+ return;
622
+ this.matterbridge.plugins.saveConfigFromJson(plugin, req.body);
623
+ }
624
+ this.wssSendRestartRequired();
625
+ res.json({ message: 'Command received' });
626
+ return;
627
+ }
628
+ if (command === 'installplugin') {
629
+ param = param.replace(/\*/g, '\\');
630
+ this.log.info(`Installing plugin ${plg}${param}${nf}...`);
631
+ try {
632
+ await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
633
+ this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
634
+ }
635
+ catch (error) {
636
+ this.log.error(`Error installing plugin ${plg}${param}${er}`);
637
+ }
638
+ this.wssSendRestartRequired();
639
+ param = param.split('@')[0];
640
+ if (param === 'matterbridge') {
641
+ res.json({ message: 'Command received' });
642
+ return;
643
+ }
644
+ }
645
+ if (command === 'addplugin' || command === 'installplugin') {
646
+ param = param.replace(/\*/g, '\\');
647
+ const plugin = await this.matterbridge.plugins.add(param);
648
+ if (plugin) {
649
+ if (this.matterbridge.bridgeMode === 'childbridge') {
650
+ this.matterbridge.createDynamicPlugin(plugin, true);
651
+ }
652
+ this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true);
653
+ }
654
+ res.json({ message: 'Command received' });
655
+ this.wssSendRefreshRequired();
656
+ return;
657
+ }
658
+ if (command === 'removeplugin') {
659
+ if (!this.matterbridge.plugins.has(param)) {
660
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
661
+ }
662
+ else {
663
+ const plugin = this.matterbridge.plugins.get(param);
664
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
665
+ await this.matterbridge.plugins.remove(param);
666
+ }
667
+ res.json({ message: 'Command received' });
668
+ this.wssSendRefreshRequired();
669
+ return;
670
+ }
671
+ if (command === 'enableplugin') {
672
+ if (!this.matterbridge.plugins.has(param)) {
673
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
674
+ }
675
+ else {
676
+ const plugin = this.matterbridge.plugins.get(param);
677
+ if (plugin && !plugin.enabled) {
678
+ plugin.locked = undefined;
679
+ plugin.error = undefined;
680
+ plugin.loaded = undefined;
681
+ plugin.started = undefined;
682
+ plugin.configured = undefined;
683
+ plugin.connected = undefined;
684
+ plugin.platform = undefined;
685
+ plugin.registeredDevices = undefined;
686
+ plugin.addedDevices = undefined;
687
+ await this.matterbridge.plugins.enable(param);
688
+ if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
689
+ this.matterbridge.createDynamicPlugin(plugin, true);
690
+ }
691
+ this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
692
+ }
693
+ }
694
+ res.json({ message: 'Command received' });
695
+ this.wssSendRefreshRequired();
696
+ return;
697
+ }
698
+ if (command === 'disableplugin') {
699
+ if (!this.matterbridge.plugins.has(param)) {
700
+ this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
701
+ }
702
+ else {
703
+ const plugin = this.matterbridge.plugins.get(param);
704
+ if (plugin && plugin.enabled) {
705
+ await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
706
+ await this.matterbridge.plugins.disable(param);
707
+ }
708
+ }
709
+ res.json({ message: 'Command received' });
710
+ this.wssSendRefreshRequired();
711
+ return;
712
+ }
713
+ });
714
+ this.expressApp.get('*', (req, res) => {
715
+ this.log.debug('The frontend sent:', req.url);
716
+ this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
717
+ res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
718
+ });
719
+ this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
720
+ }
721
+ async stop() {
722
+ if (this.httpServer) {
723
+ this.httpServer.close();
724
+ this.httpServer.removeAllListeners();
725
+ this.httpServer = undefined;
726
+ this.log.debug('Frontend http server closed successfully');
727
+ }
728
+ if (this.httpsServer) {
729
+ this.httpsServer.close();
730
+ this.httpsServer.removeAllListeners();
731
+ this.httpsServer = undefined;
732
+ this.log.debug('Frontend https server closed successfully');
733
+ }
734
+ if (this.expressApp) {
735
+ this.expressApp.removeAllListeners();
736
+ this.expressApp = undefined;
737
+ this.log.debug('Frontend app closed successfully');
738
+ }
739
+ if (this.webSocketServer) {
740
+ this.webSocketServer.clients.forEach((client) => {
741
+ if (client.readyState === WebSocket.OPEN) {
742
+ client.close();
743
+ }
744
+ });
745
+ this.webSocketServer.close((error) => {
746
+ if (error) {
747
+ this.log.error(`Error closing WebSocket server: ${error}`);
748
+ }
749
+ else {
750
+ this.log.debug('WebSocket server closed successfully');
751
+ }
752
+ });
753
+ this.webSocketServer = undefined;
754
+ }
755
+ }
756
+ getClusterTextFromDevice(device) {
757
+ const getAttribute = (device, cluster, attribute) => {
758
+ let value = undefined;
759
+ Object.entries(device.state)
760
+ .filter(([clusterName]) => clusterName.toLowerCase() === cluster.toLowerCase())
761
+ .forEach(([, clusterAttributes]) => {
762
+ Object.entries(clusterAttributes)
763
+ .filter(([attributeName]) => attributeName.toLowerCase() === attribute.toLowerCase())
764
+ .forEach(([, attributeValue]) => {
765
+ value = attributeValue;
766
+ });
767
+ });
768
+ if (value === undefined)
769
+ this.log.error(`Cluster ${cluster} or attribute ${attribute} not found in device ${device.deviceName}`);
770
+ return value;
771
+ };
772
+ const getUserLabel = (device) => {
773
+ const labelList = getAttribute(device, 'userLabel', 'labelList');
774
+ if (!labelList)
775
+ return;
776
+ const composed = labelList.find((entry) => entry.label === 'composed');
777
+ if (composed)
778
+ return 'Composed: ' + composed.value;
779
+ else
780
+ return '';
781
+ };
782
+ const getFixedLabel = (device) => {
783
+ const labelList = getAttribute(device, 'fixedLabel', 'labelList');
784
+ if (!labelList)
785
+ return;
786
+ const composed = labelList.find((entry) => entry.label === 'composed');
787
+ if (composed)
788
+ return 'Composed: ' + composed.value;
789
+ else
790
+ return '';
791
+ };
792
+ let attributes = '';
793
+ Object.entries(device.state).forEach(([clusterName, clusterAttributes]) => {
794
+ Object.entries(clusterAttributes).forEach(([attributeName, attributeValue]) => {
795
+ if (clusterName === 'onOff' && attributeName === 'onOff')
796
+ attributes += `OnOff: ${attributeValue} `;
797
+ if (clusterName === 'switch' && attributeName === 'currentPosition')
798
+ attributes += `Position: ${attributeValue} `;
799
+ if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths')
800
+ attributes += `Cover position: ${attributeValue / 100}% `;
801
+ if (clusterName === 'doorLock' && attributeName === 'lockState')
802
+ attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
803
+ if (clusterName === 'thermostat' && attributeName === 'localTemperature')
804
+ attributes += `Temperature: ${attributeValue / 100}°C `;
805
+ if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint')
806
+ attributes += `Heat to: ${attributeValue / 100}°C `;
807
+ if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint')
808
+ attributes += `Cool to: ${attributeValue / 100}°C `;
809
+ if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
810
+ attributes += `Mode: ${attributeValue} `;
811
+ if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
812
+ attributes += `State: ${attributeValue} `;
813
+ if (clusterName === 'levelControl' && attributeName === 'currentLevel')
814
+ attributes += `Level: ${attributeValue}% `;
815
+ if (clusterName === 'colorControl' && attributeName === 'colorMode')
816
+ attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
817
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue')
818
+ attributes += `Hue: ${Math.round(attributeValue)} `;
819
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation')
820
+ attributes += `Saturation: ${Math.round(attributeValue)} `;
821
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX')
822
+ attributes += `X: ${Math.round(attributeValue)} `;
823
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY')
824
+ attributes += `Y: ${Math.round(attributeValue)} `;
825
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds')
826
+ attributes += `ColorTemp: ${Math.round(attributeValue)} `;
827
+ if (clusterName === 'booleanState' && attributeName === 'stateValue')
828
+ attributes += `Contact: ${attributeValue} `;
829
+ if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive')
830
+ attributes += `Active alarms: ${stringify(attributeValue)} `;
831
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
832
+ attributes += `Smoke: ${attributeValue} `;
833
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
834
+ attributes += `Co: ${attributeValue} `;
835
+ if (clusterName === 'fanControl' && attributeName === 'fanMode')
836
+ attributes += `Mode: ${attributeValue} `;
837
+ if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
838
+ attributes += `Percent: ${attributeValue} `;
839
+ if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
840
+ attributes += `Speed: ${attributeValue} `;
841
+ if (clusterName === 'occupancySensing' && attributeName === 'occupancy')
842
+ attributes += `Occupancy: ${attributeValue.occupied} `;
843
+ if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue')
844
+ attributes += `Illuminance: ${attributeValue} `;
845
+ if (clusterName === 'airQuality' && attributeName === 'airQuality')
846
+ attributes += `Air quality: ${attributeValue} `;
847
+ if (clusterName === 'tvocMeasurement' && attributeName === 'measuredValue')
848
+ attributes += `Voc: ${attributeValue} `;
849
+ if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue')
850
+ attributes += `Temperature: ${attributeValue / 100}°C `;
851
+ if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue')
852
+ attributes += `Humidity: ${attributeValue / 100}% `;
853
+ if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
854
+ attributes += `Pressure: ${attributeValue} `;
855
+ if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
856
+ attributes += `Flow: ${attributeValue} `;
857
+ if (clusterName === 'fixedLabel' && attributeName === 'labelList')
858
+ attributes += `${getFixedLabel(device)} `;
859
+ if (clusterName === 'userLabel' && attributeName === 'labelList')
860
+ attributes += `${getUserLabel(device)} `;
861
+ });
862
+ });
863
+ return attributes.trimStart().trimEnd();
864
+ }
865
+ async getBaseRegisteredPlugins() {
866
+ const baseRegisteredPlugins = [];
867
+ for (const plugin of this.matterbridge.plugins) {
868
+ baseRegisteredPlugins.push({
869
+ path: plugin.path,
870
+ type: plugin.type,
871
+ name: plugin.name,
872
+ version: plugin.version,
873
+ description: plugin.description,
874
+ author: plugin.author,
875
+ latestVersion: plugin.latestVersion,
876
+ locked: plugin.locked,
877
+ error: plugin.error,
878
+ enabled: plugin.enabled,
879
+ loaded: plugin.loaded,
880
+ started: plugin.started,
881
+ configured: plugin.configured,
882
+ paired: plugin.paired,
883
+ connected: plugin.connected,
884
+ fabricInformations: plugin.fabricInformations,
885
+ sessionInformations: plugin.sessionInformations,
886
+ registeredDevices: plugin.registeredDevices,
887
+ addedDevices: plugin.addedDevices,
888
+ qrPairingCode: plugin.qrPairingCode,
889
+ manualPairingCode: plugin.manualPairingCode,
890
+ configJson: plugin.configJson,
891
+ schemaJson: plugin.schemaJson,
892
+ });
893
+ }
894
+ return baseRegisteredPlugins;
895
+ }
896
+ async wsMessageHandler(client, message) {
897
+ let data;
898
+ try {
899
+ data = JSON.parse(message.toString());
900
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || !isValidObject(data.params) || data.dst !== 'Matterbridge') {
901
+ this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
902
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' }));
903
+ return;
904
+ }
905
+ this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
906
+ if (data.method === 'ping') {
907
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: 'pong' }));
908
+ return;
909
+ }
910
+ else if (data.method === '/api/login') {
911
+ if (!this.matterbridge.nodeContext) {
912
+ this.log.error('Login nodeContext not found');
913
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: nodeContext not found' }));
914
+ return;
915
+ }
916
+ const storedPassword = await this.matterbridge.nodeContext.get('password', '');
917
+ if (storedPassword === '' || storedPassword === data.params.password) {
918
+ this.log.debug('Login password valid');
919
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: { valid: true } }));
920
+ return;
921
+ }
922
+ else {
923
+ this.log.debug('Error wrong password');
924
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' }));
925
+ return;
926
+ }
927
+ }
928
+ else if (data.method === '/api/install') {
929
+ if (!isValidString(data.params.packageName, 10)) {
930
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/install' }));
931
+ return;
932
+ }
933
+ this.matterbridge
934
+ .spawnCommand('npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
935
+ .then((response) => {
936
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
937
+ })
938
+ .catch((error) => {
939
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
940
+ });
941
+ return;
942
+ }
943
+ else if (data.method === '/api/uninstall') {
944
+ if (!isValidString(data.params.packageName, 10)) {
945
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
946
+ return;
947
+ }
948
+ this.matterbridge
949
+ .spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
950
+ .then((response) => {
951
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
952
+ })
953
+ .catch((error) => {
954
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
955
+ });
956
+ return;
957
+ }
958
+ else if (data.method === '/api/restart') {
959
+ await this.matterbridge.restartProcess();
960
+ return;
961
+ }
962
+ else if (data.method === '/api/shutdown') {
963
+ await this.matterbridge.shutdownProcess();
964
+ return;
965
+ }
966
+ else if (data.method === '/api/settings') {
967
+ this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
968
+ this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
969
+ this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
970
+ this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
971
+ this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
972
+ this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
973
+ this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
974
+ this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
975
+ this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
976
+ this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
977
+ this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
978
+ this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
979
+ this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
980
+ this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
981
+ this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
982
+ this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
983
+ this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
984
+ const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
985
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
986
+ return;
987
+ }
988
+ else if (data.method === '/api/plugins') {
989
+ const response = await this.getBaseRegisteredPlugins();
990
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
991
+ return;
992
+ }
993
+ else if (data.method === '/api/devices') {
994
+ const devices = [];
995
+ this.matterbridge.devices.forEach(async (device) => {
996
+ if (data.params.pluginName && data.params.pluginName !== device.plugin)
997
+ return;
998
+ if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
999
+ return;
1000
+ const cluster = this.getClusterTextFromDevice(device);
1001
+ devices.push({
1002
+ pluginName: device.plugin,
1003
+ type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
1004
+ endpoint: device.number,
1005
+ name: device.deviceName,
1006
+ serial: device.serialNumber,
1007
+ productUrl: device.productUrl,
1008
+ configUrl: device.configUrl,
1009
+ uniqueId: device.uniqueId,
1010
+ cluster: cluster,
1011
+ });
1012
+ });
1013
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: devices }));
1014
+ return;
1015
+ }
1016
+ else if (data.method === '/api/clusters') {
1017
+ if (!isValidString(data.params.plugin, 10)) {
1018
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' }));
1019
+ return;
1020
+ }
1021
+ if (!isValidNumber(data.params.endpoint, 1)) {
1022
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' }));
1023
+ return;
1024
+ }
1025
+ const clusters = [];
1026
+ let deviceName = '';
1027
+ let serialNumber = '';
1028
+ let deviceTypes = [];
1029
+ this.matterbridge.devices.forEach(async (device) => {
1030
+ if (data.params.plugin !== device.plugin)
1031
+ return;
1032
+ if (data.params.endpoint !== device.number)
1033
+ return;
1034
+ deviceName = device.deviceName ?? 'Unknown';
1035
+ serialNumber = device.serialNumber ?? 'Unknown';
1036
+ deviceTypes = [];
1037
+ const endpointServer = EndpointServer.forEndpoint(device);
1038
+ const clusterServers = endpointServer.getAllClusterServers();
1039
+ clusterServers.forEach((clusterServer) => {
1040
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
1041
+ if (clusterServer.name === 'EveHistory')
1042
+ return;
1043
+ if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
1044
+ value.getLocal().forEach((deviceType) => {
1045
+ deviceTypes.push(deviceType.deviceType);
1046
+ });
1047
+ }
1048
+ let attributeValue;
1049
+ let attributeLocalValue;
1050
+ try {
1051
+ if (typeof value.getLocal() === 'object')
1052
+ attributeValue = stringify(value.getLocal());
1053
+ else
1054
+ attributeValue = value.getLocal().toString();
1055
+ attributeLocalValue = value.getLocal();
1056
+ }
1057
+ catch (error) {
1058
+ attributeValue = 'Fabric-Scoped';
1059
+ attributeLocalValue = 'Fabric-Scoped';
1060
+ this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
1061
+ }
1062
+ clusters.push({
1063
+ endpoint: device.number ? device.number.toString() : '...',
1064
+ id: 'main',
1065
+ deviceTypes,
1066
+ clusterName: clusterServer.name,
1067
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
1068
+ attributeName: key,
1069
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
1070
+ attributeValue,
1071
+ attributeLocalValue,
1072
+ });
1073
+ });
1074
+ });
1075
+ endpointServer.getChildEndpoints().forEach((childEndpoint) => {
1076
+ deviceTypes = [];
1077
+ const name = childEndpoint.endpoint?.id;
1078
+ const clusterServers = childEndpoint.getAllClusterServers();
1079
+ clusterServers.forEach((clusterServer) => {
1080
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
1081
+ if (clusterServer.name === 'EveHistory')
1082
+ return;
1083
+ if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
1084
+ value.getLocal().forEach((deviceType) => {
1085
+ deviceTypes.push(deviceType.deviceType);
1086
+ });
1087
+ }
1088
+ let attributeValue;
1089
+ let attributeLocalValue;
1090
+ try {
1091
+ if (typeof value.getLocal() === 'object')
1092
+ attributeValue = stringify(value.getLocal());
1093
+ else
1094
+ attributeValue = value.getLocal().toString();
1095
+ attributeLocalValue = value.getLocal();
1096
+ }
1097
+ catch (error) {
1098
+ attributeValue = 'Fabric-Scoped';
1099
+ attributeLocalValue = 'Fabric-Scoped';
1100
+ this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
1101
+ }
1102
+ clusters.push({
1103
+ endpoint: childEndpoint.number ? childEndpoint.number.toString() : '...',
1104
+ id: name,
1105
+ deviceTypes,
1106
+ clusterName: clusterServer.name,
1107
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
1108
+ attributeName: key,
1109
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
1110
+ attributeValue,
1111
+ attributeLocalValue,
1112
+ });
1113
+ });
1114
+ });
1115
+ });
1116
+ });
1117
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, deviceName, serialNumber, endpoint: data.params.endpoint, deviceTypes, response: clusters }));
1118
+ return;
1119
+ }
1120
+ else if (data.method === '/api/select') {
1121
+ if (!isValidString(data.params.plugin, 10)) {
1122
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select' }));
1123
+ return;
1124
+ }
1125
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1126
+ if (!plugin) {
1127
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select' }));
1128
+ return;
1129
+ }
1130
+ const selectDeviceValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectDevice.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1131
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectDeviceValues }));
1132
+ return;
1133
+ }
1134
+ else if (data.method === '/api/select/entities') {
1135
+ if (!isValidString(data.params.plugin, 10)) {
1136
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/entities' }));
1137
+ return;
1138
+ }
1139
+ const plugin = this.matterbridge.plugins.get(data.params.plugin);
1140
+ if (!plugin) {
1141
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' }));
1142
+ return;
1143
+ }
1144
+ const selectEntityValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectEntity.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
1145
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectEntityValues }));
1146
+ return;
1147
+ }
1148
+ else {
1149
+ this.log.error(`Invalid method from websocket client: ${debugStringify(data)}`);
1150
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid method' }));
1151
+ return;
1152
+ }
1153
+ }
1154
+ catch (error) {
1155
+ this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1156
+ return;
1157
+ }
1158
+ }
1159
+ wssSendMessage(level, time, name, message) {
1160
+ if (!level || !time || !name || !message)
1161
+ return;
1162
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
1163
+ message = message.replace(/^\*+/, '');
1164
+ message = message.replace(/[\t\n]/g, '');
1165
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
1166
+ message = message.replace(/\\"/g, '"');
1167
+ const maxContinuousLength = 100;
1168
+ const keepStartLength = 20;
1169
+ const keepEndLength = 20;
1170
+ message = message
1171
+ .split(' ')
1172
+ .map((word) => {
1173
+ if (word.length > maxContinuousLength) {
1174
+ return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
1175
+ }
1176
+ return word;
1177
+ })
1178
+ .join(' ');
1179
+ this.webSocketServer?.clients.forEach((client) => {
1180
+ if (client.readyState === WebSocket.OPEN) {
1181
+ client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
1182
+ }
1183
+ });
1184
+ }
1185
+ wssSendRefreshRequired() {
1186
+ this.log.debug('Sending a refresh required message to all connected clients');
1187
+ this.matterbridge.matterbridgeInformation.refreshRequired = true;
1188
+ this.webSocketServer?.clients.forEach((client) => {
1189
+ if (client.readyState === WebSocket.OPEN) {
1190
+ client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
1191
+ }
1192
+ });
1193
+ }
1194
+ wssSendRestartRequired() {
1195
+ this.log.debug('Sending a restart required message to all connected clients');
1196
+ this.matterbridge.matterbridgeInformation.restartRequired = true;
1197
+ this.webSocketServer?.clients.forEach((client) => {
1198
+ if (client.readyState === WebSocket.OPEN) {
1199
+ client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
1200
+ }
1201
+ });
1202
+ }
1203
+ }