@matterbridge/core 3.7.2-dev-20260331-ac050d8 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backendExpress.d.ts +18 -0
- package/dist/backendExpress.js +445 -0
- package/dist/backendWsServer.d.ts +35 -0
- package/dist/backendWsServer.js +307 -0
- package/dist/cli.js +2 -1
- package/dist/frontend.d.ts +16 -9
- package/dist/frontend.js +46 -36
- package/dist/jestutils/jestHelpers.d.ts +1 -1
- package/dist/jestutils/jestHelpers.js +19 -9
- package/dist/matterNode.js +16 -14
- package/dist/matterbridge.d.ts +0 -7
- package/dist/matterbridge.js +23 -59
- package/dist/pluginManager.d.ts +1 -0
- package/dist/pluginManager.js +14 -2
- package/package.json +5 -5
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
|
|
2
|
+
import { BroadcastServer } from '@matterbridge/thread';
|
|
3
|
+
import { hasParameter } from '@matterbridge/utils/cli';
|
|
4
|
+
import { inspectError } from '@matterbridge/utils/error';
|
|
5
|
+
import { isValidNumber, isValidString } from '@matterbridge/utils/validate';
|
|
6
|
+
import { withTimeout } from '@matterbridge/utils/wait';
|
|
7
|
+
import { AnsiLogger, CYAN, debugStringify, nf } from 'node-ansi-logger';
|
|
8
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
9
|
+
if (hasParameter('loader'))
|
|
10
|
+
console.log('\u001B[32mBackendWsServer loaded.\u001B[40;0m');
|
|
11
|
+
export class BackendsWsServer {
|
|
12
|
+
debug;
|
|
13
|
+
verbose;
|
|
14
|
+
webSocketServer;
|
|
15
|
+
log;
|
|
16
|
+
backend;
|
|
17
|
+
matterbridge;
|
|
18
|
+
server;
|
|
19
|
+
constructor(matterbridge, backend) {
|
|
20
|
+
this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-frontend') || hasParameter('verbose-frontend');
|
|
21
|
+
this.verbose = hasParameter('verbose') || hasParameter('verbose-frontend');
|
|
22
|
+
this.backend = backend;
|
|
23
|
+
this.matterbridge = matterbridge;
|
|
24
|
+
this.log = new AnsiLogger({
|
|
25
|
+
logName: 'BackendWsServer',
|
|
26
|
+
logNameColor: '\x1b[38;5;97m',
|
|
27
|
+
logTimestampFormat: 4,
|
|
28
|
+
logLevel: this.debug ? "debug" : "info",
|
|
29
|
+
});
|
|
30
|
+
this.server = new BroadcastServer('frontend', this.log);
|
|
31
|
+
this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
|
|
32
|
+
}
|
|
33
|
+
destroy() {
|
|
34
|
+
this.server.off('broadcast_message', this.broadcastMsgHandler.bind(this));
|
|
35
|
+
this.server.close();
|
|
36
|
+
}
|
|
37
|
+
async broadcastMsgHandler(msg) {
|
|
38
|
+
if (this.server.isWorkerRequest(msg)) {
|
|
39
|
+
switch (msg.type) {
|
|
40
|
+
case 'get_log_level':
|
|
41
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
42
|
+
break;
|
|
43
|
+
case 'set_log_level':
|
|
44
|
+
this.log.logLevel = msg.params.logLevel;
|
|
45
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async start() {
|
|
51
|
+
this.log.debug(`Creating WebSocketServer...`);
|
|
52
|
+
this.webSocketServer = new WebSocketServer({ noServer: true });
|
|
53
|
+
this.backend.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
54
|
+
this.webSocketServer.on('connection', (websocket, request) => {
|
|
55
|
+
const clientIp = request.socket.remoteAddress;
|
|
56
|
+
this.log.info(`WebSocketServer client ${CYAN}${clientIp}${nf} connected to Matterbridge`);
|
|
57
|
+
let callbackLogLevel = "notice";
|
|
58
|
+
if (this.log.logLevel === "info" || Logger.level === MatterLogLevel.INFO)
|
|
59
|
+
callbackLogLevel = "info";
|
|
60
|
+
if (this.log.logLevel === "debug" || Logger.level === MatterLogLevel.DEBUG)
|
|
61
|
+
callbackLogLevel = "debug";
|
|
62
|
+
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
63
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
64
|
+
websocket.on('message', (data) => {
|
|
65
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} sent a message`);
|
|
66
|
+
this.wsMessageHandler(websocket, data);
|
|
67
|
+
});
|
|
68
|
+
websocket.on('ping', () => {
|
|
69
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} ping received`);
|
|
70
|
+
websocket.pong();
|
|
71
|
+
});
|
|
72
|
+
websocket.on('pong', () => {
|
|
73
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} pong received`);
|
|
74
|
+
});
|
|
75
|
+
websocket.on('close', (code, reason) => {
|
|
76
|
+
this.log.info(`WebSocket client ${CYAN}${clientIp}${nf} disconnected code ${code} reason ${reason.toString()}`);
|
|
77
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
78
|
+
AnsiLogger.setGlobalCallback(undefined);
|
|
79
|
+
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
82
|
+
this.log.debug('All WebSocket clients disconnected. Auth clients list cleared');
|
|
83
|
+
this.backend.authClients.clear();
|
|
84
|
+
}
|
|
85
|
+
}, 1000).unref();
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.log.debug(`WebSocketServer still has ${this.webSocketServer?.clients.size} connected clients`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
websocket.on('error', (error) => {
|
|
92
|
+
inspectError(this.log, `WebSocket client error`, error);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
this.webSocketServer.on('close', () => {
|
|
96
|
+
this.log.debug(`WebSocketServer closed`);
|
|
97
|
+
});
|
|
98
|
+
this.webSocketServer.on('error', (ws, error) => {
|
|
99
|
+
inspectError(this.log, `WebSocketServer error`, error);
|
|
100
|
+
});
|
|
101
|
+
return this.webSocketServer;
|
|
102
|
+
}
|
|
103
|
+
async stop() {
|
|
104
|
+
if (this.webSocketServer) {
|
|
105
|
+
this.log.debug('Closing WebSocket server...');
|
|
106
|
+
this.webSocketServer.clients.forEach((client) => {
|
|
107
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
108
|
+
client.close();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
await withTimeout(new Promise((resolve) => {
|
|
112
|
+
this.webSocketServer?.close((error) => {
|
|
113
|
+
if (error) {
|
|
114
|
+
inspectError(this.log, `Error closing WebSocket server`, error);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.log.debug('WebSocket server closed successfully');
|
|
118
|
+
this.backend.emit('websocket_server_stopped');
|
|
119
|
+
}
|
|
120
|
+
resolve();
|
|
121
|
+
});
|
|
122
|
+
}), 10000, false);
|
|
123
|
+
this.webSocketServer.removeAllListeners();
|
|
124
|
+
this.webSocketServer = undefined;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.log.debug('WebSocket server is not running');
|
|
128
|
+
}
|
|
129
|
+
return this.webSocketServer;
|
|
130
|
+
}
|
|
131
|
+
hasActiveClients() {
|
|
132
|
+
return this.webSocketServer !== undefined && this.webSocketServer.clients.size > 0;
|
|
133
|
+
}
|
|
134
|
+
async wsMessageHandler(client, rawData) {
|
|
135
|
+
let data;
|
|
136
|
+
const sendResponse = (data) => {
|
|
137
|
+
if (client.readyState === client.OPEN) {
|
|
138
|
+
if ('response' in data) {
|
|
139
|
+
const { response, ...rest } = data;
|
|
140
|
+
this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
|
|
141
|
+
}
|
|
142
|
+
else if ('error' in data) {
|
|
143
|
+
this.log.debug(`Sending api error message: ${debugStringify(data)}`);
|
|
144
|
+
}
|
|
145
|
+
client.send(JSON.stringify(data));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.log.error('Cannot send api response, client not connected');
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
data = JSON.parse(rawData.toString());
|
|
153
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
|
|
154
|
+
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
155
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
inspectError(this.log, `Error parsing message from websocket client`, error);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
wssBroadcastMessage(msg) {
|
|
166
|
+
if (!this.hasActiveClients())
|
|
167
|
+
return;
|
|
168
|
+
try {
|
|
169
|
+
const stringifiedMsg = JSON.stringify(msg);
|
|
170
|
+
if (this.verbose)
|
|
171
|
+
this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
|
|
172
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
173
|
+
if (client.readyState === client.OPEN) {
|
|
174
|
+
client.send(stringifiedMsg);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
inspectError(this.log, `Error broadcasting message to websocket clients`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
wssSendLogMessage(level, time, name, message) {
|
|
183
|
+
if (!this.hasActiveClients())
|
|
184
|
+
return;
|
|
185
|
+
if (!level || !time || !name || !message)
|
|
186
|
+
return;
|
|
187
|
+
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
188
|
+
message = message.replace(/^\*+/, '');
|
|
189
|
+
message = message.replace(/[\t\n]/g, '');
|
|
190
|
+
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
191
|
+
message = message.replace(/\\"/g, '"');
|
|
192
|
+
const maxContinuousLength = 100;
|
|
193
|
+
const keepStartLength = 20;
|
|
194
|
+
const keepEndLength = 20;
|
|
195
|
+
if (level !== 'spawn') {
|
|
196
|
+
message = message
|
|
197
|
+
.split(' ')
|
|
198
|
+
.map((word) => {
|
|
199
|
+
if (word.length > maxContinuousLength) {
|
|
200
|
+
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
201
|
+
}
|
|
202
|
+
return word;
|
|
203
|
+
})
|
|
204
|
+
.join(' ');
|
|
205
|
+
}
|
|
206
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
207
|
+
}
|
|
208
|
+
wssSendRefreshRequired(changed, params) {
|
|
209
|
+
if (!this.hasActiveClients())
|
|
210
|
+
return;
|
|
211
|
+
if (this.verbose)
|
|
212
|
+
this.log.debug('Sending a refresh required message to all connected clients');
|
|
213
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, lock: params?.lock, ...params } });
|
|
214
|
+
}
|
|
215
|
+
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
216
|
+
if (!this.hasActiveClients())
|
|
217
|
+
return;
|
|
218
|
+
if (this.verbose)
|
|
219
|
+
this.log.debug('Sending a restart required message to all connected clients');
|
|
220
|
+
this.backend.restartRequired = true;
|
|
221
|
+
this.backend.fixedRestartRequired = fixed;
|
|
222
|
+
if (snackbar === true)
|
|
223
|
+
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
224
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
225
|
+
}
|
|
226
|
+
wssSendRestartNotRequired(snackbar = true) {
|
|
227
|
+
if (!this.hasActiveClients())
|
|
228
|
+
return;
|
|
229
|
+
if (this.verbose)
|
|
230
|
+
this.log.debug('Sending a restart not required message to all connected clients');
|
|
231
|
+
this.backend.restartRequired = false;
|
|
232
|
+
if (snackbar === true)
|
|
233
|
+
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
234
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
235
|
+
}
|
|
236
|
+
wssSendUpdateRequired(devVersion = false) {
|
|
237
|
+
if (!this.hasActiveClients())
|
|
238
|
+
return;
|
|
239
|
+
if (this.verbose)
|
|
240
|
+
this.log.debug('Sending an update required message to all connected clients');
|
|
241
|
+
this.backend.updateRequired = true;
|
|
242
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
243
|
+
}
|
|
244
|
+
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
245
|
+
if (!this.hasActiveClients())
|
|
246
|
+
return;
|
|
247
|
+
if (this.verbose)
|
|
248
|
+
this.log.debug('Sending a cpu update message to all connected clients');
|
|
249
|
+
this.wssBroadcastMessage({
|
|
250
|
+
id: 0,
|
|
251
|
+
src: 'Matterbridge',
|
|
252
|
+
dst: 'Frontend',
|
|
253
|
+
method: 'cpu_update',
|
|
254
|
+
success: true,
|
|
255
|
+
response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
259
|
+
if (!this.hasActiveClients())
|
|
260
|
+
return;
|
|
261
|
+
if (this.verbose)
|
|
262
|
+
this.log.debug('Sending a memory update message to all connected clients');
|
|
263
|
+
this.wssBroadcastMessage({
|
|
264
|
+
id: 0,
|
|
265
|
+
src: 'Matterbridge',
|
|
266
|
+
dst: 'Frontend',
|
|
267
|
+
method: 'memory_update',
|
|
268
|
+
success: true,
|
|
269
|
+
response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
273
|
+
if (!this.hasActiveClients())
|
|
274
|
+
return;
|
|
275
|
+
if (this.verbose)
|
|
276
|
+
this.log.debug('Sending a uptime update message to all connected clients');
|
|
277
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
278
|
+
}
|
|
279
|
+
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
280
|
+
if (!this.hasActiveClients())
|
|
281
|
+
return;
|
|
282
|
+
if (this.verbose)
|
|
283
|
+
this.log.debug('Sending a snackbar message to all connected clients');
|
|
284
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
285
|
+
}
|
|
286
|
+
wssSendCloseSnackbarMessage(message) {
|
|
287
|
+
if (!this.hasActiveClients())
|
|
288
|
+
return;
|
|
289
|
+
if (this.verbose)
|
|
290
|
+
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
291
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
292
|
+
}
|
|
293
|
+
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
294
|
+
if (!this.hasActiveClients())
|
|
295
|
+
return;
|
|
296
|
+
if (this.verbose)
|
|
297
|
+
this.log.debug('Sending an attribute update message to all connected clients');
|
|
298
|
+
this.wssBroadcastMessage({
|
|
299
|
+
id: 0,
|
|
300
|
+
src: 'Matterbridge',
|
|
301
|
+
dst: 'Frontend',
|
|
302
|
+
method: 'state_update',
|
|
303
|
+
success: true,
|
|
304
|
+
response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,8 @@ export let instance;
|
|
|
12
12
|
export const tracker = new Tracker('Cli', false, false);
|
|
13
13
|
export const inspector = new Inspector('Cli', false, false);
|
|
14
14
|
const manager = new ThreadsManager();
|
|
15
|
-
|
|
15
|
+
const colorEnabled = Boolean(process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb' && process.env.FORCE_COLOR !== '0' && !hasParameter('no-ansi'));
|
|
16
|
+
if (!colorEnabled)
|
|
16
17
|
process.env.NO_COLOR = '1';
|
|
17
18
|
const log = new AnsiLogger({ logName: 'Cli', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
|
|
18
19
|
function startCpuMemoryCheck() {
|
package/dist/frontend.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
2
|
import { EndpointNumber } from '@matter/types/datatype';
|
|
3
|
-
import type { ApiMatter, RefreshRequiredChanged, WsMessageBroadcast } from '@matterbridge/types';
|
|
3
|
+
import type { ApiClusters, ApiDevice, ApiMatter, ApiPlugin, ApiSettings, RefreshRequiredChanged, WsMessageBroadcast } from '@matterbridge/types';
|
|
4
4
|
import { LogLevel } from 'node-ansi-logger';
|
|
5
5
|
import type { Matterbridge } from './matterbridge.js';
|
|
6
6
|
interface FrontendEvents {
|
|
@@ -15,8 +15,8 @@ export declare class Frontend extends EventEmitter<FrontendEvents> {
|
|
|
15
15
|
private log;
|
|
16
16
|
private port;
|
|
17
17
|
private listening;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
storedPassword: string | undefined;
|
|
19
|
+
authClients: Set<string>;
|
|
20
20
|
private expressApp;
|
|
21
21
|
private httpServer;
|
|
22
22
|
private httpsServer;
|
|
@@ -25,22 +25,29 @@ export declare class Frontend extends EventEmitter<FrontendEvents> {
|
|
|
25
25
|
private serverFetchTimeout;
|
|
26
26
|
private readonly debug;
|
|
27
27
|
private readonly verbose;
|
|
28
|
+
readonly readOnly: boolean;
|
|
29
|
+
readonly shellyBoard: boolean;
|
|
30
|
+
shellySysUpdate: boolean;
|
|
31
|
+
shellyMainUpdate: boolean;
|
|
32
|
+
restartRequired: boolean;
|
|
33
|
+
fixedRestartRequired: boolean;
|
|
34
|
+
updateRequired: boolean;
|
|
28
35
|
constructor(matterbridge: Matterbridge);
|
|
29
36
|
destroy(): void;
|
|
30
|
-
private
|
|
37
|
+
private broadcastMsgHandler;
|
|
31
38
|
set logLevel(logLevel: LogLevel);
|
|
32
39
|
private validateReq;
|
|
33
40
|
start(port?: number): Promise<void>;
|
|
34
41
|
stop(): Promise<void>;
|
|
35
|
-
|
|
42
|
+
getApiSettings(): ApiSettings;
|
|
36
43
|
private getReachability;
|
|
37
44
|
private getPowerSource;
|
|
38
45
|
private getBatteryLevel;
|
|
39
46
|
private getClusterTextFromDevice;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
getApiPlugins(): ApiPlugin[];
|
|
48
|
+
getApiDevices(pluginName?: string): ApiDevice[];
|
|
49
|
+
getClusters(pluginName: string, endpointNumber: number, serialNumber?: string, uniqueId?: string): ApiClusters | undefined;
|
|
50
|
+
generateDiagnostic(): Promise<void>;
|
|
44
51
|
private wsMessageHandler;
|
|
45
52
|
wssSendLogMessage(level: string, time: string, name: string, message: string): void;
|
|
46
53
|
wssSendRefreshRequired(changed: RefreshRequiredChanged, params?: {
|
package/dist/frontend.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
2
|
-
console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
|
|
3
1
|
import EventEmitter from 'node:events';
|
|
4
2
|
import os from 'node:os';
|
|
5
3
|
import path from 'node:path';
|
|
@@ -10,7 +8,7 @@ import { PowerSource } from '@matter/types/clusters/power-source';
|
|
|
10
8
|
import { CommissioningOptions } from '@matter/types/commissioning';
|
|
11
9
|
import { FabricIndex } from '@matter/types/datatype';
|
|
12
10
|
import { BroadcastServer } from '@matterbridge/thread/server';
|
|
13
|
-
import { MATTER_LOGGER_FILE,
|
|
11
|
+
import { MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
|
|
14
12
|
import { getParameter, hasParameter } from '@matterbridge/utils/cli';
|
|
15
13
|
import { inspectError } from '@matterbridge/utils/error';
|
|
16
14
|
import { formatBytes, formatPercent, formatUptime } from '@matterbridge/utils/format';
|
|
@@ -20,6 +18,8 @@ import { AnsiLogger, CYAN, db, debugStringify, er, nf, nt, rs, stringify, UNDERL
|
|
|
20
18
|
import { cliEmitter, lastOsCpuUsage, lastProcessCpuUsage } from './cliEmitter.js';
|
|
21
19
|
import { generateHistoryPage } from './cliHistory.js';
|
|
22
20
|
import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
|
|
21
|
+
if (hasParameter('loader'))
|
|
22
|
+
console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
|
|
23
23
|
export class Frontend extends EventEmitter {
|
|
24
24
|
matterbridge;
|
|
25
25
|
log;
|
|
@@ -35,6 +35,13 @@ export class Frontend extends EventEmitter {
|
|
|
35
35
|
serverFetchTimeout = 2000;
|
|
36
36
|
debug = hasParameter('debug') || hasParameter('verbose');
|
|
37
37
|
verbose = hasParameter('verbose');
|
|
38
|
+
readOnly = hasParameter('readonly') || hasParameter('shelly');
|
|
39
|
+
shellyBoard = hasParameter('shelly');
|
|
40
|
+
shellySysUpdate = false;
|
|
41
|
+
shellyMainUpdate = false;
|
|
42
|
+
restartRequired = false;
|
|
43
|
+
fixedRestartRequired = false;
|
|
44
|
+
updateRequired = false;
|
|
38
45
|
constructor(matterbridge) {
|
|
39
46
|
super();
|
|
40
47
|
this.matterbridge = matterbridge;
|
|
@@ -45,13 +52,13 @@ export class Frontend extends EventEmitter {
|
|
|
45
52
|
logLevel: hasParameter('debug') ? "debug" : "info",
|
|
46
53
|
});
|
|
47
54
|
this.server = new BroadcastServer('frontend', this.log);
|
|
48
|
-
this.server.on('broadcast_message', this.
|
|
55
|
+
this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
|
|
49
56
|
}
|
|
50
57
|
destroy() {
|
|
51
|
-
this.server.off('broadcast_message', this.
|
|
58
|
+
this.server.off('broadcast_message', this.broadcastMsgHandler.bind(this));
|
|
52
59
|
this.server.close();
|
|
53
60
|
}
|
|
54
|
-
async
|
|
61
|
+
async broadcastMsgHandler(msg) {
|
|
55
62
|
if (this.server.isWorkerRequest(msg)) {
|
|
56
63
|
if (this.verbose)
|
|
57
64
|
this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
@@ -116,6 +123,8 @@ export class Frontend extends EventEmitter {
|
|
|
116
123
|
if (msg.result && msg.result.packageCommand === 'install') {
|
|
117
124
|
this.wssSendCloseSnackbarMessage(`Installing package ${msg.result.packageName}...`);
|
|
118
125
|
if (msg.result.success) {
|
|
126
|
+
this.restartRequired = true;
|
|
127
|
+
this.fixedRestartRequired = true;
|
|
119
128
|
this.wssSendRestartRequired(true, true);
|
|
120
129
|
this.wssSendSnackbarMessage(`Installed package ${msg.result.packageName}`, 5, 'success');
|
|
121
130
|
}
|
|
@@ -126,6 +135,8 @@ export class Frontend extends EventEmitter {
|
|
|
126
135
|
if (msg.result && msg.result.packageCommand === 'uninstall') {
|
|
127
136
|
this.wssSendCloseSnackbarMessage(`Uninstalling package ${msg.result.packageName}...`);
|
|
128
137
|
if (msg.result.success) {
|
|
138
|
+
this.restartRequired = true;
|
|
139
|
+
this.fixedRestartRequired = true;
|
|
129
140
|
this.wssSendRestartRequired(true, true);
|
|
130
141
|
this.wssSendSnackbarMessage(`Uninstalled package ${msg.result.packageName}`, 5, 'success');
|
|
131
142
|
}
|
|
@@ -490,19 +501,19 @@ export class Frontend extends EventEmitter {
|
|
|
490
501
|
this.log.debug('The frontend sent /api/settings');
|
|
491
502
|
if (!this.validateReq(req, res))
|
|
492
503
|
return;
|
|
493
|
-
res.json(
|
|
504
|
+
res.json(this.getApiSettings());
|
|
494
505
|
});
|
|
495
506
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
496
507
|
this.log.debug('The frontend sent /api/plugins');
|
|
497
508
|
if (!this.validateReq(req, res))
|
|
498
509
|
return;
|
|
499
|
-
res.json(this.
|
|
510
|
+
res.json(this.getApiPlugins());
|
|
500
511
|
});
|
|
501
512
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
502
513
|
this.log.debug('The frontend sent /api/devices');
|
|
503
514
|
if (!this.validateReq(req, res))
|
|
504
515
|
return;
|
|
505
|
-
res.json(this.
|
|
516
|
+
res.json(this.getApiDevices());
|
|
506
517
|
});
|
|
507
518
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
508
519
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
@@ -699,14 +710,14 @@ export class Frontend extends EventEmitter {
|
|
|
699
710
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
700
711
|
if (!this.validateReq(req, res))
|
|
701
712
|
return;
|
|
702
|
-
res.download(path.join(os.tmpdir(), `matterbridge.${
|
|
713
|
+
res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_DIR}.zip`), `matterbridge.${MATTER_STORAGE_DIR}.zip`, (error) => {
|
|
703
714
|
this.wssSendCloseSnackbarMessage('Creating matter storage backup...');
|
|
704
715
|
if (error) {
|
|
705
|
-
this.log.error(`Error downloading the matter storage matterbridge.${
|
|
716
|
+
this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_DIR}.zip: ${error instanceof Error ? error.message : error}`);
|
|
706
717
|
res.status(500).send('Error downloading the matter storage zip file');
|
|
707
718
|
}
|
|
708
719
|
else {
|
|
709
|
-
this.log.debug(`Matter storage matterbridge.${
|
|
720
|
+
this.log.debug(`Matter storage matterbridge.${MATTER_STORAGE_DIR}.zip downloaded successfully`);
|
|
710
721
|
}
|
|
711
722
|
});
|
|
712
723
|
});
|
|
@@ -842,7 +853,7 @@ export class Frontend extends EventEmitter {
|
|
|
842
853
|
}
|
|
843
854
|
this.log.debug('Frontend stopped successfully');
|
|
844
855
|
}
|
|
845
|
-
|
|
856
|
+
getApiSettings() {
|
|
846
857
|
this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
847
858
|
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
848
859
|
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
@@ -871,23 +882,23 @@ export class Frontend extends EventEmitter {
|
|
|
871
882
|
restartMode: this.matterbridge.restartMode,
|
|
872
883
|
virtualMode: this.matterbridge.virtualMode,
|
|
873
884
|
profile: this.matterbridge.profile,
|
|
874
|
-
|
|
875
|
-
shellyBoard: this.matterbridge.shellyBoard,
|
|
876
|
-
shellySysUpdate: this.matterbridge.shellySysUpdate,
|
|
877
|
-
shellyMainUpdate: this.matterbridge.shellyMainUpdate,
|
|
878
|
-
loggerLevel: await this.matterbridge.getLogLevel(),
|
|
885
|
+
loggerLevel: this.matterbridge.logLevel,
|
|
879
886
|
fileLogger: this.matterbridge.fileLogger,
|
|
880
|
-
matterLoggerLevel:
|
|
887
|
+
matterLoggerLevel: this.matterbridge.matterLogLevel,
|
|
881
888
|
matterFileLogger: this.matterbridge.matterFileLogger,
|
|
882
889
|
matterMdnsInterface: this.matterbridge.mdnsInterface,
|
|
883
890
|
matterIpv4Address: this.matterbridge.ipv4Address,
|
|
884
891
|
matterIpv6Address: this.matterbridge.ipv6Address,
|
|
885
|
-
matterPort:
|
|
886
|
-
matterDiscriminator:
|
|
887
|
-
matterPasscode:
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
892
|
+
matterPort: this.matterbridge.port || 5540,
|
|
893
|
+
matterDiscriminator: this.matterbridge.discriminator,
|
|
894
|
+
matterPasscode: this.matterbridge.passcode,
|
|
895
|
+
readOnly: this.readOnly,
|
|
896
|
+
shellyBoard: this.shellyBoard,
|
|
897
|
+
shellySysUpdate: this.shellySysUpdate,
|
|
898
|
+
shellyMainUpdate: this.shellyMainUpdate,
|
|
899
|
+
restartRequired: this.restartRequired,
|
|
900
|
+
fixedRestartRequired: this.fixedRestartRequired,
|
|
901
|
+
updateRequired: this.updateRequired,
|
|
891
902
|
};
|
|
892
903
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
|
|
893
904
|
}
|
|
@@ -1065,7 +1076,7 @@ export class Frontend extends EventEmitter {
|
|
|
1065
1076
|
});
|
|
1066
1077
|
return attributes.trimStart().trimEnd();
|
|
1067
1078
|
}
|
|
1068
|
-
|
|
1079
|
+
getApiPlugins() {
|
|
1069
1080
|
if (this.matterbridge.hasCleanupStarted)
|
|
1070
1081
|
return [];
|
|
1071
1082
|
const plugins = [];
|
|
@@ -1100,7 +1111,7 @@ export class Frontend extends EventEmitter {
|
|
|
1100
1111
|
}
|
|
1101
1112
|
return plugins;
|
|
1102
1113
|
}
|
|
1103
|
-
|
|
1114
|
+
getApiDevices(pluginName) {
|
|
1104
1115
|
if (this.matterbridge.hasCleanupStarted)
|
|
1105
1116
|
return [];
|
|
1106
1117
|
const devices = [];
|
|
@@ -1488,7 +1499,7 @@ export class Frontend extends EventEmitter {
|
|
|
1488
1499
|
else if (data.method === '/api/create-matter-storage-backup') {
|
|
1489
1500
|
this.wssSendSnackbarMessage('Creating matter storage backup...', 0);
|
|
1490
1501
|
this.log.notice(`Creating matter storage backup...`);
|
|
1491
|
-
this.zip('zip', path.join(os.tmpdir(), `matterbridge.${
|
|
1502
|
+
this.zip('zip', path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_DIR}.zip`), [path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_DIR)], '');
|
|
1492
1503
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1493
1504
|
}
|
|
1494
1505
|
else if (data.method === '/api/create-plugin-backup') {
|
|
@@ -1580,14 +1591,13 @@ export class Frontend extends EventEmitter {
|
|
|
1580
1591
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
|
|
1581
1592
|
}
|
|
1582
1593
|
else if (data.method === '/api/settings') {
|
|
1583
|
-
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response:
|
|
1594
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: this.getApiSettings() });
|
|
1584
1595
|
}
|
|
1585
1596
|
else if (data.method === '/api/plugins') {
|
|
1586
|
-
|
|
1587
|
-
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
|
|
1597
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: this.getApiPlugins() });
|
|
1588
1598
|
}
|
|
1589
1599
|
else if (data.method === '/api/devices') {
|
|
1590
|
-
const devices = this.
|
|
1600
|
+
const devices = this.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
|
|
1591
1601
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
|
|
1592
1602
|
}
|
|
1593
1603
|
else if (data.method === '/api/clusters') {
|
|
@@ -2033,8 +2043,8 @@ export class Frontend extends EventEmitter {
|
|
|
2033
2043
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2034
2044
|
return;
|
|
2035
2045
|
this.log.debug('Sending a restart required message to all connected clients');
|
|
2036
|
-
this.
|
|
2037
|
-
this.
|
|
2046
|
+
this.restartRequired = true;
|
|
2047
|
+
this.fixedRestartRequired = fixed;
|
|
2038
2048
|
if (snackbar === true)
|
|
2039
2049
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
2040
2050
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
@@ -2043,7 +2053,7 @@ export class Frontend extends EventEmitter {
|
|
|
2043
2053
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2044
2054
|
return;
|
|
2045
2055
|
this.log.debug('Sending a restart not required message to all connected clients');
|
|
2046
|
-
this.
|
|
2056
|
+
this.restartRequired = false;
|
|
2047
2057
|
if (snackbar === true)
|
|
2048
2058
|
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
2049
2059
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
@@ -2052,7 +2062,7 @@ export class Frontend extends EventEmitter {
|
|
|
2052
2062
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2053
2063
|
return;
|
|
2054
2064
|
this.log.debug('Sending an update required message to all connected clients');
|
|
2055
|
-
this.
|
|
2065
|
+
this.updateRequired = true;
|
|
2056
2066
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
2057
2067
|
}
|
|
2058
2068
|
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
@@ -83,7 +83,7 @@ export declare function createTestEnvironment(name: string, createOnly?: boolean
|
|
|
83
83
|
export declare function destroyTestEnvironment(createOnly?: boolean): Promise<void>;
|
|
84
84
|
export declare function flushAsync(ticks?: number, microTurns?: number, pause?: number): Promise<void>;
|
|
85
85
|
export declare function logKeepAlives(log?: AnsiLogger): number;
|
|
86
|
-
export declare function flushAllEndpointNumberPersistence(targetServer: ServerNode, rounds?: number): Promise<void>;
|
|
86
|
+
export declare function flushAllEndpointNumberPersistence(targetServer: ServerNode, rounds?: number, pause?: number): Promise<void>;
|
|
87
87
|
export declare function assertAllEndpointNumbersPersisted(targetServer: ServerNode): Promise<number>;
|
|
88
88
|
export declare function closeServerNodeStores(targetServer?: ServerNode): Promise<void>;
|
|
89
89
|
export declare function startServerNode(name: string, port: number, deviceType?: DeviceTypeId, createOnly?: boolean): Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>;
|