matterbridge 3.1.1-dev-20250703-80c685d → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -5
- package/README.md +5 -25
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +93 -6
- package/dist/cli.js.map +1 -0
- package/dist/cliEmitter.d.ts +34 -0
- package/dist/cliEmitter.d.ts.map +1 -0
- package/dist/cliEmitter.js +36 -0
- package/dist/cliEmitter.js.map +1 -0
- package/dist/clusters/export.d.ts +2 -0
- package/dist/clusters/export.d.ts.map +1 -0
- package/dist/clusters/export.js +2 -0
- package/dist/clusters/export.js.map +1 -0
- package/dist/defaultConfigSchema.d.ts +28 -0
- package/dist/defaultConfigSchema.d.ts.map +1 -0
- package/dist/defaultConfigSchema.js +24 -0
- package/dist/defaultConfigSchema.js.map +1 -0
- package/dist/deviceManager.d.ts +112 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +94 -1
- package/dist/deviceManager.js.map +1 -0
- package/dist/devices/batteryStorage.d.ts +48 -0
- package/dist/devices/batteryStorage.d.ts.map +1 -0
- package/dist/devices/batteryStorage.js +48 -1
- package/dist/devices/batteryStorage.js.map +1 -0
- package/dist/devices/evse.d.ts +75 -0
- package/dist/devices/evse.d.ts.map +1 -0
- package/dist/devices/evse.js +74 -10
- package/dist/devices/evse.js.map +1 -0
- package/dist/devices/export.d.ts +9 -0
- package/dist/devices/export.d.ts.map +1 -0
- package/dist/devices/export.js +2 -0
- package/dist/devices/export.js.map +1 -0
- package/dist/devices/heatPump.d.ts +47 -0
- package/dist/devices/heatPump.d.ts.map +1 -0
- package/dist/devices/heatPump.js +50 -2
- package/dist/devices/heatPump.js.map +1 -0
- package/dist/devices/laundryDryer.d.ts +87 -0
- package/dist/devices/laundryDryer.d.ts.map +1 -0
- package/dist/devices/laundryDryer.js +83 -6
- package/dist/devices/laundryDryer.js.map +1 -0
- package/dist/devices/laundryWasher.d.ts +242 -0
- package/dist/devices/laundryWasher.d.ts.map +1 -0
- package/dist/devices/laundryWasher.js +91 -7
- package/dist/devices/laundryWasher.js.map +1 -0
- package/dist/devices/roboticVacuumCleaner.d.ts +103 -0
- package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/devices/roboticVacuumCleaner.js +82 -6
- package/dist/devices/roboticVacuumCleaner.js.map +1 -0
- package/dist/devices/solarPower.d.ts +40 -0
- package/dist/devices/solarPower.d.ts.map +1 -0
- package/dist/devices/solarPower.js +38 -0
- package/dist/devices/solarPower.js.map +1 -0
- package/dist/devices/waterHeater.d.ts +111 -0
- package/dist/devices/waterHeater.d.ts.map +1 -0
- package/dist/devices/waterHeater.js +82 -2
- package/dist/devices/waterHeater.js.map +1 -0
- package/dist/frontend.d.ts +302 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +455 -27
- package/dist/frontend.js.map +1 -0
- package/dist/globalMatterbridge.d.ts +59 -0
- package/dist/globalMatterbridge.d.ts.map +1 -0
- package/dist/globalMatterbridge.js +47 -0
- package/dist/globalMatterbridge.js.map +1 -0
- package/dist/helpers.d.ts +48 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +53 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -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 +3 -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 +3 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matterbridge.d.ts +450 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +819 -68
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +36 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +1340 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +61 -1
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +709 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +579 -15
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +36 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +1179 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +1027 -42
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +3198 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +322 -12
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +310 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +233 -0
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +192 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +25 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/pluginManager.d.ts +291 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +269 -5
- package/dist/pluginManager.js.map +1 -0
- package/dist/shelly.d.ts +174 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +168 -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 +59 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +54 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/colorUtils.d.ts +117 -0
- package/dist/utils/colorUtils.d.ts.map +1 -0
- package/dist/utils/colorUtils.js +263 -2
- package/dist/utils/colorUtils.js.map +1 -0
- package/dist/utils/commandLine.d.ts +59 -0
- package/dist/utils/commandLine.d.ts.map +1 -0
- package/dist/utils/commandLine.js +54 -0
- package/dist/utils/commandLine.js.map +1 -0
- package/dist/utils/copyDirectory.d.ts +33 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +38 -1
- package/dist/utils/copyDirectory.js.map +1 -0
- package/dist/utils/createDirectory.d.ts +34 -0
- package/dist/utils/createDirectory.d.ts.map +1 -0
- package/dist/utils/createDirectory.js +33 -0
- package/dist/utils/createDirectory.js.map +1 -0
- package/dist/utils/createZip.d.ts +39 -0
- package/dist/utils/createZip.d.ts.map +1 -0
- package/dist/utils/createZip.js +47 -2
- package/dist/utils/createZip.js.map +1 -0
- package/dist/utils/deepCopy.d.ts +32 -0
- package/dist/utils/deepCopy.d.ts.map +1 -0
- package/dist/utils/deepCopy.js +39 -0
- package/dist/utils/deepCopy.js.map +1 -0
- package/dist/utils/deepEqual.d.ts +54 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/deepEqual.js +72 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +12 -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 +49 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/hex.js +58 -0
- package/dist/utils/hex.js.map +1 -0
- package/dist/utils/isvalid.d.ts +103 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +101 -0
- package/dist/utils/isvalid.js.map +1 -0
- package/dist/utils/network.d.ts +76 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +83 -5
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/spawn.d.ts +11 -0
- package/dist/utils/spawn.d.ts.map +1 -0
- package/dist/utils/spawn.js +79 -63
- package/dist/utils/spawn.js.map +1 -0
- package/dist/utils/wait.d.ts +56 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +62 -9
- package/dist/utils/wait.js.map +1 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +3 -2
package/dist/frontend.js
CHANGED
|
@@ -1,31 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Frontend.
|
|
3
|
+
*
|
|
4
|
+
* @file frontend.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @created 2025-01-13
|
|
7
|
+
* @version 1.1.0
|
|
8
|
+
* @license Apache-2.0
|
|
9
|
+
*
|
|
10
|
+
* Copyright 2025, 2026, 2027 Luca Liguori.
|
|
11
|
+
*
|
|
12
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
* you may not use this file except in compliance with the License.
|
|
14
|
+
* You may obtain a copy of the License at
|
|
15
|
+
*
|
|
16
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
*
|
|
18
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
* See the License for the specific language governing permissions and
|
|
22
|
+
* limitations under the License.
|
|
23
|
+
*/
|
|
24
|
+
// Node modules
|
|
1
25
|
import { createServer } from 'node:http';
|
|
2
26
|
import https from 'node:https';
|
|
3
27
|
import os from 'node:os';
|
|
4
28
|
import path from 'node:path';
|
|
5
29
|
import { promises as fs } from 'node:fs';
|
|
30
|
+
import EventEmitter from 'node:events';
|
|
31
|
+
// Third-party modules
|
|
6
32
|
import express from 'express';
|
|
7
33
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
8
34
|
import multer from 'multer';
|
|
35
|
+
// AnsiLogger module
|
|
9
36
|
import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW, nt } from 'node-ansi-logger';
|
|
37
|
+
// @matter
|
|
10
38
|
import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
|
|
11
39
|
import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
|
|
40
|
+
// Matterbridge
|
|
12
41
|
import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
|
|
13
42
|
import { plg } from './matterbridgeTypes.js';
|
|
14
43
|
import { capitalizeFirstLetter } from './matterbridgeEndpointHelpers.js';
|
|
15
|
-
import
|
|
44
|
+
import { cliEmitter, lastCpuUsage } from './cliEmitter.js';
|
|
45
|
+
/**
|
|
46
|
+
* Websocket message ID for logging.
|
|
47
|
+
*
|
|
48
|
+
* @constant {number}
|
|
49
|
+
*/
|
|
16
50
|
export const WS_ID_LOG = 0;
|
|
51
|
+
/**
|
|
52
|
+
* Websocket message ID indicating a refresh is needed.
|
|
53
|
+
*
|
|
54
|
+
* @constant {number}
|
|
55
|
+
*/
|
|
17
56
|
export const WS_ID_REFRESH_NEEDED = 1;
|
|
57
|
+
/**
|
|
58
|
+
* Websocket message ID indicating a restart is needed.
|
|
59
|
+
*
|
|
60
|
+
* @constant {number}
|
|
61
|
+
*/
|
|
18
62
|
export const WS_ID_RESTART_NEEDED = 2;
|
|
63
|
+
/**
|
|
64
|
+
* Websocket message ID indicating a cpu update.
|
|
65
|
+
*
|
|
66
|
+
* @constant {number}
|
|
67
|
+
*/
|
|
19
68
|
export const WS_ID_CPU_UPDATE = 3;
|
|
69
|
+
/**
|
|
70
|
+
* Websocket message ID indicating a memory update.
|
|
71
|
+
*
|
|
72
|
+
* @constant {number}
|
|
73
|
+
*/
|
|
20
74
|
export const WS_ID_MEMORY_UPDATE = 4;
|
|
75
|
+
/**
|
|
76
|
+
* Websocket message ID indicating an uptime update.
|
|
77
|
+
*
|
|
78
|
+
* @constant {number}
|
|
79
|
+
*/
|
|
21
80
|
export const WS_ID_UPTIME_UPDATE = 5;
|
|
81
|
+
/**
|
|
82
|
+
* Websocket message ID indicating a snackbar message.
|
|
83
|
+
*
|
|
84
|
+
* @constant {number}
|
|
85
|
+
*/
|
|
22
86
|
export const WS_ID_SNACKBAR = 6;
|
|
87
|
+
/**
|
|
88
|
+
* Websocket message ID indicating matterbridge has un update available.
|
|
89
|
+
*
|
|
90
|
+
* @constant {number}
|
|
91
|
+
*/
|
|
23
92
|
export const WS_ID_UPDATE_NEEDED = 7;
|
|
93
|
+
/**
|
|
94
|
+
* Websocket message ID indicating a state update.
|
|
95
|
+
*
|
|
96
|
+
* @constant {number}
|
|
97
|
+
*/
|
|
24
98
|
export const WS_ID_STATEUPDATE = 8;
|
|
99
|
+
/**
|
|
100
|
+
* Websocket message ID indicating to close a permanent snackbar message.
|
|
101
|
+
*
|
|
102
|
+
* @constant {number}
|
|
103
|
+
*/
|
|
25
104
|
export const WS_ID_CLOSE_SNACKBAR = 9;
|
|
105
|
+
/**
|
|
106
|
+
* Websocket message ID indicating a shelly system update.
|
|
107
|
+
* check:
|
|
108
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/check
|
|
109
|
+
* perform:
|
|
110
|
+
* curl -k http://127.0.0.1:8101/api/updates/sys/perform
|
|
111
|
+
*
|
|
112
|
+
* @constant {number}
|
|
113
|
+
*/
|
|
26
114
|
export const WS_ID_SHELLY_SYS_UPDATE = 100;
|
|
115
|
+
/**
|
|
116
|
+
* Websocket message ID indicating a shelly main update.
|
|
117
|
+
* check:
|
|
118
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/check
|
|
119
|
+
* perform:
|
|
120
|
+
* curl -k http://127.0.0.1:8101/api/updates/main/perform
|
|
121
|
+
*
|
|
122
|
+
* @constant {number}
|
|
123
|
+
*/
|
|
27
124
|
export const WS_ID_SHELLY_MAIN_UPDATE = 101;
|
|
28
|
-
export class Frontend {
|
|
125
|
+
export class Frontend extends EventEmitter {
|
|
29
126
|
matterbridge;
|
|
30
127
|
log;
|
|
31
128
|
port = 8283;
|
|
@@ -35,8 +132,9 @@ export class Frontend {
|
|
|
35
132
|
httpsServer;
|
|
36
133
|
webSocketServer;
|
|
37
134
|
constructor(matterbridge) {
|
|
135
|
+
super();
|
|
38
136
|
this.matterbridge = matterbridge;
|
|
39
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
137
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
40
138
|
}
|
|
41
139
|
set logLevel(logLevel) {
|
|
42
140
|
this.log.logLevel = logLevel;
|
|
@@ -44,16 +142,54 @@ export class Frontend {
|
|
|
44
142
|
async start(port = 8283) {
|
|
45
143
|
this.port = port;
|
|
46
144
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
145
|
+
// Initialize multer with the upload directory
|
|
47
146
|
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
48
147
|
await fs.mkdir(uploadDir, { recursive: true });
|
|
49
148
|
const upload = multer({ dest: uploadDir });
|
|
149
|
+
// Create the express app that serves the frontend
|
|
50
150
|
this.expressApp = express();
|
|
151
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
152
|
+
/*
|
|
153
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
154
|
+
for (const method of methods) {
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
159
|
+
try {
|
|
160
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
161
|
+
return original(path, ...rest);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
*/
|
|
169
|
+
// Log all requests to the server for debugging
|
|
170
|
+
/*
|
|
171
|
+
this.expressApp.use((req, res, next) => {
|
|
172
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
*/
|
|
176
|
+
// Serve static files from '/static' endpoint
|
|
51
177
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
52
178
|
if (!hasParameter('ssl')) {
|
|
53
|
-
|
|
179
|
+
// Create an HTTP server and attach the express app
|
|
180
|
+
try {
|
|
181
|
+
this.httpServer = createServer(this.expressApp);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
this.log.error(`Failed to create HTTP server: ${error}`);
|
|
185
|
+
this.emit('server_error', error);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Listen on the specified port
|
|
54
189
|
if (hasParameter('ingress')) {
|
|
55
190
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
56
191
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
192
|
+
this.emit('server_listening', 'http', this.port, '0.0.0.0');
|
|
57
193
|
});
|
|
58
194
|
}
|
|
59
195
|
else {
|
|
@@ -62,6 +198,7 @@ export class Frontend {
|
|
|
62
198
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
63
199
|
if (this.matterbridge.systemInformation.ipv6Address !== '')
|
|
64
200
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
201
|
+
this.emit('server_listening', 'http', this.port);
|
|
65
202
|
});
|
|
66
203
|
}
|
|
67
204
|
this.httpServer.on('error', (error) => {
|
|
@@ -75,10 +212,12 @@ export class Frontend {
|
|
|
75
212
|
break;
|
|
76
213
|
}
|
|
77
214
|
this.initializeError = true;
|
|
215
|
+
this.emit('server_error', error);
|
|
78
216
|
return;
|
|
79
217
|
});
|
|
80
218
|
}
|
|
81
219
|
else {
|
|
220
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
82
221
|
let cert;
|
|
83
222
|
try {
|
|
84
223
|
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
@@ -86,6 +225,7 @@ export class Frontend {
|
|
|
86
225
|
}
|
|
87
226
|
catch (error) {
|
|
88
227
|
this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
|
|
228
|
+
this.emit('server_error', error);
|
|
89
229
|
return;
|
|
90
230
|
}
|
|
91
231
|
let key;
|
|
@@ -95,6 +235,7 @@ export class Frontend {
|
|
|
95
235
|
}
|
|
96
236
|
catch (error) {
|
|
97
237
|
this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
|
|
238
|
+
this.emit('server_error', error);
|
|
98
239
|
return;
|
|
99
240
|
}
|
|
100
241
|
let ca;
|
|
@@ -106,10 +247,20 @@ export class Frontend {
|
|
|
106
247
|
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
107
248
|
}
|
|
108
249
|
const serverOptions = { cert, key, ca };
|
|
109
|
-
|
|
250
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
251
|
+
try {
|
|
252
|
+
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
this.log.error(`Failed to create HTTPS server: ${error}`);
|
|
256
|
+
this.emit('server_error', error);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Listen on the specified port
|
|
110
260
|
if (hasParameter('ingress')) {
|
|
111
261
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
112
262
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
263
|
+
this.emit('server_listening', 'https', this.port, '0.0.0.0');
|
|
113
264
|
});
|
|
114
265
|
}
|
|
115
266
|
else {
|
|
@@ -118,6 +269,7 @@ export class Frontend {
|
|
|
118
269
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
119
270
|
if (this.matterbridge.systemInformation.ipv6Address !== '')
|
|
120
271
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
272
|
+
this.emit('server_listening', 'https', this.port);
|
|
121
273
|
});
|
|
122
274
|
}
|
|
123
275
|
this.httpsServer.on('error', (error) => {
|
|
@@ -131,21 +283,24 @@ export class Frontend {
|
|
|
131
283
|
break;
|
|
132
284
|
}
|
|
133
285
|
this.initializeError = true;
|
|
286
|
+
this.emit('server_error', error);
|
|
134
287
|
return;
|
|
135
288
|
});
|
|
136
289
|
}
|
|
137
290
|
if (this.initializeError)
|
|
138
291
|
return;
|
|
292
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
139
293
|
const wssPort = this.port;
|
|
140
294
|
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
141
295
|
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
142
296
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
143
297
|
const clientIp = request.socket.remoteAddress;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
298
|
+
// Set the global logger callback for the WebSocketServer
|
|
299
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
300
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
301
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
302
|
+
if (this.matterbridge.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridge.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
303
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
149
304
|
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), callbackLogLevel);
|
|
150
305
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
151
306
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -175,11 +330,12 @@ export class Frontend {
|
|
|
175
330
|
});
|
|
176
331
|
this.webSocketServer.on('listening', () => {
|
|
177
332
|
this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
|
|
333
|
+
this.emit('websocket_server_listening', wssHost);
|
|
178
334
|
});
|
|
179
335
|
this.webSocketServer.on('error', (ws, error) => {
|
|
180
336
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
181
337
|
});
|
|
182
|
-
|
|
338
|
+
// Subscribe to cli events
|
|
183
339
|
cliEmitter.removeAllListeners();
|
|
184
340
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
185
341
|
this.wssSendUptimeUpdate(systemUptime, processUptime);
|
|
@@ -190,6 +346,8 @@ export class Frontend {
|
|
|
190
346
|
cliEmitter.on('cpu', (cpuUsage) => {
|
|
191
347
|
this.wssSendCpuUpdate(cpuUsage);
|
|
192
348
|
});
|
|
349
|
+
// Endpoint to validate login code
|
|
350
|
+
// curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
|
|
193
351
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
194
352
|
const { password } = req.body;
|
|
195
353
|
this.log.debug('The frontend sent /api/login', password);
|
|
@@ -208,23 +366,27 @@ export class Frontend {
|
|
|
208
366
|
this.log.warn('/api/login error wrong password');
|
|
209
367
|
res.json({ valid: false });
|
|
210
368
|
}
|
|
369
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
211
370
|
}
|
|
212
371
|
catch (error) {
|
|
213
372
|
this.log.error('/api/login error getting password');
|
|
214
373
|
res.json({ valid: false });
|
|
215
374
|
}
|
|
216
375
|
});
|
|
376
|
+
// Endpoint to provide health check for docker
|
|
217
377
|
this.expressApp.get('/health', (req, res) => {
|
|
218
378
|
this.log.debug('Express received /health');
|
|
219
379
|
const healthStatus = {
|
|
220
|
-
status: 'ok',
|
|
221
|
-
uptime: process.uptime(),
|
|
222
|
-
timestamp: new Date().toISOString(),
|
|
380
|
+
status: 'ok', // Indicate service is healthy
|
|
381
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
382
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
223
383
|
};
|
|
224
384
|
res.status(200).json(healthStatus);
|
|
225
385
|
});
|
|
386
|
+
// Endpoint to provide memory usage details
|
|
226
387
|
this.expressApp.get('/memory', async (req, res) => {
|
|
227
388
|
this.log.debug('Express received /memory');
|
|
389
|
+
// Memory usage from process
|
|
228
390
|
const memoryUsageRaw = process.memoryUsage();
|
|
229
391
|
const memoryUsage = {
|
|
230
392
|
rss: this.formatMemoryUsage(memoryUsageRaw.rss),
|
|
@@ -233,10 +395,13 @@ export class Frontend {
|
|
|
233
395
|
external: this.formatMemoryUsage(memoryUsageRaw.external),
|
|
234
396
|
arrayBuffers: this.formatMemoryUsage(memoryUsageRaw.arrayBuffers),
|
|
235
397
|
};
|
|
398
|
+
// V8 heap statistics
|
|
236
399
|
const { default: v8 } = await import('node:v8');
|
|
237
400
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
238
401
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
402
|
+
// Format heapStats
|
|
239
403
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, this.formatMemoryUsage(value)]));
|
|
404
|
+
// Format heapSpaces
|
|
240
405
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
241
406
|
...space,
|
|
242
407
|
space_size: this.formatMemoryUsage(space.space_size),
|
|
@@ -254,19 +419,23 @@ export class Frontend {
|
|
|
254
419
|
};
|
|
255
420
|
res.status(200).json(memoryReport);
|
|
256
421
|
});
|
|
422
|
+
// Endpoint to provide settings
|
|
257
423
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
258
424
|
this.log.debug('The frontend sent /api/settings');
|
|
259
425
|
res.json(await this.getApiSettings());
|
|
260
426
|
});
|
|
427
|
+
// Endpoint to provide plugins
|
|
261
428
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
262
429
|
this.log.debug('The frontend sent /api/plugins');
|
|
263
430
|
res.json(this.getBaseRegisteredPlugins());
|
|
264
431
|
});
|
|
432
|
+
// Endpoint to provide devices
|
|
265
433
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
266
434
|
this.log.debug('The frontend sent /api/devices');
|
|
267
435
|
const devices = await this.getDevices();
|
|
268
436
|
res.json(devices);
|
|
269
437
|
});
|
|
438
|
+
// Endpoint to view the matterbridge log
|
|
270
439
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
271
440
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
272
441
|
try {
|
|
@@ -279,6 +448,7 @@ export class Frontend {
|
|
|
279
448
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
280
449
|
}
|
|
281
450
|
});
|
|
451
|
+
// Endpoint to view the matter.js log
|
|
282
452
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
283
453
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
284
454
|
try {
|
|
@@ -291,6 +461,7 @@ export class Frontend {
|
|
|
291
461
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
292
462
|
}
|
|
293
463
|
});
|
|
464
|
+
// Endpoint to view the shelly log
|
|
294
465
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
295
466
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
296
467
|
try {
|
|
@@ -303,9 +474,11 @@ export class Frontend {
|
|
|
303
474
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
304
475
|
}
|
|
305
476
|
});
|
|
477
|
+
// Endpoint to download the matterbridge log
|
|
306
478
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
307
479
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
308
480
|
try {
|
|
481
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
309
482
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
|
|
310
483
|
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
|
|
311
484
|
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), data, 'utf-8');
|
|
@@ -315,16 +488,20 @@ export class Frontend {
|
|
|
315
488
|
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
316
489
|
}
|
|
317
490
|
res.type('text/plain');
|
|
491
|
+
// res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
318
492
|
res.download(path.join(os.tmpdir(), this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
493
|
+
/* istanbul ignore if */
|
|
319
494
|
if (error) {
|
|
320
495
|
this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
321
496
|
res.status(500).send('Error downloading the matterbridge log file');
|
|
322
497
|
}
|
|
323
498
|
});
|
|
324
499
|
});
|
|
500
|
+
// Endpoint to download the matter log
|
|
325
501
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
326
502
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile)}`);
|
|
327
503
|
try {
|
|
504
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
328
505
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
|
|
329
506
|
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'utf8');
|
|
330
507
|
await fs.writeFile(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), data, 'utf-8');
|
|
@@ -335,15 +512,18 @@ export class Frontend {
|
|
|
335
512
|
}
|
|
336
513
|
res.type('text/plain');
|
|
337
514
|
res.download(path.join(os.tmpdir(), this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
|
|
515
|
+
/* istanbul ignore if */
|
|
338
516
|
if (error) {
|
|
339
517
|
this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
340
518
|
res.status(500).send('Error downloading the matter log file');
|
|
341
519
|
}
|
|
342
520
|
});
|
|
343
521
|
});
|
|
522
|
+
// Endpoint to download the shelly log
|
|
344
523
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
345
524
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
346
525
|
try {
|
|
526
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
347
527
|
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
|
|
348
528
|
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
|
|
349
529
|
await fs.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
|
|
@@ -354,12 +534,14 @@ export class Frontend {
|
|
|
354
534
|
}
|
|
355
535
|
res.type('text/plain');
|
|
356
536
|
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
537
|
+
/* istanbul ignore if */
|
|
357
538
|
if (error) {
|
|
358
539
|
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
359
540
|
res.status(500).send('Error downloading Shelly system log file');
|
|
360
541
|
}
|
|
361
542
|
});
|
|
362
543
|
});
|
|
544
|
+
// Endpoint to download the matterbridge storage directory
|
|
363
545
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
364
546
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
365
547
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
@@ -370,6 +552,7 @@ export class Frontend {
|
|
|
370
552
|
}
|
|
371
553
|
});
|
|
372
554
|
});
|
|
555
|
+
// Endpoint to download the matter storage file
|
|
373
556
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
374
557
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
375
558
|
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.matterStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName));
|
|
@@ -380,6 +563,7 @@ export class Frontend {
|
|
|
380
563
|
}
|
|
381
564
|
});
|
|
382
565
|
});
|
|
566
|
+
// Endpoint to download the matterbridge plugin directory
|
|
383
567
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
384
568
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
385
569
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
@@ -390,6 +574,7 @@ export class Frontend {
|
|
|
390
574
|
}
|
|
391
575
|
});
|
|
392
576
|
});
|
|
577
|
+
// Endpoint to download the matterbridge plugin config files
|
|
393
578
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
394
579
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
395
580
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
@@ -400,6 +585,7 @@ export class Frontend {
|
|
|
400
585
|
}
|
|
401
586
|
});
|
|
402
587
|
});
|
|
588
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
403
589
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
404
590
|
this.log.debug('The frontend sent /api/download-backup');
|
|
405
591
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
@@ -409,6 +595,7 @@ export class Frontend {
|
|
|
409
595
|
}
|
|
410
596
|
});
|
|
411
597
|
});
|
|
598
|
+
// Endpoint to upload a package
|
|
412
599
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
413
600
|
const { filename } = req.body;
|
|
414
601
|
const file = req.file;
|
|
@@ -418,12 +605,16 @@ export class Frontend {
|
|
|
418
605
|
return;
|
|
419
606
|
}
|
|
420
607
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
608
|
+
// Define the path where the plugin file will be saved
|
|
421
609
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
422
610
|
try {
|
|
611
|
+
// Move the uploaded file to the specified path
|
|
423
612
|
await fs.rename(file.path, filePath);
|
|
424
613
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
614
|
+
// Install the plugin package
|
|
425
615
|
if (filename.endsWith('.tgz')) {
|
|
426
|
-
|
|
616
|
+
const { spawnCommand } = await import('./utils/spawn.js');
|
|
617
|
+
await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose']);
|
|
427
618
|
this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
|
|
428
619
|
this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
|
|
429
620
|
this.wssSendSnackbarMessage(`Installed package ${filename}`, 10, 'success');
|
|
@@ -440,6 +631,7 @@ export class Frontend {
|
|
|
440
631
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
441
632
|
}
|
|
442
633
|
});
|
|
634
|
+
// Fallback for routing (must be the last route)
|
|
443
635
|
this.expressApp.use((req, res) => {
|
|
444
636
|
this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
|
|
445
637
|
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
@@ -448,12 +640,15 @@ export class Frontend {
|
|
|
448
640
|
}
|
|
449
641
|
async stop() {
|
|
450
642
|
this.log.debug('Stopping the frontend...');
|
|
643
|
+
// Remove listeners from the express app
|
|
451
644
|
if (this.expressApp) {
|
|
452
645
|
this.expressApp.removeAllListeners();
|
|
453
646
|
this.expressApp = undefined;
|
|
454
647
|
this.log.debug('Frontend app closed successfully');
|
|
455
648
|
}
|
|
649
|
+
// Close the WebSocket server
|
|
456
650
|
if (this.webSocketServer) {
|
|
651
|
+
// Close all active connections
|
|
457
652
|
this.webSocketServer.clients.forEach((client) => {
|
|
458
653
|
if (client.readyState === WebSocket.OPEN) {
|
|
459
654
|
client.close();
|
|
@@ -473,6 +668,7 @@ export class Frontend {
|
|
|
473
668
|
this.webSocketServer.removeAllListeners();
|
|
474
669
|
this.webSocketServer = undefined;
|
|
475
670
|
}
|
|
671
|
+
// Close the http server
|
|
476
672
|
if (this.httpServer) {
|
|
477
673
|
await withTimeout(new Promise((resolve) => {
|
|
478
674
|
this.httpServer?.close((error) => {
|
|
@@ -489,6 +685,7 @@ export class Frontend {
|
|
|
489
685
|
this.httpServer = undefined;
|
|
490
686
|
this.log.debug('Frontend http server closed successfully');
|
|
491
687
|
}
|
|
688
|
+
// Close the https server
|
|
492
689
|
if (this.httpsServer) {
|
|
493
690
|
await withTimeout(new Promise((resolve) => {
|
|
494
691
|
this.httpsServer?.close((error) => {
|
|
@@ -507,6 +704,7 @@ export class Frontend {
|
|
|
507
704
|
}
|
|
508
705
|
this.log.debug('Frontend stopped successfully');
|
|
509
706
|
}
|
|
707
|
+
// Function to format bytes to KB, MB, or GB
|
|
510
708
|
formatMemoryUsage = (bytes) => {
|
|
511
709
|
if (bytes >= 1024 ** 3) {
|
|
512
710
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -518,6 +716,7 @@ export class Frontend {
|
|
|
518
716
|
return `${(bytes / 1024).toFixed(2)} KB`;
|
|
519
717
|
}
|
|
520
718
|
};
|
|
719
|
+
// Function to format system uptime with only the most significant unit
|
|
521
720
|
formatOsUpTime = (seconds) => {
|
|
522
721
|
if (seconds >= 86400) {
|
|
523
722
|
const days = Math.floor(seconds / 86400);
|
|
@@ -533,8 +732,13 @@ export class Frontend {
|
|
|
533
732
|
}
|
|
534
733
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
535
734
|
};
|
|
735
|
+
/**
|
|
736
|
+
* Retrieves the api settings data.
|
|
737
|
+
*
|
|
738
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
739
|
+
*/
|
|
536
740
|
async getApiSettings() {
|
|
537
|
-
|
|
741
|
+
// Update the system information
|
|
538
742
|
this.matterbridge.systemInformation.totalMemory = this.formatMemoryUsage(os.totalmem());
|
|
539
743
|
this.matterbridge.systemInformation.freeMemory = this.formatMemoryUsage(os.freemem());
|
|
540
744
|
this.matterbridge.systemInformation.systemUptime = this.formatOsUpTime(os.uptime());
|
|
@@ -543,6 +747,7 @@ export class Frontend {
|
|
|
543
747
|
this.matterbridge.systemInformation.rss = this.formatMemoryUsage(process.memoryUsage().rss);
|
|
544
748
|
this.matterbridge.systemInformation.heapTotal = this.formatMemoryUsage(process.memoryUsage().heapTotal);
|
|
545
749
|
this.matterbridge.systemInformation.heapUsed = this.formatMemoryUsage(process.memoryUsage().heapUsed);
|
|
750
|
+
// Update the matterbridge information
|
|
546
751
|
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
547
752
|
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
548
753
|
this.matterbridge.matterbridgeInformation.loggerLevel = this.matterbridge.log.logLevel;
|
|
@@ -561,6 +766,12 @@ export class Frontend {
|
|
|
561
766
|
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
562
767
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
563
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Retrieves the reachable attribute.
|
|
771
|
+
*
|
|
772
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
|
|
773
|
+
* @returns {boolean} The reachable attribute.
|
|
774
|
+
*/
|
|
564
775
|
getReachability(device) {
|
|
565
776
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
566
777
|
return false;
|
|
@@ -572,6 +783,12 @@ export class Frontend {
|
|
|
572
783
|
return true;
|
|
573
784
|
return false;
|
|
574
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Retrieves the power source attribute.
|
|
788
|
+
*
|
|
789
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice object.
|
|
790
|
+
* @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
|
|
791
|
+
*/
|
|
575
792
|
getPowerSource(endpoint) {
|
|
576
793
|
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
577
794
|
return undefined;
|
|
@@ -587,13 +804,21 @@ export class Frontend {
|
|
|
587
804
|
}
|
|
588
805
|
return;
|
|
589
806
|
};
|
|
807
|
+
// Root endpoint
|
|
590
808
|
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
591
809
|
return powerSource(endpoint);
|
|
810
|
+
// Child endpoints
|
|
592
811
|
for (const child of endpoint.getChildEndpoints()) {
|
|
593
812
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
594
813
|
return powerSource(child);
|
|
595
814
|
}
|
|
596
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Retrieves the matter pairing code from a given device.
|
|
818
|
+
*
|
|
819
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object to retrieve the QR pairing code from.
|
|
820
|
+
* @returns {ApiDevicesMatter | undefined} An ApiDevicesMatter object or undefined if not found.
|
|
821
|
+
*/
|
|
597
822
|
getMatterDataFromDevice(device) {
|
|
598
823
|
if (device.mode === 'server' && device.serverNode && device.serverContext) {
|
|
599
824
|
return {
|
|
@@ -605,6 +830,12 @@ export class Frontend {
|
|
|
605
830
|
};
|
|
606
831
|
}
|
|
607
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Retrieves the cluster text description from a given device.
|
|
835
|
+
*
|
|
836
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
837
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
838
|
+
*/
|
|
608
839
|
getClusterTextFromDevice(device) {
|
|
609
840
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
610
841
|
return '';
|
|
@@ -645,7 +876,19 @@ export class Frontend {
|
|
|
645
876
|
};
|
|
646
877
|
let attributes = '';
|
|
647
878
|
let supportedModes = [];
|
|
879
|
+
/*
|
|
880
|
+
Object.keys(device.behaviors.supported).forEach((clusterName) => {
|
|
881
|
+
const clusterBehavior = device.behaviors.supported[lowercaseFirstLetter(clusterName)] as ClusterBehavior.Type | undefined;
|
|
882
|
+
// console.log(`Device: ${device.deviceName} => Cluster: ${clusterName} Behavior: ${clusterBehavior?.id}`, clusterBehavior);
|
|
883
|
+
if (clusterBehavior && clusterBehavior.cluster && clusterBehavior.cluster.attributes) {
|
|
884
|
+
Object.entries(clusterBehavior.cluster.attributes).forEach(([attributeName, attribute]) => {
|
|
885
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName} Attribute: ${attributeName}`, attribute);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
*/
|
|
648
890
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
891
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
649
892
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
650
893
|
return;
|
|
651
894
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -737,8 +980,14 @@ export class Frontend {
|
|
|
737
980
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
738
981
|
attributes += `${getUserLabel(device)} `;
|
|
739
982
|
});
|
|
983
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
740
984
|
return attributes.trimStart().trimEnd();
|
|
741
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
988
|
+
*
|
|
989
|
+
* @returns {BaseRegisteredPlugin[]} An array of BaseRegisteredPlugin.
|
|
990
|
+
*/
|
|
742
991
|
getBaseRegisteredPlugins() {
|
|
743
992
|
const baseRegisteredPlugins = [];
|
|
744
993
|
for (const plugin of this.matterbridge.plugins) {
|
|
@@ -777,11 +1026,19 @@ export class Frontend {
|
|
|
777
1026
|
}
|
|
778
1027
|
return baseRegisteredPlugins;
|
|
779
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Retrieves the devices from Matterbridge.
|
|
1031
|
+
*
|
|
1032
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
1033
|
+
* @returns {Promise<ApiDevices[]>} A promise that resolves to an array of ApiDevices for the frontend.
|
|
1034
|
+
*/
|
|
780
1035
|
async getDevices(pluginName) {
|
|
781
1036
|
const devices = [];
|
|
782
1037
|
for (const device of this.matterbridge.devices.array()) {
|
|
1038
|
+
// Filter by pluginName if provided
|
|
783
1039
|
if (pluginName && pluginName !== device.plugin)
|
|
784
1040
|
continue;
|
|
1041
|
+
// Check if the device has the required properties
|
|
785
1042
|
if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
786
1043
|
continue;
|
|
787
1044
|
devices.push({
|
|
@@ -801,22 +1058,37 @@ export class Frontend {
|
|
|
801
1058
|
}
|
|
802
1059
|
return devices;
|
|
803
1060
|
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Retrieves the clusters from a given plugin and endpoint number.
|
|
1063
|
+
*
|
|
1064
|
+
* Response for /api/clusters
|
|
1065
|
+
*
|
|
1066
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1067
|
+
* @param {number} endpointNumber - The endpoint number.
|
|
1068
|
+
* @returns {ApiClustersResponse | undefined} A promise that resolves to the clusters or undefined if not found.
|
|
1069
|
+
*/
|
|
804
1070
|
getClusters(pluginName, endpointNumber) {
|
|
805
1071
|
const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
|
|
806
1072
|
if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.deviceName || !endpoint.serialNumber) {
|
|
807
1073
|
this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
808
1074
|
return;
|
|
809
1075
|
}
|
|
1076
|
+
// this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
|
|
1077
|
+
// Get the device types from the main endpoint
|
|
810
1078
|
const deviceTypes = [];
|
|
811
1079
|
const clusters = [];
|
|
812
1080
|
endpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
813
1081
|
deviceTypes.push(d.deviceType);
|
|
814
1082
|
});
|
|
1083
|
+
// Get the clusters from the main endpoint
|
|
815
1084
|
endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
816
1085
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
817
1086
|
return;
|
|
818
1087
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
819
1088
|
return;
|
|
1089
|
+
// console.log(
|
|
1090
|
+
// `${idn}${endpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
|
|
1091
|
+
// );
|
|
820
1092
|
clusters.push({
|
|
821
1093
|
endpoint: endpoint.number.toString(),
|
|
822
1094
|
id: 'main',
|
|
@@ -829,12 +1101,18 @@ export class Frontend {
|
|
|
829
1101
|
attributeLocalValue: attributeValue,
|
|
830
1102
|
});
|
|
831
1103
|
});
|
|
1104
|
+
// Get the child endpoints
|
|
832
1105
|
const childEndpoints = endpoint.getChildEndpoints();
|
|
1106
|
+
// if (childEndpoints.length === 0) {
|
|
1107
|
+
// this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1108
|
+
// }
|
|
833
1109
|
childEndpoints.forEach((childEndpoint) => {
|
|
834
1110
|
if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
|
|
835
1111
|
this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
836
1112
|
return;
|
|
837
1113
|
}
|
|
1114
|
+
// this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
|
|
1115
|
+
// Get the device types of the child endpoint
|
|
838
1116
|
const deviceTypes = [];
|
|
839
1117
|
childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
840
1118
|
deviceTypes.push(d.deviceType);
|
|
@@ -844,9 +1122,12 @@ export class Frontend {
|
|
|
844
1122
|
return;
|
|
845
1123
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
846
1124
|
return;
|
|
1125
|
+
// console.log(
|
|
1126
|
+
// `${idn}${childEndpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
|
|
1127
|
+
// );
|
|
847
1128
|
clusters.push({
|
|
848
1129
|
endpoint: childEndpoint.number.toString(),
|
|
849
|
-
id: childEndpoint.maybeId ?? 'null',
|
|
1130
|
+
id: childEndpoint.maybeId ?? 'null', // Never happens
|
|
850
1131
|
deviceTypes,
|
|
851
1132
|
clusterName: capitalizeFirstLetter(clusterName),
|
|
852
1133
|
clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
|
|
@@ -859,6 +1140,13 @@ export class Frontend {
|
|
|
859
1140
|
});
|
|
860
1141
|
return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, endpoint: endpoint.maybeNumber, deviceTypes, clusters };
|
|
861
1142
|
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Handles incoming websocket messages for the Matterbridge frontend.
|
|
1145
|
+
*
|
|
1146
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1147
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1148
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1149
|
+
*/
|
|
862
1150
|
async wsMessageHandler(client, message) {
|
|
863
1151
|
let data;
|
|
864
1152
|
try {
|
|
@@ -897,40 +1185,49 @@ export class Frontend {
|
|
|
897
1185
|
return;
|
|
898
1186
|
}
|
|
899
1187
|
this.wssSendSnackbarMessage(`Installing package ${data.params.packageName}...`, 0);
|
|
900
|
-
spawn
|
|
901
|
-
|
|
1188
|
+
const { spawnCommand } = await import('./utils/spawn.js');
|
|
1189
|
+
spawnCommand(this.matterbridge, 'npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
|
|
902
1190
|
.then((response) => {
|
|
903
1191
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
904
1192
|
this.wssSendCloseSnackbarMessage(`Installing package ${data.params.packageName}...`);
|
|
905
1193
|
this.wssSendSnackbarMessage(`Installed package ${data.params.packageName}`, 5, 'success');
|
|
906
1194
|
const packageName = data.params.packageName.replace(/@.*$/, '');
|
|
907
1195
|
if (data.params.restart === false && packageName !== 'matterbridge') {
|
|
1196
|
+
// The install comes from InstallPlugins
|
|
908
1197
|
this.matterbridge.plugins
|
|
909
1198
|
.add(packageName)
|
|
910
1199
|
.then((plugin) => {
|
|
911
1200
|
if (plugin) {
|
|
1201
|
+
// The plugin is not registered
|
|
912
1202
|
this.wssSendSnackbarMessage(`Added plugin ${packageName}`, 5, 'success');
|
|
913
1203
|
this.matterbridge.plugins
|
|
914
1204
|
.load(plugin, true, 'The plugin has been added', true)
|
|
1205
|
+
// eslint-disable-next-line promise/no-nesting
|
|
915
1206
|
.then(() => {
|
|
916
1207
|
this.wssSendSnackbarMessage(`Started plugin ${packageName}`, 5, 'success');
|
|
917
1208
|
this.wssSendRefreshRequired('plugins');
|
|
918
1209
|
return;
|
|
919
1210
|
})
|
|
1211
|
+
// eslint-disable-next-line promise/no-nesting
|
|
920
1212
|
.catch((_error) => {
|
|
1213
|
+
//
|
|
921
1214
|
});
|
|
922
1215
|
}
|
|
923
1216
|
else {
|
|
1217
|
+
// The plugin is already registered
|
|
924
1218
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
925
1219
|
this.wssSendRefreshRequired('plugins');
|
|
926
1220
|
this.wssSendRestartRequired();
|
|
927
1221
|
}
|
|
928
1222
|
return;
|
|
929
1223
|
})
|
|
1224
|
+
// eslint-disable-next-line promise/no-nesting
|
|
930
1225
|
.catch((_error) => {
|
|
1226
|
+
//
|
|
931
1227
|
});
|
|
932
1228
|
}
|
|
933
1229
|
else {
|
|
1230
|
+
// The package is matterbridge
|
|
934
1231
|
if (this.matterbridge.restartMode !== '') {
|
|
935
1232
|
this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
|
|
936
1233
|
this.matterbridge.shutdownProcess();
|
|
@@ -953,6 +1250,7 @@ export class Frontend {
|
|
|
953
1250
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
|
|
954
1251
|
return;
|
|
955
1252
|
}
|
|
1253
|
+
// The package is a plugin
|
|
956
1254
|
const plugin = this.matterbridge.plugins.get(data.params.packageName);
|
|
957
1255
|
if (plugin) {
|
|
958
1256
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
@@ -961,9 +1259,10 @@ export class Frontend {
|
|
|
961
1259
|
this.wssSendRefreshRequired('plugins');
|
|
962
1260
|
this.wssSendRefreshRequired('devices');
|
|
963
1261
|
}
|
|
1262
|
+
// Uninstall the package
|
|
964
1263
|
this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
|
|
965
|
-
spawn
|
|
966
|
-
|
|
1264
|
+
const { spawnCommand } = await import('./utils/spawn.js');
|
|
1265
|
+
spawnCommand(this.matterbridge, 'npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
|
|
967
1266
|
.then((response) => {
|
|
968
1267
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
969
1268
|
this.wssSendCloseSnackbarMessage(`Uninstalling package ${data.params.packageName}...`);
|
|
@@ -1001,6 +1300,7 @@ export class Frontend {
|
|
|
1001
1300
|
return;
|
|
1002
1301
|
})
|
|
1003
1302
|
.catch((_error) => {
|
|
1303
|
+
//
|
|
1004
1304
|
});
|
|
1005
1305
|
}
|
|
1006
1306
|
else {
|
|
@@ -1047,6 +1347,7 @@ export class Frontend {
|
|
|
1047
1347
|
return;
|
|
1048
1348
|
})
|
|
1049
1349
|
.catch((_error) => {
|
|
1350
|
+
//
|
|
1050
1351
|
});
|
|
1051
1352
|
}
|
|
1052
1353
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
|
|
@@ -1284,22 +1585,22 @@ export class Frontend {
|
|
|
1284
1585
|
if (isValidString(data.params.value, 4)) {
|
|
1285
1586
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1286
1587
|
if (data.params.value === 'Debug') {
|
|
1287
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1588
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1288
1589
|
}
|
|
1289
1590
|
else if (data.params.value === 'Info') {
|
|
1290
|
-
await this.matterbridge.setLogLevel("info");
|
|
1591
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1291
1592
|
}
|
|
1292
1593
|
else if (data.params.value === 'Notice') {
|
|
1293
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1594
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1294
1595
|
}
|
|
1295
1596
|
else if (data.params.value === 'Warn') {
|
|
1296
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1597
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1297
1598
|
}
|
|
1298
1599
|
else if (data.params.value === 'Error') {
|
|
1299
|
-
await this.matterbridge.setLogLevel("error");
|
|
1600
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1300
1601
|
}
|
|
1301
1602
|
else if (data.params.value === 'Fatal') {
|
|
1302
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1603
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1303
1604
|
}
|
|
1304
1605
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1305
1606
|
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
|
|
@@ -1310,6 +1611,7 @@ export class Frontend {
|
|
|
1310
1611
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1311
1612
|
this.matterbridge.matterbridgeInformation.fileLogger = data.params.value;
|
|
1312
1613
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1614
|
+
// Create the file logger for matterbridge
|
|
1313
1615
|
if (data.params.value)
|
|
1314
1616
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), this.matterbridge.matterbridgeInformation.loggerLevel, true);
|
|
1315
1617
|
else
|
|
@@ -1474,15 +1776,19 @@ export class Frontend {
|
|
|
1474
1776
|
return;
|
|
1475
1777
|
}
|
|
1476
1778
|
const config = plugin.configJson;
|
|
1779
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1477
1780
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1781
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1478
1782
|
if (select === 'serial')
|
|
1479
1783
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1480
1784
|
if (select === 'name')
|
|
1481
1785
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1482
1786
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
1787
|
+
// Remove postfix from the serial if it exists
|
|
1483
1788
|
if (config.postfix) {
|
|
1484
1789
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1485
1790
|
}
|
|
1791
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1486
1792
|
if (isValidArray(config.whiteList, 1)) {
|
|
1487
1793
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1488
1794
|
config.whiteList.push(data.params.serial);
|
|
@@ -1491,6 +1797,7 @@ export class Frontend {
|
|
|
1491
1797
|
config.whiteList.push(data.params.name);
|
|
1492
1798
|
}
|
|
1493
1799
|
}
|
|
1800
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1494
1801
|
if (isValidArray(config.blackList, 1)) {
|
|
1495
1802
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1496
1803
|
config.blackList = config.blackList.filter((item) => item !== data.params.serial);
|
|
@@ -1520,7 +1827,9 @@ export class Frontend {
|
|
|
1520
1827
|
return;
|
|
1521
1828
|
}
|
|
1522
1829
|
const config = plugin.configJson;
|
|
1830
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1523
1831
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
1832
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1524
1833
|
if (select === 'serial')
|
|
1525
1834
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1526
1835
|
if (select === 'name')
|
|
@@ -1529,6 +1838,7 @@ export class Frontend {
|
|
|
1529
1838
|
if (config.postfix) {
|
|
1530
1839
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1531
1840
|
}
|
|
1841
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1532
1842
|
if (isValidArray(config.whiteList, 1)) {
|
|
1533
1843
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1534
1844
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.serial);
|
|
@@ -1537,6 +1847,7 @@ export class Frontend {
|
|
|
1537
1847
|
config.whiteList = config.whiteList.filter((item) => item !== data.params.name);
|
|
1538
1848
|
}
|
|
1539
1849
|
}
|
|
1850
|
+
// Add the serial to the blackList
|
|
1540
1851
|
if (isValidArray(config.blackList)) {
|
|
1541
1852
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1542
1853
|
config.blackList.push(data.params.serial);
|
|
@@ -1569,114 +1880,230 @@ export class Frontend {
|
|
|
1569
1880
|
this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
|
|
1570
1881
|
}
|
|
1571
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
1885
|
+
*
|
|
1886
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1887
|
+
* @param {string} time - The time string of the message
|
|
1888
|
+
* @param {string} name - The logger name of the message
|
|
1889
|
+
* @param {string} message - The content of the message.
|
|
1890
|
+
*
|
|
1891
|
+
* @remarks
|
|
1892
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
1893
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
1894
|
+
* The function sends the message to all connected clients.
|
|
1895
|
+
*/
|
|
1572
1896
|
wssSendMessage(level, time, name, message) {
|
|
1573
1897
|
if (!level || !time || !name || !message)
|
|
1574
1898
|
return;
|
|
1899
|
+
// Remove ANSI escape codes from the message
|
|
1900
|
+
// eslint-disable-next-line no-control-regex
|
|
1575
1901
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1902
|
+
// Remove leading asterisks from the message
|
|
1576
1903
|
message = message.replace(/^\*+/, '');
|
|
1904
|
+
// Replace all occurrences of \t and \n
|
|
1577
1905
|
message = message.replace(/[\t\n]/g, '');
|
|
1906
|
+
// Remove non-printable characters
|
|
1907
|
+
// eslint-disable-next-line no-control-regex
|
|
1578
1908
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1909
|
+
// Replace all occurrences of \" with "
|
|
1579
1910
|
message = message.replace(/\\"/g, '"');
|
|
1911
|
+
// Replace all occurrences of angle-brackets with < and >"
|
|
1580
1912
|
message = message.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1913
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1581
1914
|
const maxContinuousLength = 100;
|
|
1582
1915
|
const keepStartLength = 20;
|
|
1583
1916
|
const keepEndLength = 20;
|
|
1917
|
+
// Split the message into words
|
|
1584
1918
|
message = message
|
|
1585
1919
|
.split(' ')
|
|
1586
1920
|
.map((word) => {
|
|
1921
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1587
1922
|
if (word.length > maxContinuousLength) {
|
|
1588
1923
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1589
1924
|
}
|
|
1590
1925
|
return word;
|
|
1591
1926
|
})
|
|
1592
1927
|
.join(' ');
|
|
1928
|
+
// Send the message to all connected clients
|
|
1593
1929
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1594
1930
|
if (client.readyState === WebSocket.OPEN) {
|
|
1595
1931
|
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1596
1932
|
}
|
|
1597
1933
|
});
|
|
1598
1934
|
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1937
|
+
*
|
|
1938
|
+
* @param {string} changed - The changed value. If null, the whole page will be refreshed.
|
|
1939
|
+
* possible values:
|
|
1940
|
+
* - 'matterbridgeLatestVersion'
|
|
1941
|
+
* - 'matterbridgeAdvertise'
|
|
1942
|
+
* - 'online'
|
|
1943
|
+
* - 'offline'
|
|
1944
|
+
* - 'reachability'
|
|
1945
|
+
* - 'settings'
|
|
1946
|
+
* - 'plugins'
|
|
1947
|
+
* - 'pluginsRestart'
|
|
1948
|
+
* - 'devices'
|
|
1949
|
+
* - 'fabrics'
|
|
1950
|
+
* - 'sessions'
|
|
1951
|
+
*/
|
|
1599
1952
|
wssSendRefreshRequired(changed = null) {
|
|
1600
1953
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1954
|
+
// Send the message to all connected clients
|
|
1601
1955
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1602
1956
|
if (client.readyState === WebSocket.OPEN) {
|
|
1603
1957
|
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
|
|
1604
1958
|
}
|
|
1605
1959
|
});
|
|
1606
1960
|
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1963
|
+
*
|
|
1964
|
+
* @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients.
|
|
1965
|
+
*/
|
|
1607
1966
|
wssSendRestartRequired(snackbar = true) {
|
|
1608
1967
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
1609
1968
|
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1610
1969
|
if (snackbar === true)
|
|
1611
1970
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
1971
|
+
// Send the message to all connected clients
|
|
1612
1972
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1613
1973
|
if (client.readyState === WebSocket.OPEN) {
|
|
1614
1974
|
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1615
1975
|
}
|
|
1616
1976
|
});
|
|
1617
1977
|
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
1980
|
+
*
|
|
1981
|
+
*/
|
|
1618
1982
|
wssSendUpdateRequired() {
|
|
1619
1983
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1620
1984
|
this.matterbridge.matterbridgeInformation.updateRequired = true;
|
|
1985
|
+
// Send the message to all connected clients
|
|
1621
1986
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1622
1987
|
if (client.readyState === WebSocket.OPEN) {
|
|
1623
1988
|
client.send(JSON.stringify({ id: WS_ID_UPDATE_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', params: {} }));
|
|
1624
1989
|
}
|
|
1625
1990
|
});
|
|
1626
1991
|
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Sends a cpu update message to all connected clients.
|
|
1994
|
+
*
|
|
1995
|
+
* @param {number} cpuUsage - The CPU usage percentage to send.
|
|
1996
|
+
*/
|
|
1627
1997
|
wssSendCpuUpdate(cpuUsage) {
|
|
1628
1998
|
if (hasParameter('debug'))
|
|
1629
1999
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
2000
|
+
// Send the message to all connected clients
|
|
1630
2001
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1631
2002
|
if (client.readyState === WebSocket.OPEN) {
|
|
1632
2003
|
client.send(JSON.stringify({ id: WS_ID_CPU_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', params: { cpuUsage } }));
|
|
1633
2004
|
}
|
|
1634
2005
|
});
|
|
1635
2006
|
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Sends a memory update message to all connected clients.
|
|
2009
|
+
*
|
|
2010
|
+
* @param {string} totalMemory - The total memory in bytes.
|
|
2011
|
+
* @param {string} freeMemory - The free memory in bytes.
|
|
2012
|
+
* @param {string} rss - The resident set size in bytes.
|
|
2013
|
+
* @param {string} heapTotal - The total heap memory in bytes.
|
|
2014
|
+
* @param {string} heapUsed - The used heap memory in bytes.
|
|
2015
|
+
* @param {string} external - The external memory in bytes.
|
|
2016
|
+
* @param {string} arrayBuffers - The array buffers memory in bytes.
|
|
2017
|
+
*/
|
|
1636
2018
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1637
2019
|
if (hasParameter('debug'))
|
|
1638
2020
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
2021
|
+
// Send the message to all connected clients
|
|
1639
2022
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1640
2023
|
if (client.readyState === WebSocket.OPEN) {
|
|
1641
2024
|
client.send(JSON.stringify({ id: WS_ID_MEMORY_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', params: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } }));
|
|
1642
2025
|
}
|
|
1643
2026
|
});
|
|
1644
2027
|
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Sends an uptime update message to all connected clients.
|
|
2030
|
+
*
|
|
2031
|
+
* @param {string} systemUptime - The system uptime in a human-readable format.
|
|
2032
|
+
* @param {string} processUptime - The process uptime in a human-readable format.
|
|
2033
|
+
*/
|
|
1645
2034
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1646
2035
|
if (hasParameter('debug'))
|
|
1647
2036
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
2037
|
+
// Send the message to all connected clients
|
|
1648
2038
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1649
2039
|
if (client.readyState === WebSocket.OPEN) {
|
|
1650
2040
|
client.send(JSON.stringify({ id: WS_ID_UPTIME_UPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', params: { systemUptime, processUptime } }));
|
|
1651
2041
|
}
|
|
1652
2042
|
});
|
|
1653
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Sends an open snackbar message to all connected clients.
|
|
2046
|
+
*
|
|
2047
|
+
* @param {string} message - The message to send.
|
|
2048
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message.
|
|
2049
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the snackbar message (default info).
|
|
2050
|
+
*/
|
|
1654
2051
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1655
2052
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
2053
|
+
// Send the message to all connected clients
|
|
1656
2054
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1657
2055
|
if (client.readyState === WebSocket.OPEN) {
|
|
1658
2056
|
client.send(JSON.stringify({ id: WS_ID_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message, timeout, severity } }));
|
|
1659
2057
|
}
|
|
1660
2058
|
});
|
|
1661
2059
|
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Sends a close snackbar message to all connected clients.
|
|
2062
|
+
*
|
|
2063
|
+
* @param {string} message - The message to send.
|
|
2064
|
+
*/
|
|
1662
2065
|
wssSendCloseSnackbarMessage(message) {
|
|
1663
2066
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
2067
|
+
// Send the message to all connected clients
|
|
1664
2068
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1665
2069
|
if (client.readyState === WebSocket.OPEN) {
|
|
1666
2070
|
client.send(JSON.stringify({ id: WS_ID_CLOSE_SNACKBAR, src: 'Matterbridge', dst: 'Frontend', params: { message } }));
|
|
1667
2071
|
}
|
|
1668
2072
|
});
|
|
1669
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
2076
|
+
*
|
|
2077
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
2078
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
2079
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
2080
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
2081
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
2082
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
2083
|
+
*
|
|
2084
|
+
* @remarks
|
|
2085
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
2086
|
+
* with the updated attribute information.
|
|
2087
|
+
*/
|
|
1670
2088
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, cluster, attribute, value) {
|
|
1671
2089
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
2090
|
+
// Send the message to all connected clients
|
|
1672
2091
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1673
2092
|
if (client.readyState === WebSocket.OPEN) {
|
|
1674
2093
|
client.send(JSON.stringify({ id: WS_ID_STATEUPDATE, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', params: { plugin, serialNumber, uniqueId, cluster, attribute, value } }));
|
|
1675
2094
|
}
|
|
1676
2095
|
});
|
|
1677
2096
|
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Sends a message to all connected clients.
|
|
2099
|
+
*
|
|
2100
|
+
* @param {number} id - The message id.
|
|
2101
|
+
* @param {string} method - The message method.
|
|
2102
|
+
* @param {Record<string, string | number | boolean>} params - The message parameters.
|
|
2103
|
+
*/
|
|
1678
2104
|
wssBroadcastMessage(id, method, params) {
|
|
1679
2105
|
this.log.debug(`Sending a broadcast message id ${id} method ${method} params ${debugStringify(params ?? {})} to all connected clients`);
|
|
2106
|
+
// Send the message to all connected clients
|
|
1680
2107
|
this.webSocketServer?.clients.forEach((client) => {
|
|
1681
2108
|
if (client.readyState === WebSocket.OPEN) {
|
|
1682
2109
|
client.send(JSON.stringify({ id, src: 'Matterbridge', dst: 'Frontend', method, params }));
|
|
@@ -1684,3 +2111,4 @@ export class Frontend {
|
|
|
1684
2111
|
});
|
|
1685
2112
|
}
|
|
1686
2113
|
}
|
|
2114
|
+
//# sourceMappingURL=frontend.js.map
|