matterbridge 3.0.3-dev-20250519-e6e7257 → 3.0.3
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.
- package/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -2
- package/dist/cli.js.map +1 -0
- package/dist/cluster/export.d.ts +2 -0
- package/dist/cluster/export.d.ts.map +1 -0
- package/dist/cluster/export.js +2 -0
- package/dist/cluster/export.js.map +1 -0
- package/dist/defaultConfigSchema.d.ts +27 -0
- package/dist/defaultConfigSchema.d.ts.map +1 -0
- package/dist/defaultConfigSchema.js +23 -0
- package/dist/defaultConfigSchema.js.map +1 -0
- package/dist/deviceManager.d.ts +114 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +94 -1
- package/dist/deviceManager.js.map +1 -0
- package/dist/frontend.d.ts +241 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +333 -15
- package/dist/frontend.js.map +1 -0
- package/dist/helpers.d.ts +47 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +50 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -0
- package/dist/logger/export.d.ts +2 -0
- package/dist/logger/export.d.ts.map +1 -0
- package/dist/logger/export.js +1 -0
- package/dist/logger/export.js.map +1 -0
- package/dist/matter/behaviors.d.ts +2 -0
- package/dist/matter/behaviors.d.ts.map +1 -0
- package/dist/matter/behaviors.js +2 -0
- package/dist/matter/behaviors.js.map +1 -0
- package/dist/matter/clusters.d.ts +2 -0
- package/dist/matter/clusters.d.ts.map +1 -0
- package/dist/matter/clusters.js +2 -0
- package/dist/matter/clusters.js.map +1 -0
- package/dist/matter/devices.d.ts +2 -0
- package/dist/matter/devices.d.ts.map +1 -0
- package/dist/matter/devices.js +2 -0
- package/dist/matter/devices.js.map +1 -0
- package/dist/matter/endpoints.d.ts +2 -0
- package/dist/matter/endpoints.d.ts.map +1 -0
- package/dist/matter/endpoints.js +2 -0
- package/dist/matter/endpoints.js.map +1 -0
- package/dist/matter/export.d.ts +5 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +2 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/types.d.ts +3 -0
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/types.js +2 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matterbridge.d.ts +445 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +747 -47
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +40 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +34 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +1201 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +60 -4
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +629 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +563 -15
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +40 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +34 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +967 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +807 -11
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2728 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +149 -10
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +294 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +225 -7
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +188 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +24 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/pluginManager.d.ts +273 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +264 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/roboticVacuumCleaner.d.ts +82 -0
- package/dist/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/roboticVacuumCleaner.js +78 -3
- package/dist/roboticVacuumCleaner.js.map +1 -0
- package/dist/shelly.d.ts +153 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +155 -7
- package/dist/shelly.js.map +1 -0
- package/dist/storage/export.d.ts +2 -0
- package/dist/storage/export.d.ts.map +1 -0
- package/dist/storage/export.js +1 -0
- package/dist/storage/export.js.map +1 -0
- package/dist/update.d.ts +58 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +53 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/colorUtils.d.ts +61 -0
- package/dist/utils/colorUtils.d.ts.map +1 -0
- package/dist/utils/colorUtils.js +205 -2
- package/dist/utils/colorUtils.js.map +1 -0
- package/dist/utils/commandLine.d.ts +58 -0
- package/dist/utils/commandLine.d.ts.map +1 -0
- package/dist/utils/commandLine.js +53 -0
- package/dist/utils/commandLine.js.map +1 -0
- package/dist/utils/copyDirectory.d.ts +32 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +37 -1
- package/dist/utils/copyDirectory.js.map +1 -0
- package/dist/utils/createZip.d.ts +38 -0
- package/dist/utils/createZip.d.ts.map +1 -0
- package/dist/utils/createZip.js +42 -2
- package/dist/utils/createZip.js.map +1 -0
- package/dist/utils/deepCopy.d.ts +31 -0
- package/dist/utils/deepCopy.d.ts.map +1 -0
- package/dist/utils/deepCopy.js +38 -0
- package/dist/utils/deepCopy.js.map +1 -0
- package/dist/utils/deepEqual.d.ts +53 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/deepEqual.js +71 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +11 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +1 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/utils/hex.d.ts +48 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/hex.js +57 -0
- package/dist/utils/hex.js.map +1 -0
- package/dist/utils/isvalid.d.ts +102 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +100 -0
- package/dist/utils/isvalid.js.map +1 -0
- package/dist/utils/network.d.ts +69 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +76 -5
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/wait.d.ts +52 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +58 -9
- package/dist/utils/wait.js.map +1 -0
- package/dist/waterHeater.d.ts +75 -0
- package/dist/waterHeater.d.ts.map +1 -0
- package/dist/waterHeater.js +52 -0
- package/dist/waterHeater.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/frontend.js
CHANGED
|
@@ -1,28 +1,111 @@
|
|
|
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
|
|
1
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 'node:https';
|
|
4
28
|
import os from 'node:os';
|
|
5
29
|
import path from 'node:path';
|
|
6
30
|
import { promises as fs } from 'node:fs';
|
|
31
|
+
// Third-party modules
|
|
7
32
|
import express from 'express';
|
|
8
33
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
9
34
|
import multer from 'multer';
|
|
35
|
+
// AnsiLogger module
|
|
10
36
|
import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from './logger/export.js';
|
|
37
|
+
// Matterbridge
|
|
11
38
|
import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout } from './utils/export.js';
|
|
12
39
|
import { plg } from './matterbridgeTypes.js';
|
|
13
40
|
import { hasParameter } from './utils/export.js';
|
|
14
41
|
import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
|
|
42
|
+
/**
|
|
43
|
+
* Websocket message ID for logging.
|
|
44
|
+
* @constant {number}
|
|
45
|
+
*/
|
|
15
46
|
export const WS_ID_LOG = 0;
|
|
47
|
+
/**
|
|
48
|
+
* Websocket message ID indicating a refresh is needed.
|
|
49
|
+
* @constant {number}
|
|
50
|
+
*/
|
|
16
51
|
export const WS_ID_REFRESH_NEEDED = 1;
|
|
52
|
+
/**
|
|
53
|
+
* Websocket message ID indicating a restart is needed.
|
|
54
|
+
* @constant {number}
|
|
55
|
+
*/
|
|
17
56
|
export const WS_ID_RESTART_NEEDED = 2;
|
|
57
|
+
/**
|
|
58
|
+
* Websocket message ID indicating a cpu update.
|
|
59
|
+
* @constant {number}
|
|
60
|
+
*/
|
|
18
61
|
export const WS_ID_CPU_UPDATE = 3;
|
|
62
|
+
/**
|
|
63
|
+
* Websocket message ID indicating a memory update.
|
|
64
|
+
* @constant {number}
|
|
65
|
+
*/
|
|
19
66
|
export const WS_ID_MEMORY_UPDATE = 4;
|
|
67
|
+
/**
|
|
68
|
+
* Websocket message ID indicating an uptime update.
|
|
69
|
+
* @constant {number}
|
|
70
|
+
*/
|
|
20
71
|
export const WS_ID_UPTIME_UPDATE = 5;
|
|
72
|
+
/**
|
|
73
|
+
* Websocket message ID indicating a snackbar message.
|
|
74
|
+
* @constant {number}
|
|
75
|
+
*/
|
|
21
76
|
export const WS_ID_SNACKBAR = 6;
|
|
77
|
+
/**
|
|
78
|
+
* Websocket message ID indicating matterbridge has un update available.
|
|
79
|
+
* @constant {number}
|
|
80
|
+
*/
|
|
22
81
|
export const WS_ID_UPDATE_NEEDED = 7;
|
|
82
|
+
/**
|
|
83
|
+
* Websocket message ID indicating a state update.
|
|
84
|
+
* @constant {number}
|
|
85
|
+
*/
|
|
23
86
|
export const WS_ID_STATEUPDATE = 8;
|
|
87
|
+
/**
|
|
88
|
+
* Websocket message ID indicating to close a permanent snackbar message.
|
|
89
|
+
* @constant {number}
|
|
90
|
+
*/
|
|
24
91
|
export const WS_ID_CLOSE_SNACKBAR = 9;
|
|
92
|
+
/**
|
|
93
|
+
* Websocket message ID indicating a shelly system update.
|
|
94
|
+
* check:
|
|
95
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/check
|
|
96
|
+
* perform:
|
|
97
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/perform
|
|
98
|
+
* @constant {number}
|
|
99
|
+
*/
|
|
25
100
|
export const WS_ID_SHELLY_SYS_UPDATE = 100;
|
|
101
|
+
/**
|
|
102
|
+
* Websocket message ID indicating a shelly main update.
|
|
103
|
+
* check:
|
|
104
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/check
|
|
105
|
+
* perform:
|
|
106
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/perform
|
|
107
|
+
* @constant {number}
|
|
108
|
+
*/
|
|
26
109
|
export const WS_ID_SHELLY_MAIN_UPDATE = 101;
|
|
27
110
|
export class Frontend {
|
|
28
111
|
matterbridge;
|
|
@@ -35,7 +118,7 @@ export class Frontend {
|
|
|
35
118
|
webSocketServer;
|
|
36
119
|
constructor(matterbridge) {
|
|
37
120
|
this.matterbridge = matterbridge;
|
|
38
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
121
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
39
122
|
}
|
|
40
123
|
set logLevel(logLevel) {
|
|
41
124
|
this.log.logLevel = logLevel;
|
|
@@ -43,13 +126,43 @@ export class Frontend {
|
|
|
43
126
|
async start(port = 8283) {
|
|
44
127
|
this.port = port;
|
|
45
128
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
129
|
+
// Initialize multer with the upload directory
|
|
46
130
|
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
47
131
|
await fs.mkdir(uploadDir, { recursive: true });
|
|
48
132
|
const upload = multer({ dest: uploadDir });
|
|
133
|
+
// Create the express app that serves the frontend
|
|
49
134
|
this.expressApp = express();
|
|
135
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
136
|
+
/*
|
|
137
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
138
|
+
for (const method of methods) {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
142
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
143
|
+
try {
|
|
144
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
145
|
+
return original(path, ...rest);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
*/
|
|
153
|
+
// Log all requests to the server for debugging
|
|
154
|
+
/*
|
|
155
|
+
this.expressApp.use((req, res, next) => {
|
|
156
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
157
|
+
next();
|
|
158
|
+
});
|
|
159
|
+
*/
|
|
160
|
+
// Serve static files from '/static' endpoint
|
|
50
161
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
51
162
|
if (!hasParameter('ssl')) {
|
|
163
|
+
// Create an HTTP server and attach the express app
|
|
52
164
|
this.httpServer = createServer(this.expressApp);
|
|
165
|
+
// Listen on the specified port
|
|
53
166
|
if (hasParameter('ingress')) {
|
|
54
167
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
55
168
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -63,6 +176,7 @@ export class Frontend {
|
|
|
63
176
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
64
177
|
});
|
|
65
178
|
}
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
180
|
this.httpServer.on('error', (error) => {
|
|
67
181
|
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
68
182
|
switch (error.code) {
|
|
@@ -78,6 +192,7 @@ export class Frontend {
|
|
|
78
192
|
});
|
|
79
193
|
}
|
|
80
194
|
else {
|
|
195
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
81
196
|
let cert;
|
|
82
197
|
try {
|
|
83
198
|
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
@@ -105,7 +220,9 @@ export class Frontend {
|
|
|
105
220
|
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
106
221
|
}
|
|
107
222
|
const serverOptions = { cert, key, ca };
|
|
223
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
108
224
|
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
225
|
+
// Listen on the specified port
|
|
109
226
|
if (hasParameter('ingress')) {
|
|
110
227
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
111
228
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -119,6 +236,7 @@ export class Frontend {
|
|
|
119
236
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
120
237
|
});
|
|
121
238
|
}
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
240
|
this.httpsServer.on('error', (error) => {
|
|
123
241
|
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
124
242
|
switch (error.code) {
|
|
@@ -135,16 +253,18 @@ export class Frontend {
|
|
|
135
253
|
}
|
|
136
254
|
if (this.initializeError)
|
|
137
255
|
return;
|
|
256
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
138
257
|
const wssPort = this.port;
|
|
139
258
|
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
140
259
|
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
141
260
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
142
261
|
const clientIp = request.socket.remoteAddress;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
262
|
+
// Set the global logger callback for the WebSocketServer
|
|
263
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
264
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
265
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
266
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
267
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
148
268
|
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
|
|
149
269
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
150
270
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -178,6 +298,7 @@ export class Frontend {
|
|
|
178
298
|
this.webSocketServer.on('error', (ws, error) => {
|
|
179
299
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
180
300
|
});
|
|
301
|
+
// Subscribe to cli events
|
|
181
302
|
const { cliEmitter } = await import('./cli.js');
|
|
182
303
|
cliEmitter.removeAllListeners();
|
|
183
304
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
@@ -189,6 +310,7 @@ export class Frontend {
|
|
|
189
310
|
cliEmitter.on('cpu', (cpuUsage) => {
|
|
190
311
|
this.wssSendCpuUpdate(cpuUsage);
|
|
191
312
|
});
|
|
313
|
+
// Endpoint to validate login code
|
|
192
314
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
193
315
|
const { password } = req.body;
|
|
194
316
|
this.log.debug('The frontend sent /api/login', password);
|
|
@@ -207,23 +329,27 @@ export class Frontend {
|
|
|
207
329
|
this.log.warn('/api/login error wrong password');
|
|
208
330
|
res.json({ valid: false });
|
|
209
331
|
}
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
210
333
|
}
|
|
211
334
|
catch (error) {
|
|
212
335
|
this.log.error('/api/login error getting password');
|
|
213
336
|
res.json({ valid: false });
|
|
214
337
|
}
|
|
215
338
|
});
|
|
339
|
+
// Endpoint to provide health check for docker
|
|
216
340
|
this.expressApp.get('/health', (req, res) => {
|
|
217
341
|
this.log.debug('Express received /health');
|
|
218
342
|
const healthStatus = {
|
|
219
|
-
status: 'ok',
|
|
220
|
-
uptime: process.uptime(),
|
|
221
|
-
timestamp: new Date().toISOString(),
|
|
343
|
+
status: 'ok', // Indicate service is healthy
|
|
344
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
345
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
222
346
|
};
|
|
223
347
|
res.status(200).json(healthStatus);
|
|
224
348
|
});
|
|
349
|
+
// Endpoint to provide memory usage details
|
|
225
350
|
this.expressApp.get('/memory', async (req, res) => {
|
|
226
351
|
this.log.debug('Express received /memory');
|
|
352
|
+
// Memory usage from process
|
|
227
353
|
const memoryUsageRaw = process.memoryUsage();
|
|
228
354
|
const memoryUsage = {
|
|
229
355
|
rss: this.formatMemoryUsage(memoryUsageRaw.rss),
|
|
@@ -232,10 +358,13 @@ export class Frontend {
|
|
|
232
358
|
external: this.formatMemoryUsage(memoryUsageRaw.external),
|
|
233
359
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
234
360
|
};
|
|
361
|
+
// V8 heap statistics
|
|
235
362
|
const { default: v8 } = await import('node:v8');
|
|
236
363
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
237
364
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
365
|
+
// Format heapStats
|
|
238
366
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
|
|
367
|
+
// Format heapSpaces
|
|
239
368
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
240
369
|
...space,
|
|
241
370
|
space_size: this.formatMemoryUsage(space.space_size),
|
|
@@ -253,19 +382,23 @@ export class Frontend {
|
|
|
253
382
|
};
|
|
254
383
|
res.status(200).json(memoryReport);
|
|
255
384
|
});
|
|
385
|
+
// Endpoint to provide settings
|
|
256
386
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
257
387
|
this.log.debug('The frontend sent /api/settings');
|
|
258
388
|
res.json(await this.getApiSettings());
|
|
259
389
|
});
|
|
390
|
+
// Endpoint to provide plugins
|
|
260
391
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
261
392
|
this.log.debug('The frontend sent /api/plugins');
|
|
262
393
|
res.json(this.getBaseRegisteredPlugins());
|
|
263
394
|
});
|
|
395
|
+
// Endpoint to provide devices
|
|
264
396
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
265
397
|
this.log.debug('The frontend sent /api/devices');
|
|
266
398
|
const devices = await this.getDevices();
|
|
267
399
|
res.json(devices);
|
|
268
400
|
});
|
|
401
|
+
// Endpoint to view the matterbridge log
|
|
269
402
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
270
403
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
271
404
|
try {
|
|
@@ -278,6 +411,7 @@ export class Frontend {
|
|
|
278
411
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
279
412
|
}
|
|
280
413
|
});
|
|
414
|
+
// Endpoint to view the matter.js log
|
|
281
415
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
282
416
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
283
417
|
try {
|
|
@@ -290,6 +424,7 @@ export class Frontend {
|
|
|
290
424
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
291
425
|
}
|
|
292
426
|
});
|
|
427
|
+
// Endpoint to view the shelly log
|
|
293
428
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
294
429
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
295
430
|
try {
|
|
@@ -302,6 +437,7 @@ export class Frontend {
|
|
|
302
437
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
303
438
|
}
|
|
304
439
|
});
|
|
440
|
+
// Endpoint to download the matterbridge log
|
|
305
441
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
306
442
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
307
443
|
try {
|
|
@@ -314,6 +450,7 @@ export class Frontend {
|
|
|
314
450
|
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
315
451
|
}
|
|
316
452
|
res.type('text/plain');
|
|
453
|
+
// res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
317
454
|
res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
318
455
|
if (error) {
|
|
319
456
|
this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
@@ -321,6 +458,7 @@ export class Frontend {
|
|
|
321
458
|
}
|
|
322
459
|
});
|
|
323
460
|
});
|
|
461
|
+
// Endpoint to download the matter log
|
|
324
462
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
325
463
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
326
464
|
try {
|
|
@@ -340,6 +478,7 @@ export class Frontend {
|
|
|
340
478
|
}
|
|
341
479
|
});
|
|
342
480
|
});
|
|
481
|
+
// Endpoint to download the shelly log
|
|
343
482
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
344
483
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
345
484
|
try {
|
|
@@ -359,6 +498,7 @@ export class Frontend {
|
|
|
359
498
|
}
|
|
360
499
|
});
|
|
361
500
|
});
|
|
501
|
+
// Endpoint to download the matterbridge storage directory
|
|
362
502
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
363
503
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
364
504
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
@@ -369,6 +509,7 @@ export class Frontend {
|
|
|
369
509
|
}
|
|
370
510
|
});
|
|
371
511
|
});
|
|
512
|
+
// Endpoint to download the matter storage file
|
|
372
513
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
373
514
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
374
515
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
|
|
@@ -379,6 +520,7 @@ export class Frontend {
|
|
|
379
520
|
}
|
|
380
521
|
});
|
|
381
522
|
});
|
|
523
|
+
// Endpoint to download the matterbridge plugin directory
|
|
382
524
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
383
525
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
384
526
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
@@ -389,6 +531,7 @@ export class Frontend {
|
|
|
389
531
|
}
|
|
390
532
|
});
|
|
391
533
|
});
|
|
534
|
+
// Endpoint to download the matterbridge plugin config files
|
|
392
535
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
393
536
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
394
537
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
@@ -399,6 +542,7 @@ export class Frontend {
|
|
|
399
542
|
}
|
|
400
543
|
});
|
|
401
544
|
});
|
|
545
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
402
546
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
403
547
|
this.log.debug('The frontend sent /api/download-backup');
|
|
404
548
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
@@ -408,6 +552,7 @@ export class Frontend {
|
|
|
408
552
|
}
|
|
409
553
|
});
|
|
410
554
|
});
|
|
555
|
+
// Endpoint to upload a package
|
|
411
556
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
412
557
|
const { filename } = req.body;
|
|
413
558
|
const file = req.file;
|
|
@@ -417,10 +562,13 @@ export class Frontend {
|
|
|
417
562
|
return;
|
|
418
563
|
}
|
|
419
564
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
565
|
+
// Define the path where the plugin file will be saved
|
|
420
566
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
421
567
|
try {
|
|
568
|
+
// Move the uploaded file to the specified path
|
|
422
569
|
await fs.rename(file.path, filePath);
|
|
423
570
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
571
|
+
// Install the plugin package
|
|
424
572
|
if (filename.endsWith('.tgz')) {
|
|
425
573
|
await this.matterbridge.spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
|
|
426
574
|
this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
|
|
@@ -439,6 +587,7 @@ export class Frontend {
|
|
|
439
587
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
440
588
|
}
|
|
441
589
|
});
|
|
590
|
+
// Fallback for routing (must be the last route)
|
|
442
591
|
this.expressApp.use((req, res) => {
|
|
443
592
|
this.log.debug('The frontend sent:', req.url);
|
|
444
593
|
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
@@ -447,12 +596,15 @@ export class Frontend {
|
|
|
447
596
|
}
|
|
448
597
|
async stop() {
|
|
449
598
|
this.log.debug('Stopping the frontend...');
|
|
599
|
+
// Remove listeners from the express app
|
|
450
600
|
if (this.expressApp) {
|
|
451
601
|
this.expressApp.removeAllListeners();
|
|
452
602
|
this.expressApp = undefined;
|
|
453
603
|
this.log.debug('Frontend app closed successfully');
|
|
454
604
|
}
|
|
605
|
+
// Close the WebSocket server
|
|
455
606
|
if (this.webSocketServer) {
|
|
607
|
+
// Close all active connections
|
|
456
608
|
this.webSocketServer.clients.forEach((client) => {
|
|
457
609
|
if (client.readyState === WebSocket.OPEN) {
|
|
458
610
|
client.close();
|
|
@@ -472,6 +624,7 @@ export class Frontend {
|
|
|
472
624
|
this.webSocketServer.removeAllListeners();
|
|
473
625
|
this.webSocketServer = undefined;
|
|
474
626
|
}
|
|
627
|
+
// Close the http server
|
|
475
628
|
if (this.httpServer) {
|
|
476
629
|
await withTimeout(new Promise((resolve) => {
|
|
477
630
|
this.httpServer?.close((error) => {
|
|
@@ -488,6 +641,7 @@ export class Frontend {
|
|
|
488
641
|
this.httpServer = undefined;
|
|
489
642
|
this.log.debug('Frontend http server closed successfully');
|
|
490
643
|
}
|
|
644
|
+
// Close the https server
|
|
491
645
|
if (this.httpsServer) {
|
|
492
646
|
await withTimeout(new Promise((resolve) => {
|
|
493
647
|
this.httpsServer?.close((error) => {
|
|
@@ -506,6 +660,7 @@ export class Frontend {
|
|
|
506
660
|
}
|
|
507
661
|
this.log.debug('Frontend stopped successfully');
|
|
508
662
|
}
|
|
663
|
+
// Function to format bytes to KB, MB, or GB
|
|
509
664
|
formatMemoryUsage = (bytes) => {
|
|
510
665
|
if (bytes >= 1024 ** 3) {
|
|
511
666
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -517,6 +672,7 @@ export class Frontend {
|
|
|
517
672
|
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
518
673
|
}
|
|
519
674
|
};
|
|
675
|
+
// Function to format system uptime with only the most significant unit
|
|
520
676
|
formatOsUpTime = (seconds) => {
|
|
521
677
|
if (seconds >= 86400) {
|
|
522
678
|
const days = Math.floor(seconds / 86400);
|
|
@@ -532,8 +688,14 @@ export class Frontend {
|
|
|
532
688
|
}
|
|
533
689
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
534
690
|
};
|
|
691
|
+
/**
|
|
692
|
+
* Retrieves the api settings data.
|
|
693
|
+
*
|
|
694
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
695
|
+
*/
|
|
535
696
|
async getApiSettings() {
|
|
536
697
|
const { lastCpuUsage } = await import('./cli.js');
|
|
698
|
+
// Update the system information
|
|
537
699
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
538
700
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
539
701
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
|
|
@@ -542,6 +704,7 @@ export class Frontend {
|
|
|
542
704
|
this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
|
|
543
705
|
this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
|
|
544
706
|
this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
|
|
707
|
+
// Update the matterbridge information
|
|
545
708
|
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
546
709
|
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
547
710
|
this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
|
|
@@ -560,6 +723,11 @@ export class Frontend {
|
|
|
560
723
|
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
561
724
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
562
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* Retrieves the reachable attribute.
|
|
728
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
729
|
+
* @returns {boolean} The reachable attribute.
|
|
730
|
+
*/
|
|
563
731
|
getReachability(device) {
|
|
564
732
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
565
733
|
return false;
|
|
@@ -584,13 +752,20 @@ export class Frontend {
|
|
|
584
752
|
}
|
|
585
753
|
return;
|
|
586
754
|
};
|
|
755
|
+
// Root endpoint
|
|
587
756
|
if (device.hasClusterServer(PowerSource.Cluster.id))
|
|
588
757
|
return powerSource(device);
|
|
758
|
+
// Child endpoints
|
|
589
759
|
for (const child of device.getChildEndpoints()) {
|
|
590
760
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
591
761
|
return powerSource(child);
|
|
592
762
|
}
|
|
593
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Retrieves the cluster text description from a given device.
|
|
766
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
767
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
768
|
+
*/
|
|
594
769
|
getClusterTextFromDevice(device) {
|
|
595
770
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
596
771
|
return '';
|
|
@@ -632,6 +807,7 @@ export class Frontend {
|
|
|
632
807
|
let attributes = '';
|
|
633
808
|
let supportedModes = [];
|
|
634
809
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
810
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
635
811
|
if (typeof attributeValue === 'undefined')
|
|
636
812
|
return;
|
|
637
813
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -723,8 +899,13 @@ export class Frontend {
|
|
|
723
899
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
724
900
|
attributes += `${getUserLabel(device)} `;
|
|
725
901
|
});
|
|
902
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
726
903
|
return attributes.trimStart().trimEnd();
|
|
727
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
907
|
+
* @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
|
|
908
|
+
*/
|
|
728
909
|
getBaseRegisteredPlugins() {
|
|
729
910
|
const baseRegisteredPlugins = [];
|
|
730
911
|
for (const plugin of this.matterbridge.plugins) {
|
|
@@ -763,11 +944,18 @@ export class Frontend {
|
|
|
763
944
|
}
|
|
764
945
|
return baseRegisteredPlugins;
|
|
765
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Retrieves the devices from Matterbridge.
|
|
949
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
950
|
+
* @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices.
|
|
951
|
+
*/
|
|
766
952
|
async getDevices(pluginName) {
|
|
767
953
|
const devices = [];
|
|
768
954
|
this.matterbridge.devices.forEach(async (device) => {
|
|
955
|
+
// Filter by pluginName if provided
|
|
769
956
|
if (pluginName && pluginName !== device.plugin)
|
|
770
957
|
return;
|
|
958
|
+
// Check if the device has the required properties
|
|
771
959
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
772
960
|
return;
|
|
773
961
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -787,6 +975,13 @@ export class Frontend {
|
|
|
787
975
|
});
|
|
788
976
|
return devices;
|
|
789
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* Handles incoming websocket messages for the Matterbridge frontend.
|
|
980
|
+
*
|
|
981
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
982
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
983
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
984
|
+
*/
|
|
790
985
|
async wsMessageHandler(client, message) {
|
|
791
986
|
let data;
|
|
792
987
|
try {
|
|
@@ -833,8 +1028,10 @@ export class Frontend {
|
|
|
833
1028
|
this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
|
|
834
1029
|
const packageName = data.params.packageName.replace(/@.*$/, '');
|
|
835
1030
|
if (data.params.restart === false && packageName !== 'matterbridge') {
|
|
1031
|
+
// The install comes from InstallPlugins
|
|
836
1032
|
this.matterbridge.plugins.add(packageName).then((plugin) => {
|
|
837
1033
|
if (plugin) {
|
|
1034
|
+
// The plugin is not registered
|
|
838
1035
|
this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
|
|
839
1036
|
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
|
|
840
1037
|
this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
|
|
@@ -842,6 +1039,7 @@ export class Frontend {
|
|
|
842
1039
|
});
|
|
843
1040
|
}
|
|
844
1041
|
else {
|
|
1042
|
+
// The plugin is already registered
|
|
845
1043
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
846
1044
|
this.wssSendRefreshRequired('plugins');
|
|
847
1045
|
this.wssSendRestartRequired();
|
|
@@ -849,6 +1047,7 @@ export class Frontend {
|
|
|
849
1047
|
});
|
|
850
1048
|
}
|
|
851
1049
|
else {
|
|
1050
|
+
// The package is matterbridge
|
|
852
1051
|
if (this.matterbridge.restartMode !== '') {
|
|
853
1052
|
this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
|
|
854
1053
|
this.matterbridge.shutdownProcess();
|
|
@@ -870,6 +1069,7 @@ export class Frontend {
|
|
|
870
1069
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
|
|
871
1070
|
return;
|
|
872
1071
|
}
|
|
1072
|
+
// The package is a plugin
|
|
873
1073
|
const plugin = this.matterbridge.plugins.get(data.params.packageName);
|
|
874
1074
|
if (plugin) {
|
|
875
1075
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
@@ -878,6 +1078,7 @@ export class Frontend {
|
|
|
878
1078
|
this.wssSendRefreshRequired('plugins');
|
|
879
1079
|
this.wssSendRefreshRequired('devices');
|
|
880
1080
|
}
|
|
1081
|
+
// Uninstall the package
|
|
881
1082
|
this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
|
|
882
1083
|
this.matterbridge
|
|
883
1084
|
.spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
|
|
@@ -1154,6 +1355,7 @@ export class Frontend {
|
|
|
1154
1355
|
});
|
|
1155
1356
|
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
1156
1357
|
deviceTypes = [];
|
|
1358
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1157
1359
|
const name = childEndpoint.endpoint?.id;
|
|
1158
1360
|
const clusterServers = childEndpoint.getAllClusterServers();
|
|
1159
1361
|
clusterServers.forEach((clusterServer) => {
|
|
@@ -1267,22 +1469,22 @@ export class Frontend {
|
|
|
1267
1469
|
if (isValidString(data.params.value, 4)) {
|
|
1268
1470
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1269
1471
|
if (data.params.value === 'Debug') {
|
|
1270
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1472
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1271
1473
|
}
|
|
1272
1474
|
else if (data.params.value === 'Info') {
|
|
1273
|
-
await this.matterbridge.setLogLevel("info");
|
|
1475
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1274
1476
|
}
|
|
1275
1477
|
else if (data.params.value === 'Notice') {
|
|
1276
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1478
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1277
1479
|
}
|
|
1278
1480
|
else if (data.params.value === 'Warn') {
|
|
1279
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1481
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1280
1482
|
}
|
|
1281
1483
|
else if (data.params.value === 'Error') {
|
|
1282
|
-
await this.matterbridge.setLogLevel("error");
|
|
1484
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1283
1485
|
}
|
|
1284
1486
|
else if (data.params.value === 'Fatal') {
|
|
1285
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1487
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1286
1488
|
}
|
|
1287
1489
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1288
1490
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
|
|
@@ -1293,6 +1495,7 @@ export class Frontend {
|
|
|
1293
1495
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1294
1496
|
this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
|
|
1295
1497
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1498
|
+
// Create the file logger for matterbridge
|
|
1296
1499
|
if (data.params.value)
|
|
1297
1500
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
|
|
1298
1501
|
else
|
|
@@ -1457,15 +1660,19 @@ export class Frontend {
|
|
|
1457
1660
|
return;
|
|
1458
1661
|
}
|
|
1459
1662
|
const config = plugin.configJson;
|
|
1663
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1460
1664
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1665
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1461
1666
|
if (select === 'serial')
|
|
1462
1667
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1463
1668
|
if (select === 'name')
|
|
1464
1669
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1465
1670
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
1671
|
+
// Remove postfix from the serial if it exists
|
|
1466
1672
|
if (config.postfix) {
|
|
1467
1673
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1468
1674
|
}
|
|
1675
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1469
1676
|
if (isValidArray(config.whiteList, 1)) {
|
|
1470
1677
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1471
1678
|
config.whiteList.push(data.params.serial);
|
|
@@ -1474,6 +1681,7 @@ export class Frontend {
|
|
|
1474
1681
|
config.whiteList.push(data.params.name);
|
|
1475
1682
|
}
|
|
1476
1683
|
}
|
|
1684
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1477
1685
|
if (isValidArray(config.blackList, 1)) {
|
|
1478
1686
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1479
1687
|
config.blackList = config.blackList.filter((item) => item !== data.params.serial);
|
|
@@ -1503,7 +1711,9 @@ export class Frontend {
|
|
|
1503
1711
|
return;
|
|
1504
1712
|
}
|
|
1505
1713
|
const config = plugin.configJson;
|
|
1714
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1506
1715
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1716
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1507
1717
|
if (select === 'serial')
|
|
1508
1718
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1509
1719
|
if (select === 'name')
|
|
@@ -1512,6 +1722,7 @@ export class Frontend {
|
|
|
1512
1722
|
if (config.postfix) {
|
|
1513
1723
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1514
1724
|
}
|
|
1725
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1515
1726
|
if (isValidArray(config.whiteList, 1)) {
|
|
1516
1727
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1517
1728
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
|
|
@@ -1520,6 +1731,7 @@ export class Frontend {
|
|
|
1520
1731
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
|
|
1521
1732
|
}
|
|
1522
1733
|
}
|
|
1734
|
+
// Add the serial to the blackList
|
|
1523
1735
|
if (isValidArray(config.blackList)) {
|
|
1524
1736
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1525
1737
|
config.blackList.push(data.params.serial);
|
|
@@ -1552,114 +1764,219 @@ export class Frontend {
|
|
|
1552
1764
|
this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
|
|
1553
1765
|
}
|
|
1554
1766
|
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
1769
|
+
*
|
|
1770
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1771
|
+
* @param {string} time - The time string of the message
|
|
1772
|
+
* @param {string} name - The logger name of the message
|
|
1773
|
+
* @param {string} message - The content of the message.
|
|
1774
|
+
*
|
|
1775
|
+
* @remark
|
|
1776
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
1777
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
1778
|
+
* The function sends the message to all connected clients.
|
|
1779
|
+
*/
|
|
1555
1780
|
wssSendMessage(level, time, name, message) {
|
|
1556
1781
|
if (!level || !time || !name || !message)
|
|
1557
1782
|
return;
|
|
1783
|
+
// Remove ANSI escape codes from the message
|
|
1784
|
+
// eslint-disable-next-line no-control-regex
|
|
1558
1785
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1786
|
+
// Remove leading asterisks from the message
|
|
1559
1787
|
message = message.replace(/^\*+/, '');
|
|
1788
|
+
// Replace all occurrences of \t and \n
|
|
1560
1789
|
message = message.replace(/[\t\n]/g, '');
|
|
1790
|
+
// Remove non-printable characters
|
|
1791
|
+
// eslint-disable-next-line no-control-regex
|
|
1561
1792
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1793
|
+
// Replace all occurrences of \" with "
|
|
1562
1794
|
message = message.replace(/\\"/g, '"');
|
|
1795
|
+
// Replace all occurrences of angle-brackets with < and >"
|
|
1563
1796
|
message = message.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1797
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1564
1798
|
const maxContinuousLength = 100;
|
|
1565
1799
|
const keepStartLength = 20;
|
|
1566
1800
|
const keepEndLength = 20;
|
|
1801
|
+
// Split the message into words
|
|
1567
1802
|
message = message
|
|
1568
1803
|
.split(' ')
|
|
1569
1804
|
.map((word) => {
|
|
1805
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1570
1806
|
if (word.length > maxContinuousLength) {
|
|
1571
1807
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1572
1808
|
}
|
|
1573
1809
|
return word;
|
|
1574
1810
|
})
|
|
1575
1811
|
.join(' ');
|
|
1812
|
+
// Send the message to all connected clients
|
|
1576
1813
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1577
1814
|
if (client.readyState === WebSocket.OPEN) {
|
|
1578
1815
|
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1579
1816
|
}
|
|
1580
1817
|
});
|
|
1581
1818
|
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1821
|
+
*
|
|
1822
|
+
* @param {string} changed - The changed value. If null, the whole page will be refreshed.
|
|
1823
|
+
* possible values:
|
|
1824
|
+
* - 'matterbridgeLatestVersion'
|
|
1825
|
+
* - 'matterbridgeAdvertise'
|
|
1826
|
+
* - 'online'
|
|
1827
|
+
* - 'offline'
|
|
1828
|
+
* - 'reachability'
|
|
1829
|
+
* - 'settings'
|
|
1830
|
+
* - 'plugins'
|
|
1831
|
+
* - 'pluginsRestart'
|
|
1832
|
+
* - 'devices'
|
|
1833
|
+
* - 'fabrics'
|
|
1834
|
+
* - 'sessions'
|
|
1835
|
+
*/
|
|
1582
1836
|
wssSendRefreshRequired(changed = null) {
|
|
1583
1837
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1838
|
+
// Send the message to all connected clients
|
|
1584
1839
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1585
1840
|
if (client.readyState === WebSocket.OPEN) {
|
|
1586
1841
|
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
|
|
1587
1842
|
}
|
|
1588
1843
|
});
|
|
1589
1844
|
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1847
|
+
*
|
|
1848
|
+
*/
|
|
1590
1849
|
wssSendRestartRequired(snackbar = true) {
|
|
1591
1850
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
1592
1851
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1593
1852
|
if (snackbar === true)
|
|
1594
1853
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
1854
|
+
// Send the message to all connected clients
|
|
1595
1855
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1596
1856
|
if (client.readyState === WebSocket.OPEN) {
|
|
1597
1857
|
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1598
1858
|
}
|
|
1599
1859
|
});
|
|
1600
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
1863
|
+
*
|
|
1864
|
+
*/
|
|
1601
1865
|
wssSendUpdateRequired() {
|
|
1602
1866
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1603
1867
|
this.matterbridge.matterbridgeInformation.updateRequired = true;
|
|
1868
|
+
// Send the message to all connected clients
|
|
1604
1869
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1605
1870
|
if (client.readyState === WebSocket.OPEN) {
|
|
1606
1871
|
client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
|
|
1607
1872
|
}
|
|
1608
1873
|
});
|
|
1609
1874
|
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Sends a cpu update message to all connected clients.
|
|
1877
|
+
*
|
|
1878
|
+
*/
|
|
1610
1879
|
wssSendCpuUpdate(cpuUsage) {
|
|
1611
1880
|
if (hasParameter('debug'))
|
|
1612
1881
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
1882
|
+
// Send the message to all connected clients
|
|
1613
1883
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1614
1884
|
if (client.readyState === WebSocket.OPEN) {
|
|
1615
1885
|
client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
|
|
1616
1886
|
}
|
|
1617
1887
|
});
|
|
1618
1888
|
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Sends a memory update message to all connected clients.
|
|
1891
|
+
*
|
|
1892
|
+
*/
|
|
1619
1893
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1620
1894
|
if (hasParameter('debug'))
|
|
1621
1895
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
1896
|
+
// Send the message to all connected clients
|
|
1622
1897
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1623
1898
|
if (client.readyState === WebSocket.OPEN) {
|
|
1624
1899
|
client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
|
|
1625
1900
|
}
|
|
1626
1901
|
});
|
|
1627
1902
|
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Sends an uptime update message to all connected clients.
|
|
1905
|
+
*
|
|
1906
|
+
*/
|
|
1628
1907
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1629
1908
|
if (hasParameter('debug'))
|
|
1630
1909
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
1910
|
+
// Send the message to all connected clients
|
|
1631
1911
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1632
1912
|
if (client.readyState === WebSocket.OPEN) {
|
|
1633
1913
|
client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
|
|
1634
1914
|
}
|
|
1635
1915
|
});
|
|
1636
1916
|
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Sends an open snackbar message to all connected clients.
|
|
1919
|
+
* @param {string} message - The message to send.
|
|
1920
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message.
|
|
1921
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
|
|
1922
|
+
*
|
|
1923
|
+
*/
|
|
1637
1924
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1638
1925
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
1926
|
+
// Send the message to all connected clients
|
|
1639
1927
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1640
1928
|
if (client.readyState === WebSocket.OPEN) {
|
|
1641
1929
|
client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
|
|
1642
1930
|
}
|
|
1643
1931
|
});
|
|
1644
1932
|
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Sends a close snackbar message to all connected clients.
|
|
1935
|
+
* @param {string} message - The message to send.
|
|
1936
|
+
*
|
|
1937
|
+
*/
|
|
1645
1938
|
wssSendCloseSnackbarMessage(message) {
|
|
1646
1939
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
1940
|
+
// Send the message to all connected clients
|
|
1647
1941
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1648
1942
|
if (client.readyState === WebSocket.OPEN) {
|
|
1649
1943
|
client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
|
|
1650
1944
|
}
|
|
1651
1945
|
});
|
|
1652
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
1949
|
+
*
|
|
1950
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
1951
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
1952
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
1953
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
1954
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
1955
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
1956
|
+
*
|
|
1957
|
+
* @remarks
|
|
1958
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
1959
|
+
* with the updated attribute information.
|
|
1960
|
+
*/
|
|
1653
1961
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
|
|
1654
1962
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
1963
|
+
// Send the message to all connected clients
|
|
1655
1964
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1656
1965
|
if (client.readyState === WebSocket.OPEN) {
|
|
1657
1966
|
client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
|
|
1658
1967
|
}
|
|
1659
1968
|
});
|
|
1660
1969
|
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Sends a message to all connected clients.
|
|
1972
|
+
* @param {number} id - The message id.
|
|
1973
|
+
* @param {string} method - The message method.
|
|
1974
|
+
* @param {Record<string, string | number | boolean>} params - The message parameters.
|
|
1975
|
+
*
|
|
1976
|
+
*/
|
|
1661
1977
|
wssBroadcastMessage(id, method, params) {
|
|
1662
1978
|
this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
|
|
1979
|
+
// Send the message to all connected clients
|
|
1663
1980
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1664
1981
|
if (client.readyState === WebSocket.OPEN) {
|
|
1665
1982
|
client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
|
|
@@ -1667,3 +1984,4 @@ export class Frontend {
|
|
|
1667
1984
|
});
|
|
1668
1985
|
}
|
|
1669
1986
|
}
|
|
1987
|
+
//# sourceMappingURL=frontend.js.map
|