@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.
@@ -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
@@ -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 {};
@@ -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, lastOsCpuUsage, lastProcessCpuUsage } from './cliEmitter.js';
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
- this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
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', hasParameter('ssl') ? 'wss' : 'ws');
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 (!hasParameter('ssl')) {
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(lastOsCpuUsage);
933
- this.matterbridge.systemInformation.processCpuUsage = formatPercent(lastProcessCpuUsage);
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);
@@ -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 = 100) {
1005
+ async yieldToNode(timeout = 10) {
971
1006
  await Promise.resolve();
972
1007
  await new Promise((resolve) => {
973
1008
  setImmediate(resolve);