@matterbridge/core 3.10.13-dev-20261007-320f739 → 3.10.13-dev-20261008-55d98fb
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 +369 -0
- package/dist/backendExpress.d.ts +21 -0
- package/dist/backendExpress.js +455 -0
- package/dist/backendWsServer.d.ts +39 -0
- package/dist/backendWsServer.js +393 -0
- package/dist/cli.js +6 -4
- 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,393 @@
|
|
|
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/restart') {
|
|
167
|
+
this.wssSendSnackbarMessage('Restarting matterbridge...', 0);
|
|
168
|
+
await this.server.fetch({ type: 'matterbridge_restart', src: 'frontend', dst: 'matterbridge', params: undefined });
|
|
169
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
170
|
+
}
|
|
171
|
+
else if (data.method === '/api/shutdown') {
|
|
172
|
+
this.wssSendSnackbarMessage('Shutting down matterbridge...', 0);
|
|
173
|
+
await this.server.fetch({ type: 'matterbridge_shutdown', src: 'frontend', dst: 'matterbridge', params: undefined });
|
|
174
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
175
|
+
}
|
|
176
|
+
else if (data.method === '/api/settings') {
|
|
177
|
+
const start = performance.now();
|
|
178
|
+
const settings = await this.backend.getApiSettings();
|
|
179
|
+
if (this.diagnostic)
|
|
180
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiSettings() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
181
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: settings });
|
|
182
|
+
}
|
|
183
|
+
else if (data.method === '/api/plugins') {
|
|
184
|
+
const start = performance.now();
|
|
185
|
+
const plugins = await this.backend.getApiPlugins();
|
|
186
|
+
if (this.diagnostic)
|
|
187
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
188
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
|
|
189
|
+
}
|
|
190
|
+
else if (data.method === '/api/devices') {
|
|
191
|
+
const start = performance.now();
|
|
192
|
+
const devices = await this.backend.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
|
|
193
|
+
if (this.diagnostic)
|
|
194
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
195
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
|
|
196
|
+
}
|
|
197
|
+
else if (data.method === '/api/clusters') {
|
|
198
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
199
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!isValidNumber(data.params.endpoint, 1)) {
|
|
203
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const start = performance.now();
|
|
207
|
+
const clusters = await this.backend.getApiCluster(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
|
|
208
|
+
if (this.diagnostic)
|
|
209
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
210
|
+
if (clusters) {
|
|
211
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: clusters });
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Endpoint not found in /api/clusters' });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (data.method === '/api/matter') {
|
|
218
|
+
if (!isValidString(data.params.id)) {
|
|
219
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const matter = await this.backend.getApiMatter(data.params.id);
|
|
223
|
+
if (!matter) {
|
|
224
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown server node id ${data.params.id} in /api/matter` });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
|
|
228
|
+
this.wssSendRefreshRequired('matter', { matter });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
inspectError(this.log, `Error parsing message from websocket client`, error);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
wssBroadcastMessage(msg) {
|
|
237
|
+
if (!this.hasActiveClients())
|
|
238
|
+
return;
|
|
239
|
+
try {
|
|
240
|
+
const stringifiedMsg = JSON.stringify(msg);
|
|
241
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
242
|
+
if (client.readyState === client.OPEN) {
|
|
243
|
+
client.send(stringifiedMsg);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
inspectError(this.log, `Error broadcasting message to websocket clients`, error);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
wssSendLogMessage(level, time, name, message) {
|
|
252
|
+
if (!this.hasActiveClients())
|
|
253
|
+
return;
|
|
254
|
+
if (!level || !time || !name || !message)
|
|
255
|
+
return;
|
|
256
|
+
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
257
|
+
message = message.replace(/^\*+/, '');
|
|
258
|
+
message = message.replace(/[\t\n]/g, '');
|
|
259
|
+
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
260
|
+
message = message.replace(/\\"/g, '"');
|
|
261
|
+
const maxContinuousLength = 100;
|
|
262
|
+
const keepStartLength = 20;
|
|
263
|
+
const keepEndLength = 20;
|
|
264
|
+
if (level !== 'spawn') {
|
|
265
|
+
message = message
|
|
266
|
+
.split(' ')
|
|
267
|
+
.map((word) => {
|
|
268
|
+
if (word.length > maxContinuousLength) {
|
|
269
|
+
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
270
|
+
}
|
|
271
|
+
return word;
|
|
272
|
+
})
|
|
273
|
+
.join(' ');
|
|
274
|
+
}
|
|
275
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
276
|
+
}
|
|
277
|
+
wssSendRefreshRequired(changed, params) {
|
|
278
|
+
if (!this.hasActiveClients())
|
|
279
|
+
return;
|
|
280
|
+
if (this.verbose)
|
|
281
|
+
this.log.debug('Sending a refresh required message to all connected clients');
|
|
282
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, lock: params?.lock, ...params } });
|
|
283
|
+
}
|
|
284
|
+
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
285
|
+
if (!this.hasActiveClients())
|
|
286
|
+
return;
|
|
287
|
+
if (this.verbose)
|
|
288
|
+
this.log.debug('Sending a restart required message to all connected clients');
|
|
289
|
+
if (snackbar)
|
|
290
|
+
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
291
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
292
|
+
}
|
|
293
|
+
wssSendRestartNotRequired(snackbar = true) {
|
|
294
|
+
if (!this.hasActiveClients())
|
|
295
|
+
return;
|
|
296
|
+
if (this.verbose)
|
|
297
|
+
this.log.debug('Sending a restart not required message to all connected clients');
|
|
298
|
+
if (snackbar)
|
|
299
|
+
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
300
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
301
|
+
}
|
|
302
|
+
wssSendUpdateRequired(version, devVersion = false) {
|
|
303
|
+
if (!this.hasActiveClients())
|
|
304
|
+
return;
|
|
305
|
+
if (this.verbose)
|
|
306
|
+
this.log.debug('Sending a matterbridge version update required message to all connected clients');
|
|
307
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { version, devVersion } });
|
|
308
|
+
}
|
|
309
|
+
wssSendPluginUpdateRequired(plugin, version, devVersion = false) {
|
|
310
|
+
if (!this.hasActiveClients())
|
|
311
|
+
return;
|
|
312
|
+
if (this.verbose)
|
|
313
|
+
this.log.debug('Sending a plugin version update required message to all connected clients');
|
|
314
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_update_required', success: true, response: { plugin, version, devVersion } });
|
|
315
|
+
}
|
|
316
|
+
wssSendPluginStatusUpdate(plugin, status) {
|
|
317
|
+
if (!this.hasActiveClients())
|
|
318
|
+
return;
|
|
319
|
+
if (this.verbose)
|
|
320
|
+
this.log.debug('Sending a plugin status update message to all connected clients');
|
|
321
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'plugin_status_update', success: true, response: { plugin, status } });
|
|
322
|
+
}
|
|
323
|
+
wssSendMatterbridgeStatusUpdate(status) {
|
|
324
|
+
if (!this.hasActiveClients())
|
|
325
|
+
return;
|
|
326
|
+
if (this.verbose)
|
|
327
|
+
this.log.debug('Sending a matterbridge status update message to all connected clients');
|
|
328
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'matterbridge_status_update', success: true, response: { status } });
|
|
329
|
+
}
|
|
330
|
+
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
331
|
+
if (!this.hasActiveClients())
|
|
332
|
+
return;
|
|
333
|
+
if (this.verbose)
|
|
334
|
+
this.log.debug('Sending a cpu update message to all connected clients');
|
|
335
|
+
this.wssBroadcastMessage({
|
|
336
|
+
id: 0,
|
|
337
|
+
src: 'Matterbridge',
|
|
338
|
+
dst: 'Frontend',
|
|
339
|
+
method: 'cpu_update',
|
|
340
|
+
success: true,
|
|
341
|
+
response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
345
|
+
if (!this.hasActiveClients())
|
|
346
|
+
return;
|
|
347
|
+
if (this.verbose)
|
|
348
|
+
this.log.debug('Sending a memory update message to all connected clients');
|
|
349
|
+
this.wssBroadcastMessage({
|
|
350
|
+
id: 0,
|
|
351
|
+
src: 'Matterbridge',
|
|
352
|
+
dst: 'Frontend',
|
|
353
|
+
method: 'memory_update',
|
|
354
|
+
success: true,
|
|
355
|
+
response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
359
|
+
if (!this.hasActiveClients())
|
|
360
|
+
return;
|
|
361
|
+
if (this.verbose)
|
|
362
|
+
this.log.debug('Sending a uptime update message to all connected clients');
|
|
363
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
364
|
+
}
|
|
365
|
+
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
366
|
+
if (!this.hasActiveClients())
|
|
367
|
+
return;
|
|
368
|
+
if (this.verbose)
|
|
369
|
+
this.log.debug('Sending a snackbar message to all connected clients');
|
|
370
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
371
|
+
}
|
|
372
|
+
wssSendCloseSnackbarMessage(message) {
|
|
373
|
+
if (!this.hasActiveClients())
|
|
374
|
+
return;
|
|
375
|
+
if (this.verbose)
|
|
376
|
+
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
377
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
378
|
+
}
|
|
379
|
+
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
380
|
+
if (!this.hasActiveClients())
|
|
381
|
+
return;
|
|
382
|
+
if (this.verbose)
|
|
383
|
+
this.log.debug('Sending an attribute update message to all connected clients');
|
|
384
|
+
this.wssBroadcastMessage({
|
|
385
|
+
id: 0,
|
|
386
|
+
src: 'Matterbridge',
|
|
387
|
+
dst: 'Frontend',
|
|
388
|
+
method: 'state_update',
|
|
389
|
+
success: true,
|
|
390
|
+
response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
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();
|
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);
|
package/dist/matterbridge.js
CHANGED
|
@@ -170,6 +170,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
170
170
|
this.server.close();
|
|
171
171
|
}
|
|
172
172
|
getPlatformMatterbridge() {
|
|
173
|
+
this.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
174
|
+
this.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
175
|
+
this.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
176
|
+
this.systemInformation.processUptime = formatUptime(Math.floor(process.uptime()));
|
|
177
|
+
this.systemInformation.rss = formatBytes(memoryFootprint());
|
|
178
|
+
this.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
179
|
+
this.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
173
180
|
return {
|
|
174
181
|
systemInformation: { ...this.systemInformation },
|
|
175
182
|
uuid: this.uuid,
|
|
@@ -193,6 +200,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
193
200
|
};
|
|
194
201
|
}
|
|
195
202
|
getSharedMatterbridge() {
|
|
203
|
+
this.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
204
|
+
this.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
205
|
+
this.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
206
|
+
this.systemInformation.processUptime = formatUptime(Math.floor(process.uptime()));
|
|
207
|
+
this.systemInformation.rss = formatBytes(memoryFootprint());
|
|
208
|
+
this.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
209
|
+
this.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
196
210
|
return {
|
|
197
211
|
systemInformation: { ...this.systemInformation },
|
|
198
212
|
uuid: this.uuid,
|
|
@@ -318,6 +332,15 @@ export class Matterbridge extends EventEmitter {
|
|
|
318
332
|
await this.nodeContext?.set('globalModulesDirectory', msg.params.prefix);
|
|
319
333
|
this.server.respond({ ...msg, result: { success: true } });
|
|
320
334
|
break;
|
|
335
|
+
case 'matterbridge_matterdata':
|
|
336
|
+
this.server.respond({ ...msg, result: { port: this.port, passcode: this.passcode, discriminator: this.discriminator } });
|
|
337
|
+
if (this.port !== undefined)
|
|
338
|
+
this.port++;
|
|
339
|
+
if (this.passcode !== undefined)
|
|
340
|
+
this.passcode++;
|
|
341
|
+
if (this.discriminator !== undefined)
|
|
342
|
+
this.discriminator++;
|
|
343
|
+
break;
|
|
321
344
|
case 'matterbridge_platform':
|
|
322
345
|
this.server.respond({ ...msg, result: { data: this.getPlatformMatterbridge(), success: true } });
|
|
323
346
|
break;
|
|
@@ -750,6 +773,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
750
773
|
}
|
|
751
774
|
else {
|
|
752
775
|
this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
776
|
+
if (this.globalModulesDirectory.includes('.bun'))
|
|
777
|
+
this.globalModulesDirectory = '';
|
|
753
778
|
}
|
|
754
779
|
if (this.globalModulesDirectory === '') {
|
|
755
780
|
this.log.debug(`Getting global node_modules directory...`);
|
|
@@ -924,7 +949,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
924
949
|
this.shutdown = true;
|
|
925
950
|
return;
|
|
926
951
|
}
|
|
927
|
-
if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
|
|
952
|
+
if ((getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined) && !hasParameter('experimental-backend'))
|
|
928
953
|
await this.frontend.start(getIntParameter('frontend'));
|
|
929
954
|
try {
|
|
930
955
|
await this.startMatterStorage();
|
|
@@ -1017,15 +1042,45 @@ export class Matterbridge extends EventEmitter {
|
|
|
1017
1042
|
this.log.warn('Fixed delay switch found. Waiting for ' + delay + ' seconds before starting matterbridge...');
|
|
1018
1043
|
await wait(delay * 1000, 'Fixed race condition delay', true);
|
|
1019
1044
|
}
|
|
1045
|
+
if (hasParameter('experimental-tracker')) {
|
|
1046
|
+
this.log.notice('***Experimental tracker enabled for Threads');
|
|
1047
|
+
for (let i = 0; i < 10; i++) {
|
|
1048
|
+
this.server.request({ type: 'manager_run', src: 'matterbridge', dst: 'manager', params: { name: 'Tracker' } });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1020
1051
|
if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
|
|
1021
1052
|
this.bridgeMode = 'bridge';
|
|
1022
1053
|
this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
|
|
1054
|
+
if (hasParameter('experimental-node')) {
|
|
1055
|
+
this.log.notice(`***Starting experimental MatterNode in ${this.bridgeMode} mode...`);
|
|
1056
|
+
this.server.request({
|
|
1057
|
+
type: 'manager_run',
|
|
1058
|
+
src: 'matterbridge',
|
|
1059
|
+
dst: 'manager',
|
|
1060
|
+
params: { name: 'RootNode', workerData: { threadName: 'RootNode', sharedMatterbridge: this.getSharedMatterbridge() } },
|
|
1061
|
+
});
|
|
1062
|
+
this.log.notice(`***Started experimental MatterNode in ${this.bridgeMode} mode`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1023
1065
|
await this.startBridge();
|
|
1024
1066
|
return;
|
|
1025
1067
|
}
|
|
1026
1068
|
if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
|
|
1027
1069
|
this.bridgeMode = 'childbridge';
|
|
1028
1070
|
this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
|
|
1071
|
+
if (hasParameter('experimental-node')) {
|
|
1072
|
+
this.log.notice(`***Starting experimental MatterNode in ${this.bridgeMode} mode...`);
|
|
1073
|
+
for (const plugin of this.plugins.array().filter((p) => p.enabled)) {
|
|
1074
|
+
this.server.request({
|
|
1075
|
+
type: 'manager_run',
|
|
1076
|
+
src: 'matterbridge',
|
|
1077
|
+
dst: 'manager',
|
|
1078
|
+
params: { name: 'RootNode', workerData: { threadName: 'RootNode', sharedMatterbridge: this.getSharedMatterbridge(), pluginName: plugin.name } },
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
this.log.notice(`***Started experimental MatterNode in ${this.bridgeMode} mode`);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1029
1084
|
await this.startChildbridge();
|
|
1030
1085
|
return;
|
|
1031
1086
|
}
|
|
@@ -1478,6 +1533,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
1478
1533
|
this.frontend.destroy();
|
|
1479
1534
|
this.plugins.destroy();
|
|
1480
1535
|
this.devices.destroy();
|
|
1536
|
+
this.server.request({ type: 'manager_shutdown', src: 'matterbridge', dst: 'manager', params: {} });
|
|
1537
|
+
await wait(100);
|
|
1481
1538
|
this.server.close();
|
|
1482
1539
|
if (this.nodeStorage && this.nodeContext) {
|
|
1483
1540
|
this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ThreadsWrapper } from '@matterbridge/thread/wrapper';
|
|
2
|
+
import { inspectError } from '@matterbridge/utils/error';
|
|
3
|
+
import { logModuleLoaded } from '@matterbridge/utils/loader';
|
|
4
|
+
logModuleLoaded('Backend', '\u001B[35m');
|
|
5
|
+
export default new ThreadsWrapper('Backend', async (worker) => {
|
|
6
|
+
worker.logger("info", 'Creating backend...');
|
|
7
|
+
try {
|
|
8
|
+
worker.logger("debug", 'Fetching shared Matterbridge state...');
|
|
9
|
+
const shared = (await worker.server.fetch({ type: 'matterbridge_shared', src: 'matterbridge', dst: 'matterbridge' }, 1000)).result.data;
|
|
10
|
+
worker.logger("debug", 'Shared Matterbridge state fetched; importing Backend...');
|
|
11
|
+
const { Backend } = await import('../backend.js');
|
|
12
|
+
worker.logger("debug", 'Backend imported; creating instance...');
|
|
13
|
+
const backend = new Backend(shared);
|
|
14
|
+
worker.logger("debug", 'Starting Backend...');
|
|
15
|
+
await backend.start();
|
|
16
|
+
worker.logger("debug", 'Backend started');
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const errorMessage = inspectError(worker.log, 'Failed to create backend', error);
|
|
21
|
+
worker.logger("error", errorMessage);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
});
|