@matterbridge/core 3.10.13-dev-20261007-320f739 → 3.10.13-dev-20261009-7fc645a
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/backend.d.ts +42 -0
- package/dist/backend.js +370 -0
- package/dist/backendExpress.d.ts +21 -0
- package/dist/backendExpress.js +460 -0
- package/dist/backendWsServer.d.ts +39 -0
- package/dist/backendWsServer.js +409 -0
- package/dist/cli.js +7 -5
- package/dist/cliEmitter.d.ts +0 -4
- package/dist/cliEmitter.js +0 -8
- package/dist/frontend.js +8 -7
- package/dist/matterNode.js +37 -2
- package/dist/matterbridge.js +58 -1
- package/dist/runners/threadBackend.d.ts +3 -0
- package/dist/runners/threadBackend.js +24 -0
- package/dist/runners/threadRootNode.d.ts +3 -0
- package/dist/runners/threadRootNode.js +30 -0
- package/dist/runners/threadTracker.d.ts +3 -0
- package/dist/runners/threadTracker.js +26 -0
- package/package.json +5 -5
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
|
|
2
|
+
import { BroadcastServer } from '@matterbridge/thread/server';
|
|
3
|
+
import { hasAnyParameter } from '@matterbridge/utils/cli';
|
|
4
|
+
import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
|
|
5
|
+
import { inspectError } from '@matterbridge/utils/error';
|
|
6
|
+
import { logModuleLoaded } from '@matterbridge/utils/loader';
|
|
7
|
+
import { isValidNumber, isValidString } from '@matterbridge/utils/validate';
|
|
8
|
+
import { fireAndForget, withTimeout } from '@matterbridge/utils/wait';
|
|
9
|
+
import { AnsiLogger, CYAN, debugStringify, nf } from 'node-ansi-logger';
|
|
10
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
11
|
+
logModuleLoaded('BackendWsServer');
|
|
12
|
+
export class BackendWsServer {
|
|
13
|
+
debug;
|
|
14
|
+
verbose;
|
|
15
|
+
diagnostic;
|
|
16
|
+
log;
|
|
17
|
+
backend;
|
|
18
|
+
matterbridge;
|
|
19
|
+
server;
|
|
20
|
+
webSocketServer;
|
|
21
|
+
constructor(matterbridge, backend) {
|
|
22
|
+
this.debug = hasAnyParameter('debug', 'verbose', 'debug-backend', 'verbose-backend');
|
|
23
|
+
this.verbose = hasAnyParameter('verbose', 'verbose-backend');
|
|
24
|
+
this.diagnostic = hasAnyParameter('diagnostic', 'diagnostic-backend');
|
|
25
|
+
this.backend = backend;
|
|
26
|
+
this.matterbridge = matterbridge;
|
|
27
|
+
this.log = new AnsiLogger({
|
|
28
|
+
logName: 'BackendWsServer',
|
|
29
|
+
logNameColor: '\x1b[38;5;97m',
|
|
30
|
+
logTimestampFormat: 4,
|
|
31
|
+
logLevel: this.debug ? "debug" : "info",
|
|
32
|
+
});
|
|
33
|
+
this.server = new BroadcastServer('frontend', this.log);
|
|
34
|
+
this.server.on('broadcast_message', (msg) => fireAndForget(this.broadcastMsgHandler(msg), this.log, 'Broadcast message handler'));
|
|
35
|
+
}
|
|
36
|
+
destroy() {
|
|
37
|
+
this.server.close();
|
|
38
|
+
}
|
|
39
|
+
async broadcastMsgHandler(msg) {
|
|
40
|
+
if (this.server.isWorkerRequest(msg)) {
|
|
41
|
+
switch (msg.type) {
|
|
42
|
+
case 'get_log_level':
|
|
43
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
44
|
+
break;
|
|
45
|
+
case 'set_log_level':
|
|
46
|
+
this.log.logLevel = msg.params.logLevel;
|
|
47
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async start() {
|
|
53
|
+
this.log.debug(`Creating WebSocketServer...`);
|
|
54
|
+
this.webSocketServer = new WebSocketServer({ noServer: true });
|
|
55
|
+
this.backend.emit('websocket_server_listening', this.backend.secure ? 'wss' : 'ws');
|
|
56
|
+
this.webSocketServer.on('connection', (websocket, request) => {
|
|
57
|
+
const clientIp = request.socket.remoteAddress;
|
|
58
|
+
this.log.info(`WebSocketServer client ${CYAN}${clientIp}${nf} connected to Matterbridge`);
|
|
59
|
+
let callbackLogLevel = "notice";
|
|
60
|
+
if (this.log.logLevel === "info" || Logger.level === MatterLogLevel.INFO)
|
|
61
|
+
callbackLogLevel = "info";
|
|
62
|
+
if (this.log.logLevel === "debug" || Logger.level === MatterLogLevel.DEBUG)
|
|
63
|
+
callbackLogLevel = "debug";
|
|
64
|
+
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
65
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
66
|
+
websocket.on('message', (data) => {
|
|
67
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} sent a message`);
|
|
68
|
+
fireAndForget(this.wsMessageHandler(websocket, data), this.log, `Error handling message from WebSocket client ${clientIp}`);
|
|
69
|
+
});
|
|
70
|
+
websocket.on('ping', () => {
|
|
71
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} ping received`);
|
|
72
|
+
websocket.pong();
|
|
73
|
+
});
|
|
74
|
+
websocket.on('pong', () => {
|
|
75
|
+
this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} pong received`);
|
|
76
|
+
});
|
|
77
|
+
websocket.on('close', (code, reason) => {
|
|
78
|
+
this.log.info(`WebSocket client ${CYAN}${clientIp}${nf} disconnected code ${code} reason ${reason.toString()}`);
|
|
79
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
80
|
+
AnsiLogger.setGlobalCallback(undefined);
|
|
81
|
+
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
84
|
+
this.log.debug('All WebSocket clients disconnected. Auth clients list cleared');
|
|
85
|
+
this.backend.authClients.clear();
|
|
86
|
+
}
|
|
87
|
+
}, 1000).unref();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
this.log.debug(`WebSocketServer still has ${this.webSocketServer?.clients.size} connected clients`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
websocket.on('error', (error) => {
|
|
94
|
+
inspectError(this.log, `WebSocket client error`, error);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
this.webSocketServer.on('close', () => {
|
|
98
|
+
this.log.debug(`WebSocketServer closed`);
|
|
99
|
+
});
|
|
100
|
+
this.webSocketServer.on('error', (ws, error) => {
|
|
101
|
+
inspectError(this.log, `WebSocketServer error`, error);
|
|
102
|
+
});
|
|
103
|
+
return this.webSocketServer;
|
|
104
|
+
}
|
|
105
|
+
async stop() {
|
|
106
|
+
if (this.webSocketServer) {
|
|
107
|
+
this.log.debug('Closing WebSocket server...');
|
|
108
|
+
this.webSocketServer.clients.forEach((client) => {
|
|
109
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
110
|
+
client.close();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
await withTimeout(new Promise((resolve) => {
|
|
114
|
+
this.webSocketServer?.close((error) => {
|
|
115
|
+
if (error) {
|
|
116
|
+
inspectError(this.log, `Error closing WebSocket server`, error);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
this.log.debug('WebSocket server closed successfully');
|
|
120
|
+
this.backend.emit('websocket_server_stopped');
|
|
121
|
+
}
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
}), 10000, false);
|
|
125
|
+
this.webSocketServer.removeAllListeners();
|
|
126
|
+
this.webSocketServer = undefined;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.log.debug('WebSocket server is not running');
|
|
130
|
+
}
|
|
131
|
+
return this.webSocketServer;
|
|
132
|
+
}
|
|
133
|
+
hasActiveClients() {
|
|
134
|
+
return this.webSocketServer !== undefined && this.webSocketServer.clients.size > 0;
|
|
135
|
+
}
|
|
136
|
+
async wsMessageHandler(client, rawData) {
|
|
137
|
+
let data;
|
|
138
|
+
const sendResponse = (data) => {
|
|
139
|
+
if (client.readyState === client.OPEN) {
|
|
140
|
+
if ('response' in data) {
|
|
141
|
+
const { response, ...rest } = data;
|
|
142
|
+
this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.log.debug(`Sending api error message: ${debugStringify(data)}`);
|
|
146
|
+
}
|
|
147
|
+
const bigintReplacer = (_, v) => (typeof v === 'bigint' ? `${v}n` : v);
|
|
148
|
+
client.send(JSON.stringify(data, bigintReplacer));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.log.error('Cannot send api response, client not connected');
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
const raw = Array.isArray(rawData) ? Buffer.concat(rawData) : rawData instanceof ArrayBuffer ? Buffer.from(rawData) : rawData;
|
|
156
|
+
data = JSON.parse(raw.toString());
|
|
157
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
|
|
158
|
+
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
159
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
|
|
163
|
+
if (data.method === 'ping') {
|
|
164
|
+
sendResponse({ id: data.id, method: 'pong', src: 'Matterbridge', dst: data.src, success: true, response: 'pong' });
|
|
165
|
+
}
|
|
166
|
+
else if (data.method === '/api/login') {
|
|
167
|
+
const storedPassword = this.backend.storedPassword;
|
|
168
|
+
if (storedPassword === undefined) {
|
|
169
|
+
this.log.error('Login stored password not loaded');
|
|
170
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: stored password not loaded' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (storedPassword === '' || storedPassword === data.params.password) {
|
|
174
|
+
this.log.debug('Login password valid');
|
|
175
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.log.debug('Error wrong password');
|
|
179
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (data.method === '/api/restart') {
|
|
183
|
+
this.wssSendSnackbarMessage('Restarting matterbridge...', 0);
|
|
184
|
+
await this.server.fetch({ type: 'matterbridge_restart', src: 'frontend', dst: 'matterbridge', params: undefined });
|
|
185
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
186
|
+
}
|
|
187
|
+
else if (data.method === '/api/shutdown') {
|
|
188
|
+
this.wssSendSnackbarMessage('Shutting down matterbridge...', 0);
|
|
189
|
+
await this.server.fetch({ type: 'matterbridge_shutdown', src: 'frontend', dst: 'matterbridge', params: undefined });
|
|
190
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
191
|
+
}
|
|
192
|
+
else if (data.method === '/api/settings') {
|
|
193
|
+
const start = performance.now();
|
|
194
|
+
const settings = await this.backend.getApiSettings();
|
|
195
|
+
if (this.diagnostic)
|
|
196
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiSettings() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
197
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: settings });
|
|
198
|
+
}
|
|
199
|
+
else if (data.method === '/api/plugins') {
|
|
200
|
+
const start = performance.now();
|
|
201
|
+
const plugins = await this.backend.getApiPlugins();
|
|
202
|
+
if (this.diagnostic)
|
|
203
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
204
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
|
|
205
|
+
}
|
|
206
|
+
else if (data.method === '/api/devices') {
|
|
207
|
+
const start = performance.now();
|
|
208
|
+
const devices = await this.backend.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
|
|
209
|
+
if (this.diagnostic)
|
|
210
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
211
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
|
|
212
|
+
}
|
|
213
|
+
else if (data.method === '/api/clusters') {
|
|
214
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
215
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!isValidNumber(data.params.endpoint, 1)) {
|
|
219
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const start = performance.now();
|
|
223
|
+
const clusters = await this.backend.getApiCluster(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
|
|
224
|
+
if (this.diagnostic)
|
|
225
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
226
|
+
if (clusters) {
|
|
227
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: clusters });
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Endpoint not found in /api/clusters' });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (data.method === '/api/matter') {
|
|
234
|
+
if (!isValidString(data.params.id)) {
|
|
235
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const matter = await this.backend.getApiMatter(data.params.id);
|
|
239
|
+
if (!matter) {
|
|
240
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown server node id ${data.params.id} in /api/matter` });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
|
|
244
|
+
this.wssSendRefreshRequired('matter', { matter });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
inspectError(this.log, `Error parsing message from websocket client`, error);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
wssBroadcastMessage(msg) {
|
|
253
|
+
if (!this.hasActiveClients())
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
const stringifiedMsg = JSON.stringify(msg);
|
|
257
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
258
|
+
if (client.readyState === client.OPEN) {
|
|
259
|
+
client.send(stringifiedMsg);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
inspectError(this.log, `Error broadcasting message to websocket clients`, error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
wssSendLogMessage(level, time, name, message) {
|
|
268
|
+
if (!this.hasActiveClients())
|
|
269
|
+
return;
|
|
270
|
+
if (!level || !time || !name || !message)
|
|
271
|
+
return;
|
|
272
|
+
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
273
|
+
message = message.replace(/^\*+/, '');
|
|
274
|
+
message = message.replace(/[\t\n]/g, '');
|
|
275
|
+
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
276
|
+
message = message.replace(/\\"/g, '"');
|
|
277
|
+
const maxContinuousLength = 100;
|
|
278
|
+
const keepStartLength = 20;
|
|
279
|
+
const keepEndLength = 20;
|
|
280
|
+
if (level !== 'spawn') {
|
|
281
|
+
message = message
|
|
282
|
+
.split(' ')
|
|
283
|
+
.map((word) => {
|
|
284
|
+
if (word.length > maxContinuousLength) {
|
|
285
|
+
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
286
|
+
}
|
|
287
|
+
return word;
|
|
288
|
+
})
|
|
289
|
+
.join(' ');
|
|
290
|
+
}
|
|
291
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
292
|
+
}
|
|
293
|
+
wssSendRefreshRequired(changed, params) {
|
|
294
|
+
if (!this.hasActiveClients())
|
|
295
|
+
return;
|
|
296
|
+
if (this.verbose)
|
|
297
|
+
this.log.debug('Sending a refresh required message to all connected clients');
|
|
298
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, lock: params?.lock, ...params } });
|
|
299
|
+
}
|
|
300
|
+
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
301
|
+
if (!this.hasActiveClients())
|
|
302
|
+
return;
|
|
303
|
+
if (this.verbose)
|
|
304
|
+
this.log.debug('Sending a restart required message to all connected clients');
|
|
305
|
+
if (snackbar)
|
|
306
|
+
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
307
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
308
|
+
}
|
|
309
|
+
wssSendRestartNotRequired(snackbar = true) {
|
|
310
|
+
if (!this.hasActiveClients())
|
|
311
|
+
return;
|
|
312
|
+
if (this.verbose)
|
|
313
|
+
this.log.debug('Sending a restart not required message to all connected clients');
|
|
314
|
+
if (snackbar)
|
|
315
|
+
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
316
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
317
|
+
}
|
|
318
|
+
wssSendUpdateRequired(version, devVersion = false) {
|
|
319
|
+
if (!this.hasActiveClients())
|
|
320
|
+
return;
|
|
321
|
+
if (this.verbose)
|
|
322
|
+
this.log.debug('Sending a matterbridge version update required message to all connected clients');
|
|
323
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { version, devVersion } });
|
|
324
|
+
}
|
|
325
|
+
wssSendPluginUpdateRequired(plugin, version, devVersion = false) {
|
|
326
|
+
if (!this.hasActiveClients())
|
|
327
|
+
return;
|
|
328
|
+
if (this.verbose)
|
|
329
|
+
this.log.debug('Sending a plugin version update required message to all connected clients');
|
|
330
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_update_required', success: true, response: { plugin, version, devVersion } });
|
|
331
|
+
}
|
|
332
|
+
wssSendPluginStatusUpdate(plugin, status) {
|
|
333
|
+
if (!this.hasActiveClients())
|
|
334
|
+
return;
|
|
335
|
+
if (this.verbose)
|
|
336
|
+
this.log.debug('Sending a plugin status update message to all connected clients');
|
|
337
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_status_update', success: true, response: { plugin, status } });
|
|
338
|
+
}
|
|
339
|
+
wssSendMatterbridgeStatusUpdate(status) {
|
|
340
|
+
if (!this.hasActiveClients())
|
|
341
|
+
return;
|
|
342
|
+
if (this.verbose)
|
|
343
|
+
this.log.debug('Sending a matterbridge status update message to all connected clients');
|
|
344
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'matterbridge_status_update', success: true, response: { status } });
|
|
345
|
+
}
|
|
346
|
+
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
347
|
+
if (!this.hasActiveClients())
|
|
348
|
+
return;
|
|
349
|
+
if (this.verbose)
|
|
350
|
+
this.log.debug('Sending a cpu update message to all connected clients');
|
|
351
|
+
this.wssBroadcastMessage({
|
|
352
|
+
id: 0,
|
|
353
|
+
src: 'Matterbridge',
|
|
354
|
+
dst: 'Frontend',
|
|
355
|
+
method: 'cpu_update',
|
|
356
|
+
success: true,
|
|
357
|
+
response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
361
|
+
if (!this.hasActiveClients())
|
|
362
|
+
return;
|
|
363
|
+
if (this.verbose)
|
|
364
|
+
this.log.debug('Sending a memory update message to all connected clients');
|
|
365
|
+
this.wssBroadcastMessage({
|
|
366
|
+
id: 0,
|
|
367
|
+
src: 'Matterbridge',
|
|
368
|
+
dst: 'Frontend',
|
|
369
|
+
method: 'memory_update',
|
|
370
|
+
success: true,
|
|
371
|
+
response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
375
|
+
if (!this.hasActiveClients())
|
|
376
|
+
return;
|
|
377
|
+
if (this.verbose)
|
|
378
|
+
this.log.debug('Sending a uptime update message to all connected clients');
|
|
379
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
380
|
+
}
|
|
381
|
+
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
382
|
+
if (!this.hasActiveClients())
|
|
383
|
+
return;
|
|
384
|
+
if (this.verbose)
|
|
385
|
+
this.log.debug('Sending a snackbar message to all connected clients');
|
|
386
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
387
|
+
}
|
|
388
|
+
wssSendCloseSnackbarMessage(message) {
|
|
389
|
+
if (!this.hasActiveClients())
|
|
390
|
+
return;
|
|
391
|
+
if (this.verbose)
|
|
392
|
+
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
393
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
394
|
+
}
|
|
395
|
+
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
396
|
+
if (!this.hasActiveClients())
|
|
397
|
+
return;
|
|
398
|
+
if (this.verbose)
|
|
399
|
+
this.log.debug('Sending an attribute update message to all connected clients');
|
|
400
|
+
this.wssBroadcastMessage({
|
|
401
|
+
id: 0,
|
|
402
|
+
src: 'Matterbridge',
|
|
403
|
+
dst: 'Frontend',
|
|
404
|
+
method: 'state_update',
|
|
405
|
+
success: true,
|
|
406
|
+
response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { ThreadsManager } from '@matterbridge/thread/manager';
|
|
2
4
|
import { hasAnyParameter, hasParameter } from '@matterbridge/utils/cli';
|
|
3
5
|
import { inspectError } from '@matterbridge/utils/error';
|
|
@@ -11,7 +13,7 @@ logModuleLoaded('Cli');
|
|
|
11
13
|
export let instance;
|
|
12
14
|
export const tracker = new Tracker('Cli', false, false);
|
|
13
15
|
export const inspector = new Inspector('Cli', false, false);
|
|
14
|
-
const manager = new ThreadsManager();
|
|
16
|
+
const manager = new ThreadsManager(path.dirname(fileURLToPath(import.meta.url)));
|
|
15
17
|
const colorEnabled = !process.env.NO_COLOR && process.env.TERM !== 'dumb' && process.env.FORCE_COLOR !== '0' && !hasParameter('no-ansi');
|
|
16
18
|
if (!colorEnabled)
|
|
17
19
|
process.env.NO_COLOR = '1';
|
|
@@ -100,10 +102,10 @@ async function main() {
|
|
|
100
102
|
const { Matterbridge } = await import('./matterbridge.js');
|
|
101
103
|
instance = await Matterbridge.loadInstance(true);
|
|
102
104
|
log.debug(`***Matterbridge.loadInstance(true) exited`);
|
|
103
|
-
if (hasParameter('experimental')) {
|
|
104
|
-
log.debug(`***Starting Backend thread...`);
|
|
105
|
+
if (hasParameter('experimental-backend')) {
|
|
106
|
+
log.debug(`***Starting experimental Backend thread...`);
|
|
105
107
|
manager.runThread('Backend');
|
|
106
|
-
log.debug(`***Started Backend thread`);
|
|
108
|
+
log.debug(`***Started experimental Backend thread`);
|
|
107
109
|
}
|
|
108
110
|
if (!instance || instance.shutdown) {
|
|
109
111
|
await shutdown();
|
|
@@ -173,7 +175,7 @@ function help() {
|
|
|
173
175
|
--novirtual: disable the creation of the virtual devices Restart, Update and Reboot Matterbridge
|
|
174
176
|
--root-power-source: add a Power Source cluster for the mains power feed on the Root endpoint
|
|
175
177
|
--ssl: enable SSL for the frontend and the WebSocketServer (the server will use the certificates and switch to https)
|
|
176
|
-
--mtls: enable mTLS for the frontend and the WebSocketServer (both server and client will use and require the certificates and switch to https)
|
|
178
|
+
--mtls: enable mTLS for the frontend and the WebSocketServer (both server and client will use and require the certificates and switch to https); it also enables https, so --ssl --tls is not needed
|
|
177
179
|
--vendorId: override the default vendorId 0xfff1
|
|
178
180
|
--vendorName: override the default vendorName "Matterbridge"
|
|
179
181
|
--productId: override the default productId 0x8000
|
package/dist/cliEmitter.d.ts
CHANGED
|
@@ -7,8 +7,4 @@ interface CliEvents {
|
|
|
7
7
|
ready: [];
|
|
8
8
|
}
|
|
9
9
|
export declare const cliEmitter: EventEmitter<CliEvents>;
|
|
10
|
-
export declare let lastOsCpuUsage: number;
|
|
11
|
-
export declare let lastProcessCpuUsage: number;
|
|
12
|
-
export declare function setLastOsCpuUsage(val: number): void;
|
|
13
|
-
export declare function setLastProcessCpuUsage(val: number): void;
|
|
14
10
|
export {};
|
package/dist/cliEmitter.js
CHANGED
|
@@ -2,11 +2,3 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import { logModuleLoaded } from '@matterbridge/utils/loader';
|
|
3
3
|
logModuleLoaded('CliEmitter');
|
|
4
4
|
export const cliEmitter = new EventEmitter();
|
|
5
|
-
export let lastOsCpuUsage = 0;
|
|
6
|
-
export let lastProcessCpuUsage = 0;
|
|
7
|
-
export function setLastOsCpuUsage(val) {
|
|
8
|
-
lastOsCpuUsage = val;
|
|
9
|
-
}
|
|
10
|
-
export function setLastProcessCpuUsage(val) {
|
|
11
|
-
lastProcessCpuUsage = val;
|
|
12
|
-
}
|
package/dist/frontend.js
CHANGED
|
@@ -8,7 +8,7 @@ import { FabricIndex } from '@matter/types/datatype';
|
|
|
8
8
|
import { BroadcastServer } from '@matterbridge/thread/server';
|
|
9
9
|
import { MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
|
|
10
10
|
import { isBun, memoryFootprint } from '@matterbridge/utils/bun';
|
|
11
|
-
import { getParameter, hasParameter } from '@matterbridge/utils/cli';
|
|
11
|
+
import { getParameter, hasAnyParameter, hasParameter } from '@matterbridge/utils/cli';
|
|
12
12
|
import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
|
|
13
13
|
import { getErrorMessage, inspectError, logError } from '@matterbridge/utils/error';
|
|
14
14
|
import { formatBytes, formatPercent, formatUptime } from '@matterbridge/utils/format';
|
|
@@ -16,7 +16,7 @@ import { logModuleLoaded } from '@matterbridge/utils/loader';
|
|
|
16
16
|
import { isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString } from '@matterbridge/utils/validate';
|
|
17
17
|
import { fireAndForget, wait, withTimeout } from '@matterbridge/utils/wait';
|
|
18
18
|
import { AnsiLogger, bgHex, CYAN, db, debugStringify, er, GREEN, nf, nt, rs, stringify, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
|
|
19
|
-
import { cliEmitter
|
|
19
|
+
import { cliEmitter } from './cliEmitter.js';
|
|
20
20
|
import { generateHistoryPage } from './cliHistory.js';
|
|
21
21
|
import { capitalizeFirstLetter } from './matterbridgeEndpointHelpers.js';
|
|
22
22
|
import { deleteAdvertisingNode, getBatteryLevel, getClusterTextFromDevice, getPowerSource, getReachability, getServerNodeData, setAdvertisingNode } from './matterNodeHelpers.js';
|
|
@@ -209,7 +209,8 @@ export class Frontend extends EventEmitter {
|
|
|
209
209
|
async start(port = 8283) {
|
|
210
210
|
this.port = port;
|
|
211
211
|
this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
|
|
212
|
-
|
|
212
|
+
const secure = hasAnyParameter('ssl', 'tls', 'mtls');
|
|
213
|
+
this.log.debug(`Initializing the frontend ${secure ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
213
214
|
const multer = await import('multer');
|
|
214
215
|
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
215
216
|
const upload = multer.default({ dest: uploadDir });
|
|
@@ -219,7 +220,7 @@ export class Frontend extends EventEmitter {
|
|
|
219
220
|
this.log.debug(`Creating WebSocketServer...`);
|
|
220
221
|
const ws = await import('ws');
|
|
221
222
|
this.webSocketServer = new ws.WebSocketServer({ noServer: true });
|
|
222
|
-
this.emit('websocket_server_listening',
|
|
223
|
+
this.emit('websocket_server_listening', secure ? 'wss' : 'ws');
|
|
223
224
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
224
225
|
const clientIp = request.socket.remoteAddress;
|
|
225
226
|
let callbackLogLevel = "notice";
|
|
@@ -264,7 +265,7 @@ export class Frontend extends EventEmitter {
|
|
|
264
265
|
this.webSocketServer.on('error', (ws, error) => {
|
|
265
266
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
266
267
|
});
|
|
267
|
-
if (!
|
|
268
|
+
if (!secure) {
|
|
268
269
|
const http = await import('node:http');
|
|
269
270
|
try {
|
|
270
271
|
this.log.debug(`Creating HTTP server...`);
|
|
@@ -929,8 +930,8 @@ export class Frontend extends EventEmitter {
|
|
|
929
930
|
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
930
931
|
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
931
932
|
this.matterbridge.systemInformation.processUptime = formatUptime(Math.floor(process.uptime()));
|
|
932
|
-
this.matterbridge.systemInformation.cpuUsage = formatPercent(
|
|
933
|
-
this.matterbridge.systemInformation.processCpuUsage = formatPercent(
|
|
933
|
+
this.matterbridge.systemInformation.cpuUsage = formatPercent(0);
|
|
934
|
+
this.matterbridge.systemInformation.processCpuUsage = formatPercent(0);
|
|
934
935
|
this.matterbridge.systemInformation.rss = formatBytes(memoryFootprint());
|
|
935
936
|
this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
936
937
|
this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
package/dist/matterNode.js
CHANGED
|
@@ -69,14 +69,15 @@ export class MatterNode extends EventEmitter {
|
|
|
69
69
|
this.log.logNameColor = '\x1b[38;5;65m';
|
|
70
70
|
if (this.debug)
|
|
71
71
|
this.log.debug(`MatterNode ${this.pluginName ? 'for plugin ' + this.pluginName : 'bridge'} loading...`);
|
|
72
|
+
this.storeId = this.device?.deviceName ? this.device.deviceName.replace(/[ .]/g, '') : (this.pluginName ?? 'Matterbridge');
|
|
72
73
|
this.port = matterbridge.port;
|
|
73
74
|
this.passcode = matterbridge.passcode;
|
|
74
75
|
this.discriminator = matterbridge.discriminator;
|
|
76
|
+
this.mdnsInterface = matterbridge.mdnsInterface;
|
|
75
77
|
this.server = new BroadcastServer('matter', this.log);
|
|
76
78
|
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
77
79
|
if (this.verbose)
|
|
78
80
|
this.log.debug(`BroadcastServer is ready`);
|
|
79
|
-
fs.mkdirSync(matterbridge.matterbridgeDirectory, { recursive: true });
|
|
80
81
|
this.pluginManager = new PluginManager(this.matterbridge);
|
|
81
82
|
this.pluginManager.logLevel = this.debug ? "debug" : "info";
|
|
82
83
|
this.pluginManager.server.close();
|
|
@@ -87,6 +88,8 @@ export class MatterNode extends EventEmitter {
|
|
|
87
88
|
this.environment.vars.set('path.root', path.join(matterbridge.matterbridgeDirectory, MATTER_STORAGE_DIR));
|
|
88
89
|
this.environment.vars.set('runtime.signals', false);
|
|
89
90
|
this.environment.vars.set('runtime.exitcode', false);
|
|
91
|
+
if (this.mdnsInterface)
|
|
92
|
+
this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
|
|
90
93
|
if (this.verbose)
|
|
91
94
|
this.log.debug(`Matter Environment is ready`);
|
|
92
95
|
this.matterMdnsService = new MdnsService(this.environment);
|
|
@@ -117,6 +120,34 @@ export class MatterNode extends EventEmitter {
|
|
|
117
120
|
this.log.logLevel = msg.params.logLevel;
|
|
118
121
|
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
119
122
|
break;
|
|
123
|
+
case 'matter_start':
|
|
124
|
+
if (this.device || msg.params.storeId !== this.storeId)
|
|
125
|
+
break;
|
|
126
|
+
try {
|
|
127
|
+
await this.create();
|
|
128
|
+
await this.start();
|
|
129
|
+
this.server.respond({ ...msg, result: { storeId: this.storeId, success: true } });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
inspectError(this.log, `Error starting MatterNode ${this.storeId}`, error);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'matter_stop':
|
|
136
|
+
if (this.device || msg.params.storeId !== this.storeId)
|
|
137
|
+
break;
|
|
138
|
+
try {
|
|
139
|
+
await this.stop();
|
|
140
|
+
this.server.respond({ ...msg, result: { storeId: this.storeId, success: true } });
|
|
141
|
+
await this.destroy(!this.pluginName);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
inspectError(this.log, `Error stopping MatterNode ${this.storeId}`, error);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
case 'matter_apimatter':
|
|
148
|
+
if (this.serverNode?.id === msg.params.id)
|
|
149
|
+
this.server.respond({ ...msg, result: { matter: getServerNodeData(this.serverNode) } });
|
|
150
|
+
break;
|
|
120
151
|
default:
|
|
121
152
|
if (this.verbose)
|
|
122
153
|
this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
|
@@ -153,6 +184,10 @@ export class MatterNode extends EventEmitter {
|
|
|
153
184
|
}
|
|
154
185
|
async create() {
|
|
155
186
|
this.log.info('Creating Matter node...');
|
|
187
|
+
const { result } = await this.server.fetch({ type: 'matterbridge_matterdata', src: this.server.name, dst: 'matterbridge' });
|
|
188
|
+
this.port = result.port;
|
|
189
|
+
this.passcode = result.passcode;
|
|
190
|
+
this.discriminator = result.discriminator;
|
|
156
191
|
await this.startMatterStorage();
|
|
157
192
|
this.pluginManager.matterbridge.nodeStorage = new NodeStorageManager({
|
|
158
193
|
dir: path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR),
|
|
@@ -967,7 +1002,7 @@ export class MatterNode extends EventEmitter {
|
|
|
967
1002
|
child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
|
|
968
1003
|
}
|
|
969
1004
|
}
|
|
970
|
-
async yieldToNode(timeout =
|
|
1005
|
+
async yieldToNode(timeout = 10) {
|
|
971
1006
|
await Promise.resolve();
|
|
972
1007
|
await new Promise((resolve) => {
|
|
973
1008
|
setImmediate(resolve);
|