matterbridge 3.0.0-edge.8 → 3.0.0
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/CHANGELOG.md +50 -9
- package/README-DEV.md +4 -0
- package/README-DOCKER.md +21 -12
- package/README-SERVICE.md +27 -21
- package/README.md +80 -2
- package/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +41 -3
- 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 +222 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +443 -35
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -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 +431 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +800 -79
- 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 +1514 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +33 -1
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +494 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +431 -12
- 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 +943 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +806 -7
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2706 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +156 -9
- 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 +227 -9
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +187 -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 +271 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +262 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/shelly.d.ts +92 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +146 -6
- 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 +32 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +70 -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/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 +40 -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 +65 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +10 -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/isvalid.d.ts +87 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +86 -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/parameter.d.ts +58 -0
- package/dist/utils/parameter.d.ts.map +1 -0
- package/dist/utils/parameter.js +92 -7
- package/dist/utils/parameter.js.map +1 -0
- package/dist/utils/wait.d.ts +43 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +50 -5
- package/dist/utils/wait.js.map +1 -0
- package/frontend/build/asset-manifest.json +6 -6
- package/frontend/build/bmc-button.svg +22 -0
- package/frontend/build/discord.svg +5 -0
- package/frontend/build/index.html +1 -1
- package/frontend/build/static/css/{main.ea7910e9.css → main.944b63c3.css} +2 -2
- package/frontend/build/static/css/main.944b63c3.css.map +1 -0
- package/frontend/build/static/js/{main.e11d6bb4.js → main.1d983660.js} +12 -12
- package/frontend/build/static/js/{main.e11d6bb4.js.map → main.1d983660.js.map} +1 -1
- package/npm-shrinkwrap.json +300 -362
- package/package.json +5 -4
- package/tsconfig.jest.json +8 -0
- package/README-EDGE.md +0 -74
- package/frontend/build/static/css/main.ea7910e9.css.map +0 -1
- /package/frontend/build/static/js/{main.e11d6bb4.js.LICENSE.txt → main.1d983660.js.LICENSE.txt} +0 -0
package/dist/frontend.js
CHANGED
|
@@ -1,27 +1,106 @@
|
|
|
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, deepCopy, isValidArray, isValidNumber, isValidObject, isValidString } from './utils/export.js';
|
|
12
39
|
import { plg } from './matterbridgeTypes.js';
|
|
13
40
|
import { hasParameter } from './utils/export.js';
|
|
14
|
-
import { BridgedDeviceBasicInformation } from '@matter/main/clusters';
|
|
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 a shelly system update.
|
|
89
|
+
* check:
|
|
90
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/check
|
|
91
|
+
* perform:
|
|
92
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/perform
|
|
93
|
+
* @constant {number}
|
|
94
|
+
*/
|
|
24
95
|
export const WS_ID_SHELLY_SYS_UPDATE = 100;
|
|
96
|
+
/**
|
|
97
|
+
* Websocket message ID indicating a shelly main update.
|
|
98
|
+
* check:
|
|
99
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/check
|
|
100
|
+
* perform:
|
|
101
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/perform
|
|
102
|
+
* @constant {number}
|
|
103
|
+
*/
|
|
25
104
|
export const WS_ID_SHELLY_MAIN_UPDATE = 101;
|
|
26
105
|
export class Frontend {
|
|
27
106
|
matterbridge;
|
|
@@ -39,7 +118,7 @@ export class Frontend {
|
|
|
39
118
|
memoryTimeout;
|
|
40
119
|
constructor(matterbridge) {
|
|
41
120
|
this.matterbridge = matterbridge;
|
|
42
|
-
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 */ });
|
|
43
122
|
}
|
|
44
123
|
set logLevel(logLevel) {
|
|
45
124
|
this.log.logLevel = logLevel;
|
|
@@ -47,13 +126,43 @@ export class Frontend {
|
|
|
47
126
|
async start(port = 8283) {
|
|
48
127
|
this.port = port;
|
|
49
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
|
|
50
130
|
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
51
131
|
await fs.mkdir(uploadDir, { recursive: true });
|
|
52
132
|
const upload = multer({ dest: uploadDir });
|
|
133
|
+
// Create the express app that serves the frontend
|
|
53
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
|
|
54
161
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
55
162
|
if (!hasParameter('ssl')) {
|
|
163
|
+
// Create an HTTP server and attach the express app
|
|
56
164
|
this.httpServer = createServer(this.expressApp);
|
|
165
|
+
// Listen on the specified port
|
|
57
166
|
if (hasParameter('ingress')) {
|
|
58
167
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
59
168
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -67,6 +176,7 @@ export class Frontend {
|
|
|
67
176
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
68
177
|
});
|
|
69
178
|
}
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
180
|
this.httpServer.on('error', (error) => {
|
|
71
181
|
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
72
182
|
switch (error.code) {
|
|
@@ -82,6 +192,7 @@ export class Frontend {
|
|
|
82
192
|
});
|
|
83
193
|
}
|
|
84
194
|
else {
|
|
195
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
85
196
|
let cert;
|
|
86
197
|
try {
|
|
87
198
|
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
@@ -109,7 +220,9 @@ export class Frontend {
|
|
|
109
220
|
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
110
221
|
}
|
|
111
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
|
|
112
224
|
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
225
|
+
// Listen on the specified port
|
|
113
226
|
if (hasParameter('ingress')) {
|
|
114
227
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
115
228
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -123,6 +236,7 @@ export class Frontend {
|
|
|
123
236
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
124
237
|
});
|
|
125
238
|
}
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
126
240
|
this.httpsServer.on('error', (error) => {
|
|
127
241
|
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
128
242
|
switch (error.code) {
|
|
@@ -139,16 +253,18 @@ export class Frontend {
|
|
|
139
253
|
}
|
|
140
254
|
if (this.initializeError)
|
|
141
255
|
return;
|
|
256
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
142
257
|
const wssPort = this.port;
|
|
143
258
|
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
144
259
|
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
145
260
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
146
261
|
const clientIp = request.socket.remoteAddress;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 */;
|
|
152
268
|
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
|
|
153
269
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
154
270
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -182,6 +298,7 @@ export class Frontend {
|
|
|
182
298
|
this.webSocketServer.on('error', (ws, error) => {
|
|
183
299
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
184
300
|
});
|
|
301
|
+
// Subscribe to cli events
|
|
185
302
|
const { cliEmitter } = await import('./cli.js');
|
|
186
303
|
cliEmitter.removeAllListeners();
|
|
187
304
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
@@ -193,6 +310,7 @@ export class Frontend {
|
|
|
193
310
|
cliEmitter.on('cpu', (cpuUsage) => {
|
|
194
311
|
this.wssSendCpuUpdate(cpuUsage);
|
|
195
312
|
});
|
|
313
|
+
// Endpoint to validate login code
|
|
196
314
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
197
315
|
const { password } = req.body;
|
|
198
316
|
this.log.debug('The frontend sent /api/login', password);
|
|
@@ -211,23 +329,27 @@ export class Frontend {
|
|
|
211
329
|
this.log.warn('/api/login error wrong password');
|
|
212
330
|
res.json({ valid: false });
|
|
213
331
|
}
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
214
333
|
}
|
|
215
334
|
catch (error) {
|
|
216
335
|
this.log.error('/api/login error getting password');
|
|
217
336
|
res.json({ valid: false });
|
|
218
337
|
}
|
|
219
338
|
});
|
|
339
|
+
// Endpoint to provide health check
|
|
220
340
|
this.expressApp.get('/health', (req, res) => {
|
|
221
341
|
this.log.debug('Express received /health');
|
|
222
342
|
const healthStatus = {
|
|
223
|
-
status: 'ok',
|
|
224
|
-
uptime: process.uptime(),
|
|
225
|
-
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
|
|
226
346
|
};
|
|
227
347
|
res.status(200).json(healthStatus);
|
|
228
348
|
});
|
|
349
|
+
// Endpoint to provide memory usage details
|
|
229
350
|
this.expressApp.get('/memory', async (req, res) => {
|
|
230
351
|
this.log.debug('Express received /memory');
|
|
352
|
+
// Memory usage from process
|
|
231
353
|
const memoryUsageRaw = process.memoryUsage();
|
|
232
354
|
const memoryUsage = {
|
|
233
355
|
rss: this.formatMemoryUsage(memoryUsageRaw.rss),
|
|
@@ -236,10 +358,13 @@ export class Frontend {
|
|
|
236
358
|
external: this.formatMemoryUsage(memoryUsageRaw.external),
|
|
237
359
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
238
360
|
};
|
|
361
|
+
// V8 heap statistics
|
|
239
362
|
const { default: v8 } = await import('node:v8');
|
|
240
363
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
241
364
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
365
|
+
// Format heapStats
|
|
242
366
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
|
|
367
|
+
// Format heapSpaces
|
|
243
368
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
244
369
|
...space,
|
|
245
370
|
space_size: this.formatMemoryUsage(space.space_size),
|
|
@@ -257,6 +382,7 @@ export class Frontend {
|
|
|
257
382
|
};
|
|
258
383
|
res.status(200).json(memoryReport);
|
|
259
384
|
});
|
|
385
|
+
// Endpoint to start advertising the server node
|
|
260
386
|
this.expressApp.get('/api/advertise', express.json(), async (req, res) => {
|
|
261
387
|
const pairingCodes = await this.matterbridge.advertiseServerNode(this.matterbridge.serverNode);
|
|
262
388
|
if (pairingCodes) {
|
|
@@ -267,18 +393,22 @@ export class Frontend {
|
|
|
267
393
|
res.status(500).json({ error: 'Failed to generate pairing codes' });
|
|
268
394
|
}
|
|
269
395
|
});
|
|
396
|
+
// Endpoint to provide settings
|
|
270
397
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
271
398
|
this.log.debug('The frontend sent /api/settings');
|
|
272
399
|
res.json(await this.getApiSettings());
|
|
273
400
|
});
|
|
401
|
+
// Endpoint to provide plugins
|
|
274
402
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
275
403
|
this.log.debug('The frontend sent /api/plugins');
|
|
276
404
|
res.json(this.getBaseRegisteredPlugins());
|
|
277
405
|
});
|
|
406
|
+
// Endpoint to provide devices
|
|
278
407
|
this.expressApp.get('/api/devices', (req, res) => {
|
|
279
408
|
this.log.debug('The frontend sent /api/devices');
|
|
280
409
|
const devices = [];
|
|
281
410
|
this.matterbridge.devices.forEach(async (device) => {
|
|
411
|
+
// Check if the device has the required properties
|
|
282
412
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
283
413
|
return;
|
|
284
414
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -297,6 +427,7 @@ export class Frontend {
|
|
|
297
427
|
});
|
|
298
428
|
res.json(devices);
|
|
299
429
|
});
|
|
430
|
+
// Endpoint to provide the cluster servers of the devices
|
|
300
431
|
this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
|
|
301
432
|
const selectedPluginName = req.params.selectedPluginName;
|
|
302
433
|
const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
|
|
@@ -369,63 +500,107 @@ export class Frontend {
|
|
|
369
500
|
});
|
|
370
501
|
res.json(data);
|
|
371
502
|
});
|
|
372
|
-
|
|
373
|
-
|
|
503
|
+
// Endpoint to view the matterbridge log
|
|
504
|
+
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
505
|
+
this.log.debug('The frontend sent /api/view-mblog');
|
|
374
506
|
try {
|
|
375
507
|
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
|
|
376
508
|
res.type('text/plain');
|
|
377
509
|
res.send(data);
|
|
378
510
|
}
|
|
379
511
|
catch (error) {
|
|
380
|
-
this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
381
|
-
res.status(500).send('Error reading log file');
|
|
512
|
+
this.log.error(`Error reading matterbridge log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
513
|
+
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
// Endpoint to view the matter.js log
|
|
517
|
+
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
518
|
+
this.log.debug('The frontend sent /api/view-mjlog');
|
|
519
|
+
try {
|
|
520
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
|
|
521
|
+
res.type('text/plain');
|
|
522
|
+
res.send(data);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
this.log.error(`Error reading matter log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
526
|
+
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
382
527
|
}
|
|
383
528
|
});
|
|
529
|
+
// Endpoint to view the shelly log
|
|
530
|
+
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
531
|
+
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
532
|
+
try {
|
|
533
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
|
|
534
|
+
res.type('text/plain');
|
|
535
|
+
res.send(data);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
this.log.error(`Error reading shelly log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
539
|
+
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
// Endpoint to download the matterbridge log
|
|
384
543
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
385
|
-
this.log.debug(
|
|
544
|
+
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
386
545
|
try {
|
|
387
546
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
|
|
547
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
|
|
548
|
+
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), data, 'utf-8');
|
|
388
549
|
}
|
|
389
550
|
catch (error) {
|
|
390
|
-
fs.
|
|
551
|
+
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'Enable the matterbridge log on file in the settings to download the matterbridge log.', 'utf-8');
|
|
552
|
+
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
391
553
|
}
|
|
392
|
-
res.
|
|
554
|
+
res.type('text/plain');
|
|
555
|
+
// res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
556
|
+
res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
393
557
|
if (error) {
|
|
394
558
|
this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
395
559
|
res.status(500).send('Error downloading the matterbridge log file');
|
|
396
560
|
}
|
|
397
561
|
});
|
|
398
562
|
});
|
|
563
|
+
// Endpoint to download the matter log
|
|
399
564
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
400
|
-
this.log.debug(
|
|
565
|
+
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
401
566
|
try {
|
|
402
567
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
|
|
568
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
|
|
569
|
+
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
|
|
403
570
|
}
|
|
404
571
|
catch (error) {
|
|
405
|
-
fs.
|
|
572
|
+
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'Enable the matter log on file in the settings to download the matter log.', 'utf-8');
|
|
573
|
+
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
406
574
|
}
|
|
407
|
-
res.
|
|
575
|
+
res.type('text/plain');
|
|
576
|
+
res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
|
|
408
577
|
if (error) {
|
|
409
578
|
this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
410
579
|
res.status(500).send('Error downloading the matter log file');
|
|
411
580
|
}
|
|
412
581
|
});
|
|
413
582
|
});
|
|
583
|
+
// Endpoint to download the shelly log
|
|
414
584
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
415
585
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
416
586
|
try {
|
|
417
587
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
|
|
588
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
|
|
589
|
+
await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
|
|
418
590
|
}
|
|
419
591
|
catch (error) {
|
|
420
|
-
fs.
|
|
592
|
+
await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), 'Create the Shelly system log before downloading it.', 'utf-8');
|
|
593
|
+
this.log.debug(`Error in /api/shellydownloadsystemlog: ${error instanceof Error ? error.message : error}`);
|
|
421
594
|
}
|
|
422
|
-
res.
|
|
595
|
+
res.type('text/plain');
|
|
596
|
+
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
423
597
|
if (error) {
|
|
424
598
|
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
425
599
|
res.status(500).send('Error downloading Shelly system log file');
|
|
426
600
|
}
|
|
427
601
|
});
|
|
428
602
|
});
|
|
603
|
+
// Endpoint to download the matter storage file
|
|
429
604
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
430
605
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
431
606
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
|
|
@@ -436,6 +611,7 @@ export class Frontend {
|
|
|
436
611
|
}
|
|
437
612
|
});
|
|
438
613
|
});
|
|
614
|
+
// Endpoint to download the matterbridge storage directory
|
|
439
615
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
440
616
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
441
617
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
@@ -446,6 +622,7 @@ export class Frontend {
|
|
|
446
622
|
}
|
|
447
623
|
});
|
|
448
624
|
});
|
|
625
|
+
// Endpoint to download the matterbridge plugin directory
|
|
449
626
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
450
627
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
451
628
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
@@ -456,9 +633,11 @@ export class Frontend {
|
|
|
456
633
|
}
|
|
457
634
|
});
|
|
458
635
|
});
|
|
636
|
+
// Endpoint to download the matterbridge plugin config files
|
|
459
637
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
460
638
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
461
639
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
640
|
+
// await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
462
641
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
463
642
|
if (error) {
|
|
464
643
|
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
@@ -466,6 +645,7 @@ export class Frontend {
|
|
|
466
645
|
}
|
|
467
646
|
});
|
|
468
647
|
});
|
|
648
|
+
// Endpoint to download the matterbridge plugin config files
|
|
469
649
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
470
650
|
this.log.debug('The frontend sent /api/download-backup');
|
|
471
651
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
@@ -475,6 +655,7 @@ export class Frontend {
|
|
|
475
655
|
}
|
|
476
656
|
});
|
|
477
657
|
});
|
|
658
|
+
// Endpoint to receive commands
|
|
478
659
|
this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
|
|
479
660
|
const command = req.params.command;
|
|
480
661
|
let param = req.params.param;
|
|
@@ -484,13 +665,15 @@ export class Frontend {
|
|
|
484
665
|
return;
|
|
485
666
|
}
|
|
486
667
|
this.log.debug(`Received frontend command: ${command}:${param}`);
|
|
668
|
+
// Handle the command setpassword from Settings
|
|
487
669
|
if (command === 'setpassword') {
|
|
488
|
-
const password = param.slice(1, -1);
|
|
670
|
+
const password = param.slice(1, -1); // Remove the first and last characters
|
|
489
671
|
this.log.debug('setpassword', param, password);
|
|
490
672
|
await this.matterbridge.nodeContext?.set('password', password);
|
|
491
673
|
res.json({ message: 'Command received' });
|
|
492
674
|
return;
|
|
493
675
|
}
|
|
676
|
+
// Handle the command setbridgemode from Settings
|
|
494
677
|
if (command === 'setbridgemode') {
|
|
495
678
|
this.log.debug(`setbridgemode: ${param}`);
|
|
496
679
|
this.wssSendRestartRequired();
|
|
@@ -498,6 +681,7 @@ export class Frontend {
|
|
|
498
681
|
res.json({ message: 'Command received' });
|
|
499
682
|
return;
|
|
500
683
|
}
|
|
684
|
+
// Handle the command backup from Settings
|
|
501
685
|
if (command === 'backup') {
|
|
502
686
|
this.log.notice(`Prepairing the backup...`);
|
|
503
687
|
await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
|
|
@@ -506,31 +690,33 @@ export class Frontend {
|
|
|
506
690
|
res.json({ message: 'Command received' });
|
|
507
691
|
return;
|
|
508
692
|
}
|
|
693
|
+
// Handle the command setmbloglevel from Settings
|
|
509
694
|
if (command === 'setmbloglevel') {
|
|
510
695
|
this.log.debug('Matterbridge log level:', param);
|
|
511
696
|
if (param === 'Debug') {
|
|
512
|
-
this.log.logLevel = "debug"
|
|
697
|
+
this.log.logLevel = "debug" /* LogLevel.DEBUG */;
|
|
513
698
|
}
|
|
514
699
|
else if (param === 'Info') {
|
|
515
|
-
this.log.logLevel = "info"
|
|
700
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
516
701
|
}
|
|
517
702
|
else if (param === 'Notice') {
|
|
518
|
-
this.log.logLevel = "notice"
|
|
703
|
+
this.log.logLevel = "notice" /* LogLevel.NOTICE */;
|
|
519
704
|
}
|
|
520
705
|
else if (param === 'Warn') {
|
|
521
|
-
this.log.logLevel = "warn"
|
|
706
|
+
this.log.logLevel = "warn" /* LogLevel.WARN */;
|
|
522
707
|
}
|
|
523
708
|
else if (param === 'Error') {
|
|
524
|
-
this.log.logLevel = "error"
|
|
709
|
+
this.log.logLevel = "error" /* LogLevel.ERROR */;
|
|
525
710
|
}
|
|
526
711
|
else if (param === 'Fatal') {
|
|
527
|
-
this.log.logLevel = "fatal"
|
|
712
|
+
this.log.logLevel = "fatal" /* LogLevel.FATAL */;
|
|
528
713
|
}
|
|
529
714
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
530
715
|
await this.matterbridge.setLogLevel(this.log.logLevel);
|
|
531
716
|
res.json({ message: 'Command received' });
|
|
532
717
|
return;
|
|
533
718
|
}
|
|
719
|
+
// Handle the command setmbloglevel from Settings
|
|
534
720
|
if (command === 'setmjloglevel') {
|
|
535
721
|
this.log.debug('Matter.js log level:', param);
|
|
536
722
|
if (param === 'Debug') {
|
|
@@ -555,6 +741,7 @@ export class Frontend {
|
|
|
555
741
|
res.json({ message: 'Command received' });
|
|
556
742
|
return;
|
|
557
743
|
}
|
|
744
|
+
// Handle the command setmdnsinterface from Settings
|
|
558
745
|
if (command === 'setmdnsinterface') {
|
|
559
746
|
if (param === 'json' && isValidString(req.body.value)) {
|
|
560
747
|
this.matterbridge.matterbridgeInformation.mattermdnsinterface = req.body.value;
|
|
@@ -564,6 +751,7 @@ export class Frontend {
|
|
|
564
751
|
res.json({ message: 'Command received' });
|
|
565
752
|
return;
|
|
566
753
|
}
|
|
754
|
+
// Handle the command setipv4address from Settings
|
|
567
755
|
if (command === 'setipv4address') {
|
|
568
756
|
if (param === 'json' && isValidString(req.body.value)) {
|
|
569
757
|
this.log.debug(`Matter.js ipv4 address: ${req.body.value === '' ? 'all ipv4 addresses' : req.body.value}`);
|
|
@@ -573,6 +761,7 @@ export class Frontend {
|
|
|
573
761
|
res.json({ message: 'Command received' });
|
|
574
762
|
return;
|
|
575
763
|
}
|
|
764
|
+
// Handle the command setipv6address from Settings
|
|
576
765
|
if (command === 'setipv6address') {
|
|
577
766
|
if (param === 'json' && isValidString(req.body.value)) {
|
|
578
767
|
this.log.debug(`Matter.js ipv6 address: ${req.body.value === '' ? 'all ipv6 addresses' : req.body.value}`);
|
|
@@ -582,6 +771,7 @@ export class Frontend {
|
|
|
582
771
|
res.json({ message: 'Command received' });
|
|
583
772
|
return;
|
|
584
773
|
}
|
|
774
|
+
// Handle the command setmatterport from Settings
|
|
585
775
|
if (command === 'setmatterport') {
|
|
586
776
|
const port = Math.min(Math.max(parseInt(req.body.value), 5540), 5560);
|
|
587
777
|
this.matterbridge.matterbridgeInformation.matterPort = port;
|
|
@@ -590,6 +780,7 @@ export class Frontend {
|
|
|
590
780
|
res.json({ message: 'Command received' });
|
|
591
781
|
return;
|
|
592
782
|
}
|
|
783
|
+
// Handle the command setmatterdiscriminator from Settings
|
|
593
784
|
if (command === 'setmatterdiscriminator') {
|
|
594
785
|
const discriminator = Math.min(Math.max(parseInt(req.body.value), 1000), 4095);
|
|
595
786
|
this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
|
|
@@ -598,6 +789,7 @@ export class Frontend {
|
|
|
598
789
|
res.json({ message: 'Command received' });
|
|
599
790
|
return;
|
|
600
791
|
}
|
|
792
|
+
// Handle the command setmatterpasscode from Settings
|
|
601
793
|
if (command === 'setmatterpasscode') {
|
|
602
794
|
const passcode = Math.min(Math.max(parseInt(req.body.value), 10000000), 90000000);
|
|
603
795
|
this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
|
|
@@ -606,17 +798,20 @@ export class Frontend {
|
|
|
606
798
|
res.json({ message: 'Command received' });
|
|
607
799
|
return;
|
|
608
800
|
}
|
|
801
|
+
// Handle the command setmbloglevel from Settings
|
|
609
802
|
if (command === 'setmblogfile') {
|
|
610
803
|
this.log.debug('Matterbridge file log:', param);
|
|
611
804
|
this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
|
|
612
805
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
|
|
806
|
+
// Create the file logger for matterbridge
|
|
613
807
|
if (param === 'true')
|
|
614
|
-
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug"
|
|
808
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), "debug" /* LogLevel.DEBUG */, true);
|
|
615
809
|
else
|
|
616
810
|
AnsiLogger.setGlobalLogfile(undefined);
|
|
617
811
|
res.json({ message: 'Command received' });
|
|
618
812
|
return;
|
|
619
813
|
}
|
|
814
|
+
// Handle the command setmbloglevel from Settings
|
|
620
815
|
if (command === 'setmjlogfile') {
|
|
621
816
|
this.log.debug('Matter file log:', param);
|
|
622
817
|
this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
|
|
@@ -643,40 +838,48 @@ export class Frontend {
|
|
|
643
838
|
res.json({ message: 'Command received' });
|
|
644
839
|
return;
|
|
645
840
|
}
|
|
841
|
+
// Handle the command unregister from Settings
|
|
646
842
|
if (command === 'unregister') {
|
|
647
843
|
await this.matterbridge.unregisterAndShutdownProcess();
|
|
648
844
|
res.json({ message: 'Command received' });
|
|
649
845
|
return;
|
|
650
846
|
}
|
|
847
|
+
// Handle the command reset from Settings
|
|
651
848
|
if (command === 'reset') {
|
|
652
849
|
await this.matterbridge.shutdownProcessAndReset();
|
|
653
850
|
res.json({ message: 'Command received' });
|
|
654
851
|
return;
|
|
655
852
|
}
|
|
853
|
+
// Handle the command factoryreset from Settings
|
|
656
854
|
if (command === 'factoryreset') {
|
|
657
855
|
await this.matterbridge.shutdownProcessAndFactoryReset();
|
|
658
856
|
res.json({ message: 'Command received' });
|
|
659
857
|
return;
|
|
660
858
|
}
|
|
859
|
+
// Handle the command shutdown from Header
|
|
661
860
|
if (command === 'shutdown') {
|
|
662
861
|
await this.matterbridge.shutdownProcess();
|
|
663
862
|
res.json({ message: 'Command received' });
|
|
664
863
|
return;
|
|
665
864
|
}
|
|
865
|
+
// Handle the command restart from Header
|
|
666
866
|
if (command === 'restart') {
|
|
667
867
|
await this.matterbridge.restartProcess();
|
|
668
868
|
res.json({ message: 'Command received' });
|
|
669
869
|
return;
|
|
670
870
|
}
|
|
871
|
+
// Handle the command update from Header
|
|
671
872
|
if (command === 'update') {
|
|
672
873
|
await this.matterbridge.updateProcess();
|
|
673
874
|
this.wssSendRestartRequired();
|
|
674
875
|
res.json({ message: 'Command received' });
|
|
675
876
|
return;
|
|
676
877
|
}
|
|
878
|
+
// Handle the command saveconfig from Home
|
|
677
879
|
if (command === 'saveconfig') {
|
|
678
880
|
param = param.replace(/\*/g, '\\');
|
|
679
881
|
this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
|
|
882
|
+
// console.log('Req.body:', JSON.stringify(req.body, null, 2));
|
|
680
883
|
if (!this.matterbridge.plugins.has(param)) {
|
|
681
884
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
682
885
|
}
|
|
@@ -691,6 +894,7 @@ export class Frontend {
|
|
|
691
894
|
res.json({ message: 'Command received' });
|
|
692
895
|
return;
|
|
693
896
|
}
|
|
897
|
+
// Handle the command installplugin from Home
|
|
694
898
|
if (command === 'installplugin') {
|
|
695
899
|
param = param.replace(/\*/g, '\\');
|
|
696
900
|
this.log.info(`Installing plugin ${plg}${param}${nf}...`);
|
|
@@ -699,6 +903,7 @@ export class Frontend {
|
|
|
699
903
|
await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
|
|
700
904
|
this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
|
|
701
905
|
this.wssSendSnackbarMessage(`Installed package ${param}`, 10, 'success');
|
|
906
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
702
907
|
}
|
|
703
908
|
catch (error) {
|
|
704
909
|
this.log.error(`Error installing plugin ${plg}${param}${er}`);
|
|
@@ -706,17 +911,22 @@ export class Frontend {
|
|
|
706
911
|
}
|
|
707
912
|
this.wssSendRestartRequired();
|
|
708
913
|
param = param.split('@')[0];
|
|
914
|
+
// Also add the plugin to matterbridge so no return!
|
|
709
915
|
if (param === 'matterbridge') {
|
|
916
|
+
// If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
|
|
710
917
|
res.json({ message: 'Command received' });
|
|
711
918
|
return;
|
|
712
919
|
}
|
|
713
920
|
}
|
|
921
|
+
// Handle the command addplugin from Home
|
|
714
922
|
if (command === 'addplugin' || command === 'installplugin') {
|
|
715
923
|
param = param.replace(/\*/g, '\\');
|
|
716
924
|
const plugin = await this.matterbridge.plugins.add(param);
|
|
717
925
|
if (plugin) {
|
|
718
926
|
this.wssSendSnackbarMessage(`Added plugin ${param}`);
|
|
719
927
|
if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
928
|
+
// We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node
|
|
929
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
720
930
|
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
721
931
|
}
|
|
722
932
|
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true).then(() => {
|
|
@@ -726,13 +936,14 @@ export class Frontend {
|
|
|
726
936
|
res.json({ message: 'Command received' });
|
|
727
937
|
return;
|
|
728
938
|
}
|
|
939
|
+
// Handle the command removeplugin from Home
|
|
729
940
|
if (command === 'removeplugin') {
|
|
730
941
|
if (!this.matterbridge.plugins.has(param)) {
|
|
731
942
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
732
943
|
}
|
|
733
944
|
else {
|
|
734
945
|
const plugin = this.matterbridge.plugins.get(param);
|
|
735
|
-
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
946
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode
|
|
736
947
|
await this.matterbridge.plugins.remove(param);
|
|
737
948
|
this.wssSendSnackbarMessage(`Removed plugin ${param}`);
|
|
738
949
|
this.wssSendRefreshRequired('plugins');
|
|
@@ -740,6 +951,7 @@ export class Frontend {
|
|
|
740
951
|
res.json({ message: 'Command received' });
|
|
741
952
|
return;
|
|
742
953
|
}
|
|
954
|
+
// Handle the command enableplugin from Home
|
|
743
955
|
if (command === 'enableplugin') {
|
|
744
956
|
if (!this.matterbridge.plugins.has(param)) {
|
|
745
957
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
@@ -758,6 +970,7 @@ export class Frontend {
|
|
|
758
970
|
await this.matterbridge.plugins.enable(param);
|
|
759
971
|
this.wssSendSnackbarMessage(`Enabled plugin ${param}`);
|
|
760
972
|
if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
|
|
973
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
761
974
|
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
762
975
|
}
|
|
763
976
|
this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true).then(() => {
|
|
@@ -768,6 +981,7 @@ export class Frontend {
|
|
|
768
981
|
res.json({ message: 'Command received' });
|
|
769
982
|
return;
|
|
770
983
|
}
|
|
984
|
+
// Handle the command disableplugin from Home
|
|
771
985
|
if (command === 'disableplugin') {
|
|
772
986
|
if (!this.matterbridge.plugins.has(param)) {
|
|
773
987
|
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
@@ -775,7 +989,7 @@ export class Frontend {
|
|
|
775
989
|
else {
|
|
776
990
|
const plugin = this.matterbridge.plugins.get(param);
|
|
777
991
|
if (plugin && plugin.enabled) {
|
|
778
|
-
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
|
|
992
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true); // This will also close the server node in childbridge mode
|
|
779
993
|
await this.matterbridge.plugins.disable(param);
|
|
780
994
|
this.wssSendSnackbarMessage(`Disabled plugin ${param}`);
|
|
781
995
|
this.wssSendRefreshRequired('plugins');
|
|
@@ -794,10 +1008,13 @@ export class Frontend {
|
|
|
794
1008
|
return;
|
|
795
1009
|
}
|
|
796
1010
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`);
|
|
1011
|
+
// Define the path where the plugin file will be saved
|
|
797
1012
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
798
1013
|
try {
|
|
1014
|
+
// Move the uploaded file to the specified path
|
|
799
1015
|
await fs.rename(file.path, filePath);
|
|
800
1016
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
1017
|
+
// Install the plugin package
|
|
801
1018
|
if (filename.endsWith('.tgz')) {
|
|
802
1019
|
await this.matterbridge.spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
|
|
803
1020
|
this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
|
|
@@ -814,32 +1031,46 @@ export class Frontend {
|
|
|
814
1031
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
815
1032
|
}
|
|
816
1033
|
});
|
|
817
|
-
|
|
1034
|
+
// Fallback for routing (must be the last route)
|
|
1035
|
+
this.expressApp.use((req, res) => {
|
|
818
1036
|
this.log.debug('The frontend sent:', req.url);
|
|
819
|
-
this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
820
1037
|
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
821
1038
|
});
|
|
1039
|
+
/* Not working in express v5!
|
|
1040
|
+
this.expressApp.get('*', (req, res) => {
|
|
1041
|
+
this.log.debug('The frontend sent:', req.url);
|
|
1042
|
+
this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
1043
|
+
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
1044
|
+
});
|
|
1045
|
+
*/
|
|
822
1046
|
this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
|
|
823
1047
|
}
|
|
824
1048
|
async stop() {
|
|
1049
|
+
// Remove all listeners from the cliEmitter
|
|
1050
|
+
// cliEmitter.removeAllListeners();
|
|
1051
|
+
// Close the http server
|
|
825
1052
|
if (this.httpServer) {
|
|
826
1053
|
this.httpServer.close();
|
|
827
1054
|
this.httpServer.removeAllListeners();
|
|
828
1055
|
this.httpServer = undefined;
|
|
829
1056
|
this.log.debug('Frontend http server closed successfully');
|
|
830
1057
|
}
|
|
1058
|
+
// Close the https server
|
|
831
1059
|
if (this.httpsServer) {
|
|
832
1060
|
this.httpsServer.close();
|
|
833
1061
|
this.httpsServer.removeAllListeners();
|
|
834
1062
|
this.httpsServer = undefined;
|
|
835
1063
|
this.log.debug('Frontend https server closed successfully');
|
|
836
1064
|
}
|
|
1065
|
+
// Remove listeners from the express app
|
|
837
1066
|
if (this.expressApp) {
|
|
838
1067
|
this.expressApp.removeAllListeners();
|
|
839
1068
|
this.expressApp = undefined;
|
|
840
1069
|
this.log.debug('Frontend app closed successfully');
|
|
841
1070
|
}
|
|
1071
|
+
// Close the WebSocket server
|
|
842
1072
|
if (this.webSocketServer) {
|
|
1073
|
+
// Close all active connections
|
|
843
1074
|
this.webSocketServer.clients.forEach((client) => {
|
|
844
1075
|
if (client.readyState === WebSocket.OPEN) {
|
|
845
1076
|
client.close();
|
|
@@ -856,6 +1087,7 @@ export class Frontend {
|
|
|
856
1087
|
this.webSocketServer = undefined;
|
|
857
1088
|
}
|
|
858
1089
|
}
|
|
1090
|
+
// Function to format bytes to KB, MB, or GB
|
|
859
1091
|
formatMemoryUsage = (bytes) => {
|
|
860
1092
|
if (bytes >= 1024 ** 3) {
|
|
861
1093
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -867,6 +1099,7 @@ export class Frontend {
|
|
|
867
1099
|
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
868
1100
|
}
|
|
869
1101
|
};
|
|
1102
|
+
// Function to format system uptime with only the most significant unit
|
|
870
1103
|
formatOsUpTime = (seconds) => {
|
|
871
1104
|
if (seconds >= 86400) {
|
|
872
1105
|
const days = Math.floor(seconds / 86400);
|
|
@@ -882,8 +1115,13 @@ export class Frontend {
|
|
|
882
1115
|
}
|
|
883
1116
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
884
1117
|
};
|
|
1118
|
+
/**
|
|
1119
|
+
* Retrieves the api settings data.
|
|
1120
|
+
* @returns {Promise<object>} A promise that resolve in the api settings object.
|
|
1121
|
+
*/
|
|
885
1122
|
async getApiSettings() {
|
|
886
1123
|
const { lastCpuUsage } = await import('./cli.js');
|
|
1124
|
+
// Update the system information
|
|
887
1125
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
888
1126
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
889
1127
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
|
|
@@ -892,6 +1130,7 @@ export class Frontend {
|
|
|
892
1130
|
this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
|
|
893
1131
|
this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
|
|
894
1132
|
this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
|
|
1133
|
+
// Update the matterbridge information
|
|
895
1134
|
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
896
1135
|
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
897
1136
|
this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
|
|
@@ -910,6 +1149,11 @@ export class Frontend {
|
|
|
910
1149
|
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
911
1150
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
912
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Retrieves the reachable attribute.
|
|
1154
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
1155
|
+
* @returns {boolean} The reachable attribute.
|
|
1156
|
+
*/
|
|
913
1157
|
getReachability(device) {
|
|
914
1158
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
915
1159
|
return false;
|
|
@@ -919,6 +1163,35 @@ export class Frontend {
|
|
|
919
1163
|
return true;
|
|
920
1164
|
return false;
|
|
921
1165
|
}
|
|
1166
|
+
getPowerSource(device) {
|
|
1167
|
+
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
1168
|
+
return undefined;
|
|
1169
|
+
const powerSource = (device) => {
|
|
1170
|
+
const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
|
|
1171
|
+
if (featureMap.wired) {
|
|
1172
|
+
const wiredCurrentType = device.getAttribute(PowerSource.Cluster.id, 'wiredCurrentType');
|
|
1173
|
+
return ['ac', 'dc'][wiredCurrentType];
|
|
1174
|
+
}
|
|
1175
|
+
if (featureMap.battery) {
|
|
1176
|
+
const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batChargeLevel');
|
|
1177
|
+
return ['ok', 'warning', 'critical'][batChargeLevel];
|
|
1178
|
+
}
|
|
1179
|
+
return;
|
|
1180
|
+
};
|
|
1181
|
+
// Root endpoint
|
|
1182
|
+
if (device.hasClusterServer(PowerSource.Cluster.id))
|
|
1183
|
+
return powerSource(device);
|
|
1184
|
+
// Child endpoints
|
|
1185
|
+
for (const child of device.getChildEndpoints()) {
|
|
1186
|
+
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
1187
|
+
return powerSource(child);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Retrieves the cluster text description from a given device.
|
|
1192
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
1193
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1194
|
+
*/
|
|
922
1195
|
getClusterTextFromDevice(device) {
|
|
923
1196
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
924
1197
|
return '';
|
|
@@ -958,7 +1231,9 @@ export class Frontend {
|
|
|
958
1231
|
return '';
|
|
959
1232
|
};
|
|
960
1233
|
let attributes = '';
|
|
1234
|
+
let supportedModes = [];
|
|
961
1235
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1236
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
962
1237
|
if (typeof attributeValue === 'undefined')
|
|
963
1238
|
return;
|
|
964
1239
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -975,6 +1250,20 @@ export class Frontend {
|
|
|
975
1250
|
attributes += `Heat to: ${attributeValue / 100}°C `;
|
|
976
1251
|
if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint' && isValidNumber(attributeValue))
|
|
977
1252
|
attributes += `Cool to: ${attributeValue / 100}°C `;
|
|
1253
|
+
const modeClusters = ['modeSelect', 'rvcRunMode', 'rvcCleanMode', 'laundryWasherMode', 'ovenMode', 'microwaveOvenMode'];
|
|
1254
|
+
if (modeClusters.includes(clusterName) && attributeName === 'supportedModes') {
|
|
1255
|
+
supportedModes = attributeValue;
|
|
1256
|
+
}
|
|
1257
|
+
if (modeClusters.includes(clusterName) && attributeName === 'currentMode') {
|
|
1258
|
+
const supportedMode = supportedModes.find((mode) => mode.mode === attributeValue);
|
|
1259
|
+
if (supportedMode)
|
|
1260
|
+
attributes += `Mode: ${supportedMode.label} `;
|
|
1261
|
+
else
|
|
1262
|
+
attributes += `Mode: ${attributeValue} `;
|
|
1263
|
+
}
|
|
1264
|
+
const operationalStateClusters = ['operationalState', 'rvcOperationalState'];
|
|
1265
|
+
if (operationalStateClusters.includes(clusterName) && attributeName === 'operationalState')
|
|
1266
|
+
attributes += `OpState: ${attributeValue} `;
|
|
978
1267
|
if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
|
|
979
1268
|
attributes += `Mode: ${attributeValue} `;
|
|
980
1269
|
if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
|
|
@@ -1036,8 +1325,13 @@ export class Frontend {
|
|
|
1036
1325
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
1037
1326
|
attributes += `${getUserLabel(device)} `;
|
|
1038
1327
|
});
|
|
1328
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
1039
1329
|
return attributes.trimStart().trimEnd();
|
|
1040
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
1333
|
+
* @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
|
|
1334
|
+
*/
|
|
1041
1335
|
getBaseRegisteredPlugins() {
|
|
1042
1336
|
const baseRegisteredPlugins = [];
|
|
1043
1337
|
for (const plugin of this.matterbridge.plugins) {
|
|
@@ -1053,6 +1347,7 @@ export class Frontend {
|
|
|
1053
1347
|
changelog: plugin.changelog,
|
|
1054
1348
|
funding: plugin.funding,
|
|
1055
1349
|
latestVersion: plugin.latestVersion,
|
|
1350
|
+
serialNumber: plugin.serialNumber,
|
|
1056
1351
|
locked: plugin.locked,
|
|
1057
1352
|
error: plugin.error,
|
|
1058
1353
|
enabled: plugin.enabled,
|
|
@@ -1074,6 +1369,13 @@ export class Frontend {
|
|
|
1074
1369
|
}
|
|
1075
1370
|
return baseRegisteredPlugins;
|
|
1076
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Handles incoming websocket messages for the Matterbridge frontend.
|
|
1374
|
+
*
|
|
1375
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1376
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1377
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1378
|
+
*/
|
|
1077
1379
|
async wsMessageHandler(client, message) {
|
|
1078
1380
|
let data;
|
|
1079
1381
|
try {
|
|
@@ -1229,8 +1531,10 @@ export class Frontend {
|
|
|
1229
1531
|
else if (data.method === '/api/devices') {
|
|
1230
1532
|
const devices = [];
|
|
1231
1533
|
this.matterbridge.devices.forEach(async (device) => {
|
|
1534
|
+
// Filter by pluginName if provided
|
|
1232
1535
|
if (data.params.pluginName && data.params.pluginName !== device.plugin)
|
|
1233
1536
|
return;
|
|
1537
|
+
// Check if the device has the required properties
|
|
1234
1538
|
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
1235
1539
|
return;
|
|
1236
1540
|
const cluster = this.getClusterTextFromDevice(device);
|
|
@@ -1244,6 +1548,7 @@ export class Frontend {
|
|
|
1244
1548
|
configUrl: device.configUrl,
|
|
1245
1549
|
uniqueId: device.uniqueId,
|
|
1246
1550
|
reachable: this.getReachability(device),
|
|
1551
|
+
powerSource: this.getPowerSource(device),
|
|
1247
1552
|
cluster: cluster,
|
|
1248
1553
|
});
|
|
1249
1554
|
});
|
|
@@ -1314,6 +1619,7 @@ export class Frontend {
|
|
|
1314
1619
|
});
|
|
1315
1620
|
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
1316
1621
|
deviceTypes = [];
|
|
1622
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1317
1623
|
const name = childEndpoint.endpoint?.id;
|
|
1318
1624
|
const clusterServers = childEndpoint.getAllClusterServers();
|
|
1319
1625
|
clusterServers.forEach((clusterServer) => {
|
|
@@ -1396,7 +1702,7 @@ export class Frontend {
|
|
|
1396
1702
|
return;
|
|
1397
1703
|
}
|
|
1398
1704
|
this.log.notice(`Action ${CYAN}${data.params.action}${nt}${data.params.value ? ' with ' + CYAN + data.params.value + nt : ''} for plugin ${CYAN}${plugin.name}${nt}`);
|
|
1399
|
-
plugin.platform?.onAction(data.params.action, data.params.value, data.params.id).catch((error) => {
|
|
1705
|
+
plugin.platform?.onAction(data.params.action, data.params.value, data.params.id, data.params.formData).catch((error) => {
|
|
1400
1706
|
this.log.error(`Error in plugin ${plugin.name} action ${data.params.action}: ${error}`);
|
|
1401
1707
|
});
|
|
1402
1708
|
}
|
|
@@ -1412,6 +1718,7 @@ export class Frontend {
|
|
|
1412
1718
|
return;
|
|
1413
1719
|
}
|
|
1414
1720
|
const config = plugin.configJson;
|
|
1721
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1415
1722
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1416
1723
|
this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1417
1724
|
if (select === 'serial')
|
|
@@ -1419,9 +1726,11 @@ export class Frontend {
|
|
|
1419
1726
|
if (select === 'name')
|
|
1420
1727
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1421
1728
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
1729
|
+
// Remove postfix from the serial if it exists
|
|
1422
1730
|
if (config.postfix) {
|
|
1423
1731
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1424
1732
|
}
|
|
1733
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1425
1734
|
if (isValidArray(config.whiteList, 1)) {
|
|
1426
1735
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1427
1736
|
config.whiteList.push(data.params.serial);
|
|
@@ -1430,6 +1739,7 @@ export class Frontend {
|
|
|
1430
1739
|
config.whiteList.push(data.params.name);
|
|
1431
1740
|
}
|
|
1432
1741
|
}
|
|
1742
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1433
1743
|
if (isValidArray(config.blackList, 1)) {
|
|
1434
1744
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1435
1745
|
config.blackList = config.blackList.filter((serial) => serial !== data.params.serial);
|
|
@@ -1451,6 +1761,7 @@ export class Frontend {
|
|
|
1451
1761
|
return;
|
|
1452
1762
|
}
|
|
1453
1763
|
const config = plugin.configJson;
|
|
1764
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1454
1765
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1455
1766
|
this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1456
1767
|
if (select === 'serial')
|
|
@@ -1461,6 +1772,7 @@ export class Frontend {
|
|
|
1461
1772
|
if (config.postfix) {
|
|
1462
1773
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1463
1774
|
}
|
|
1775
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1464
1776
|
if (isValidArray(config.whiteList, 1)) {
|
|
1465
1777
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1466
1778
|
config.whiteList = config.whiteList.filter((serial) => serial !== data.params.serial);
|
|
@@ -1469,6 +1781,7 @@ export class Frontend {
|
|
|
1469
1781
|
config.whiteList = config.whiteList.filter((name) => name !== data.params.name);
|
|
1470
1782
|
}
|
|
1471
1783
|
}
|
|
1784
|
+
// Add the serial to the blackList
|
|
1472
1785
|
if (isValidArray(config.blackList)) {
|
|
1473
1786
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1474
1787
|
config.blackList.push(data.params.serial);
|
|
@@ -1495,102 +1808,196 @@ export class Frontend {
|
|
|
1495
1808
|
return;
|
|
1496
1809
|
}
|
|
1497
1810
|
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Sends a WebSocket message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
1813
|
+
*
|
|
1814
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1815
|
+
* @param {string} time - The time string of the message
|
|
1816
|
+
* @param {string} name - The logger name of the message
|
|
1817
|
+
* @param {string} message - The content of the message.
|
|
1818
|
+
*/
|
|
1498
1819
|
wssSendMessage(level, time, name, message) {
|
|
1499
1820
|
if (!level || !time || !name || !message)
|
|
1500
1821
|
return;
|
|
1822
|
+
// Remove ANSI escape codes from the message
|
|
1823
|
+
// eslint-disable-next-line no-control-regex
|
|
1501
1824
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1825
|
+
// Remove leading asterisks from the message
|
|
1502
1826
|
message = message.replace(/^\*+/, '');
|
|
1827
|
+
// Replace all occurrences of \t and \n
|
|
1503
1828
|
message = message.replace(/[\t\n]/g, '');
|
|
1829
|
+
// Remove non-printable characters
|
|
1830
|
+
// eslint-disable-next-line no-control-regex
|
|
1504
1831
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1832
|
+
// Replace all occurrences of \" with "
|
|
1505
1833
|
message = message.replace(/\\"/g, '"');
|
|
1834
|
+
// Replace all occurrences of angle-brackets with < and >"
|
|
1835
|
+
message = message.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1836
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1506
1837
|
const maxContinuousLength = 100;
|
|
1507
1838
|
const keepStartLength = 20;
|
|
1508
1839
|
const keepEndLength = 20;
|
|
1840
|
+
// Split the message into words
|
|
1509
1841
|
message = message
|
|
1510
1842
|
.split(' ')
|
|
1511
1843
|
.map((word) => {
|
|
1844
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1512
1845
|
if (word.length > maxContinuousLength) {
|
|
1513
1846
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1514
1847
|
}
|
|
1515
1848
|
return word;
|
|
1516
1849
|
})
|
|
1517
1850
|
.join(' ');
|
|
1851
|
+
// Send the message to all connected clients
|
|
1518
1852
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1519
1853
|
if (client.readyState === WebSocket.OPEN) {
|
|
1520
1854
|
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1521
1855
|
}
|
|
1522
1856
|
});
|
|
1523
1857
|
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1860
|
+
*
|
|
1861
|
+
* @param {string} changed - The changed value. If null, the whole page will be refreshed.
|
|
1862
|
+
* possible values:
|
|
1863
|
+
* - 'matterbridgeLatestVersion'
|
|
1864
|
+
* - 'matterbridgeAdvertise'
|
|
1865
|
+
* - 'online'
|
|
1866
|
+
* - 'offline'
|
|
1867
|
+
* - 'reachability'
|
|
1868
|
+
* - 'settings'
|
|
1869
|
+
* - 'plugins'
|
|
1870
|
+
* - 'devices'
|
|
1871
|
+
* - 'fabrics'
|
|
1872
|
+
* - 'sessions'
|
|
1873
|
+
*/
|
|
1524
1874
|
wssSendRefreshRequired(changed = null) {
|
|
1525
1875
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1876
|
+
// Send the message to all connected clients
|
|
1526
1877
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1527
1878
|
if (client.readyState === WebSocket.OPEN) {
|
|
1528
1879
|
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
|
|
1529
1880
|
}
|
|
1530
1881
|
});
|
|
1531
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1885
|
+
*
|
|
1886
|
+
*/
|
|
1532
1887
|
wssSendRestartRequired(snackbar = true) {
|
|
1533
1888
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
1534
1889
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1535
1890
|
if (snackbar === true)
|
|
1536
1891
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
1892
|
+
// Send the message to all connected clients
|
|
1537
1893
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1538
1894
|
if (client.readyState === WebSocket.OPEN) {
|
|
1539
1895
|
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1540
1896
|
}
|
|
1541
1897
|
});
|
|
1542
1898
|
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
1901
|
+
*
|
|
1902
|
+
*/
|
|
1543
1903
|
wssSendUpdateRequired() {
|
|
1544
1904
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1545
1905
|
this.matterbridge.matterbridgeInformation.updateRequired = true;
|
|
1906
|
+
// Send the message to all connected clients
|
|
1546
1907
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1547
1908
|
if (client.readyState === WebSocket.OPEN) {
|
|
1548
1909
|
client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
|
|
1549
1910
|
}
|
|
1550
1911
|
});
|
|
1551
1912
|
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Sends a memory update message to all connected clients.
|
|
1915
|
+
*
|
|
1916
|
+
*/
|
|
1552
1917
|
wssSendCpuUpdate(cpuUsage) {
|
|
1553
1918
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
1919
|
+
// Send the message to all connected clients
|
|
1554
1920
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1555
1921
|
if (client.readyState === WebSocket.OPEN) {
|
|
1556
1922
|
client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
|
|
1557
1923
|
}
|
|
1558
1924
|
});
|
|
1559
1925
|
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Sends a cpu update message to all connected clients.
|
|
1928
|
+
*
|
|
1929
|
+
*/
|
|
1560
1930
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1561
1931
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
1932
|
+
// Send the message to all connected clients
|
|
1562
1933
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1563
1934
|
if (client.readyState === WebSocket.OPEN) {
|
|
1564
1935
|
client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
|
|
1565
1936
|
}
|
|
1566
1937
|
});
|
|
1567
1938
|
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Sends a memory update message to all connected clients.
|
|
1941
|
+
*
|
|
1942
|
+
*/
|
|
1568
1943
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1569
1944
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
1945
|
+
// Send the message to all connected clients
|
|
1570
1946
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1571
1947
|
if (client.readyState === WebSocket.OPEN) {
|
|
1572
1948
|
client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
|
|
1573
1949
|
}
|
|
1574
1950
|
});
|
|
1575
1951
|
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Sends a cpu update message to all connected clients.
|
|
1954
|
+
* @param {string} message - The message to send.
|
|
1955
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message.
|
|
1956
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
|
|
1957
|
+
*
|
|
1958
|
+
*/
|
|
1576
1959
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1577
1960
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
1961
|
+
// Send the message to all connected clients
|
|
1578
1962
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1579
1963
|
if (client.readyState === WebSocket.OPEN) {
|
|
1580
1964
|
client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { message, timeout, severity } }));
|
|
1581
1965
|
}
|
|
1582
1966
|
});
|
|
1583
1967
|
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
1970
|
+
*
|
|
1971
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
1972
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
1973
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
1974
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
1975
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
1976
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
1977
|
+
*
|
|
1978
|
+
* @remarks
|
|
1979
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
1980
|
+
* with the updated attribute information.
|
|
1981
|
+
*/
|
|
1584
1982
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
|
|
1585
1983
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
1984
|
+
// Send the message to all connected clients
|
|
1586
1985
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1587
1986
|
if (client.readyState === WebSocket.OPEN) {
|
|
1588
1987
|
client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
|
|
1589
1988
|
}
|
|
1590
1989
|
});
|
|
1591
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Sends a message to all connected clients.
|
|
1993
|
+
* @param {number} id - The message id.
|
|
1994
|
+
* @param {string} method - The message method.
|
|
1995
|
+
* @param {Record<string, string | number | boolean>} params - The message parameters.
|
|
1996
|
+
*
|
|
1997
|
+
*/
|
|
1592
1998
|
wssBroadcastMessage(id, method, params) {
|
|
1593
1999
|
this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
|
|
2000
|
+
// Send the message to all connected clients
|
|
1594
2001
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1595
2002
|
if (client.readyState === WebSocket.OPEN) {
|
|
1596
2003
|
client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
|
|
@@ -1598,3 +2005,4 @@ export class Frontend {
|
|
|
1598
2005
|
});
|
|
1599
2006
|
}
|
|
1600
2007
|
}
|
|
2008
|
+
//# sourceMappingURL=frontend.js.map
|