@matterbridge/core 3.10.13-dev-20261005-49e0dbd → 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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import EventEmitter from 'node:events';
2
- import type { ApiDevice, ApiPlugin, ApiSettings, SharedMatterbridge } from '@matterbridge/types';
2
+ import { type ApiClusters, type ApiDevice, type ApiPlugin, type ApiSettings, type SharedMatterbridge, type ApiMatter } from '@matterbridge/types';
3
3
  import type { BackendExpress } from './backendExpress.js';
4
4
  import type { BackendWsServer } from './backendWsServer.js';
5
5
  interface BackendEvents {
@@ -12,6 +12,7 @@ interface BackendEvents {
12
12
  export declare class Backend extends EventEmitter<BackendEvents> {
13
13
  private debug;
14
14
  private verbose;
15
+ private diagnostic;
15
16
  private log;
16
17
  private matterbridge;
17
18
  private readonly server;
@@ -19,6 +20,8 @@ export declare class Backend extends EventEmitter<BackendEvents> {
19
20
  private listening;
20
21
  private httpServer;
21
22
  private httpsServer;
23
+ readonly secure: boolean;
24
+ readonly requestCert: boolean;
22
25
  backendExpress: BackendExpress | undefined;
23
26
  backendWsServer: BackendWsServer | undefined;
24
27
  storedPassword: string | undefined;
@@ -32,6 +35,8 @@ export declare class Backend extends EventEmitter<BackendEvents> {
32
35
  getApiSettings(): Promise<ApiSettings>;
33
36
  getApiPlugins(): Promise<ApiPlugin[]>;
34
37
  getApiDevices(pluginName?: string): Promise<ApiDevice[]>;
38
+ getApiCluster(pluginName: string, endpointNumber: number, serialNumber?: string, uniqueId?: string): Promise<ApiClusters | undefined>;
35
39
  generateDiagnostic(): Promise<void>;
40
+ getApiMatter(id: string): Promise<ApiMatter | undefined>;
36
41
  }
37
42
  export {};
package/dist/backend.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import EventEmitter from 'node:events';
2
2
  import path from 'node:path';
3
- import { BroadcastServer } from '@matterbridge/thread';
4
- import { getParameter, hasParameter } from '@matterbridge/utils/cli';
3
+ import { BroadcastServer } from '@matterbridge/thread/server';
4
+ import { NODE_STORAGE_DIR, } from '@matterbridge/types';
5
+ import { getParameter, hasAnyParameter, hasParameter } from '@matterbridge/utils/cli';
6
+ import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
5
7
  import { getErrorMessage, inspectError, logError } from '@matterbridge/utils/error';
6
8
  import { logModuleLoaded } from '@matterbridge/utils/loader';
9
+ import { fireAndForget } from '@matterbridge/utils/wait';
7
10
  import { AnsiLogger, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
11
+ import { NodeStorageManager } from 'node-persist-manager';
8
12
  logModuleLoaded('Backend');
9
13
  export class Backend extends EventEmitter {
10
14
  debug;
11
15
  verbose;
16
+ diagnostic;
12
17
  log;
13
18
  matterbridge;
14
19
  server;
@@ -16,6 +21,8 @@ export class Backend extends EventEmitter {
16
21
  listening = false;
17
22
  httpServer;
18
23
  httpsServer;
24
+ secure;
25
+ requestCert;
19
26
  backendExpress;
20
27
  backendWsServer;
21
28
  storedPassword = undefined;
@@ -23,8 +30,11 @@ export class Backend extends EventEmitter {
23
30
  authClientsTimeout = undefined;
24
31
  constructor(matterbridge) {
25
32
  super();
26
- this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-frontend') || hasParameter('verbose-frontend');
27
- this.verbose = hasParameter('verbose') || hasParameter('verbose-frontend');
33
+ this.debug = hasAnyParameter('debug', 'verbose', 'debug-backend', 'verbose-backend');
34
+ this.verbose = hasAnyParameter('verbose', 'verbose-backend');
35
+ this.diagnostic = hasAnyParameter('diagnostic', 'diagnostic-backend');
36
+ this.secure = hasAnyParameter('ssl', 'tls', 'mtls');
37
+ this.requestCert = hasParameter('mtls');
28
38
  this.matterbridge = matterbridge;
29
39
  this.log = new AnsiLogger({
30
40
  logName: 'Backend',
@@ -33,7 +43,7 @@ export class Backend extends EventEmitter {
33
43
  logLevel: this.debug ? "debug" : "info",
34
44
  });
35
45
  this.server = new BroadcastServer('frontend', this.log);
36
- this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
46
+ this.server.on('broadcast_message', (msg) => fireAndForget(this.broadcastMsgHandler(msg), this.log, 'Broadcast message handler'));
37
47
  }
38
48
  destroy() {
39
49
  this.server.close();
@@ -54,81 +64,32 @@ export class Backend extends EventEmitter {
54
64
  async start(port = 8283) {
55
65
  this.log.debug('Starting backend...');
56
66
  this.port = port;
57
- const { BackendExpress } = await import('./backendExpress.js');
58
- const { BackendWsServer } = await import('./backendWsServer.js');
59
- this.backendExpress = new BackendExpress(this.matterbridge, this);
60
- this.backendWsServer = new BackendWsServer(this.matterbridge, this);
61
- if (!hasParameter('ssl')) {
62
- const http = await import('node:http');
63
- await this.backendExpress.start();
67
+ const start = performance.now();
68
+ const nodeStorage = new NodeStorageManager({
69
+ dir: path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR),
70
+ writeQueue: false,
71
+ expiredInterval: undefined,
72
+ logging: false,
73
+ });
74
+ try {
75
+ const nodeContext = await nodeStorage.createStorage('matterbridge');
64
76
  try {
65
- this.log.debug(`Creating HTTP server...`);
66
- this.httpServer = http.createServer(this.backendExpress.expressApp);
77
+ this.storedPassword = await nodeContext.get('password', '');
67
78
  }
68
- catch (error) {
69
- logError(this.log, `Failed to create HTTP server`, error);
70
- this.emit('server_error', error);
71
- await this.backendExpress.stop();
72
- return;
79
+ finally {
80
+ await nodeContext.close();
73
81
  }
74
- await this.backendWsServer.start();
75
- this.httpServer.listen(this.port, getParameter('bind'), () => {
76
- const addr = this.httpServer?.address();
77
- if (addr && typeof addr !== 'string') {
78
- this.log.info(`The frontend http server is bound to ${addr.family} ${addr.address}:${addr.port}`);
79
- }
80
- if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
81
- this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
82
- if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
83
- this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
84
- this.listening = true;
85
- this.emit('server_listening', 'http', this.port);
86
- });
87
- this.httpServer.on('upgrade', (req, socket, head) => {
88
- try {
89
- if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
90
- this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
91
- socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
92
- return socket.destroy();
93
- }
94
- const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
95
- const password = url.searchParams.get('password') ?? '';
96
- if (password !== this.storedPassword) {
97
- this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
98
- socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
99
- return socket.destroy();
100
- }
101
- this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
102
- if (req.socket.remoteAddress)
103
- this.authClients.add(req.socket.remoteAddress);
104
- this.backendWsServer?.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
105
- this.backendWsServer?.webSocketServer?.emit('connection', ws, req);
106
- });
107
- }
108
- catch (err) {
109
- {
110
- inspectError(this.log, 'WebSocket upgrade error:', err);
111
- socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
112
- socket.destroy();
113
- }
114
- }
115
- return undefined;
116
- });
117
- this.httpServer.on('error', (error) => {
118
- this.log.error(`Frontend http server error listening on ${this.port}`);
119
- switch (error.code) {
120
- case 'EACCES':
121
- this.log.error(`Port ${this.port} requires elevated privileges`);
122
- break;
123
- case 'EADDRINUSE':
124
- this.log.error(`Port ${this.port} is already in use`);
125
- break;
126
- }
127
- this.emit('server_error', error);
128
- return undefined;
129
- });
130
82
  }
131
- else {
83
+ finally {
84
+ await nodeStorage.close();
85
+ if (this.diagnostic)
86
+ writeDiagnostic('Backend', `Loading storedPassword took ${(performance.now() - start).toFixed(2)} ms`);
87
+ }
88
+ const { BackendExpress } = await import('./backendExpress.js');
89
+ const { BackendWsServer } = await import('./backendWsServer.js');
90
+ this.backendExpress = new BackendExpress(this.matterbridge, this);
91
+ this.backendWsServer = new BackendWsServer(this.matterbridge, this);
92
+ if (this.secure) {
132
93
  let cert;
133
94
  let key;
134
95
  let ca;
@@ -188,7 +149,7 @@ export class Backend extends EventEmitter {
188
149
  }
189
150
  httpsServerOptions = { cert: fullChain ?? cert, key, ca };
190
151
  }
191
- if (hasParameter('mtls')) {
152
+ if (this.requestCert) {
192
153
  httpsServerOptions.requestCert = true;
193
154
  httpsServerOptions.rejectUnauthorized = true;
194
155
  }
@@ -219,16 +180,19 @@ export class Backend extends EventEmitter {
219
180
  });
220
181
  this.httpsServer.on('upgrade', (req, socket, head) => {
221
182
  try {
222
- if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
183
+ const upgrade = (req.headers.upgrade || '').toLowerCase();
184
+ if (upgrade !== 'websocket') {
223
185
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
224
- return socket.destroy();
186
+ socket.destroy();
187
+ return;
225
188
  }
226
189
  const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
227
190
  const password = url.searchParams.get('password') ?? '';
228
191
  if (password !== this.storedPassword) {
229
192
  this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
230
193
  socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
231
- return socket.destroy();
194
+ socket.destroy();
195
+ return;
232
196
  }
233
197
  this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
234
198
  if (req.socket.remoteAddress)
@@ -244,7 +208,6 @@ export class Backend extends EventEmitter {
244
208
  socket.destroy();
245
209
  }
246
210
  }
247
- return undefined;
248
211
  });
249
212
  this.httpsServer.on('error', (error) => {
250
213
  this.log.error(`Frontend https server error listening on ${this.port}`);
@@ -257,7 +220,78 @@ export class Backend extends EventEmitter {
257
220
  break;
258
221
  }
259
222
  this.emit('server_error', error);
223
+ });
224
+ }
225
+ else {
226
+ const http = await import('node:http');
227
+ await this.backendExpress.start();
228
+ try {
229
+ this.log.debug(`Creating HTTP server...`);
230
+ this.httpServer = http.createServer(this.backendExpress.expressApp);
231
+ }
232
+ catch (error) {
233
+ logError(this.log, `Failed to create HTTP server`, error);
234
+ this.emit('server_error', error);
235
+ await this.backendExpress.stop();
260
236
  return;
237
+ }
238
+ await this.backendWsServer.start();
239
+ this.httpServer.listen(this.port, getParameter('bind'), () => {
240
+ const addr = this.httpServer?.address();
241
+ if (addr && typeof addr !== 'string') {
242
+ this.log.info(`The frontend http server is bound to ${addr.family} ${addr.address}:${addr.port}`);
243
+ }
244
+ if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
245
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
246
+ if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
247
+ this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
248
+ this.listening = true;
249
+ this.emit('server_listening', 'http', this.port);
250
+ });
251
+ this.httpServer.on('upgrade', (req, socket, head) => {
252
+ try {
253
+ const upgrade = (req.headers.upgrade || '').toLowerCase();
254
+ if (upgrade !== 'websocket') {
255
+ this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
256
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
257
+ socket.destroy();
258
+ return;
259
+ }
260
+ const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
261
+ const password = url.searchParams.get('password') ?? '';
262
+ if (password !== this.storedPassword) {
263
+ this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
264
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
265
+ socket.destroy();
266
+ return;
267
+ }
268
+ this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
269
+ if (req.socket.remoteAddress)
270
+ this.authClients.add(req.socket.remoteAddress);
271
+ this.backendWsServer?.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
272
+ this.backendWsServer?.webSocketServer?.emit('connection', ws, req);
273
+ });
274
+ }
275
+ catch (err) {
276
+ {
277
+ inspectError(this.log, 'WebSocket upgrade error:', err);
278
+ socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
279
+ socket.destroy();
280
+ }
281
+ }
282
+ return;
283
+ });
284
+ this.httpServer.on('error', (error) => {
285
+ this.log.error(`Frontend http server error listening on ${this.port}`);
286
+ switch (error.code) {
287
+ case 'EACCES':
288
+ this.log.error(`Port ${this.port} requires elevated privileges`);
289
+ break;
290
+ case 'EADDRINUSE':
291
+ this.log.error(`Port ${this.port} is already in use`);
292
+ break;
293
+ }
294
+ this.emit('server_error', error);
261
295
  });
262
296
  }
263
297
  this.log.debug('Backend started');
@@ -293,16 +327,43 @@ export class Backend extends EventEmitter {
293
327
  this.log.debug('Backend stopped');
294
328
  }
295
329
  async getApiSettings() {
330
+ const start = performance.now();
296
331
  const response = await this.server.fetch({ type: 'matterbridge_apisettings', src: 'frontend', dst: 'matterbridge', params: undefined });
332
+ if (this.diagnostic)
333
+ writeDiagnostic('Backend', `getApiSettings() took ${(performance.now() - start).toFixed(2)} ms`);
297
334
  return response.result.data;
298
335
  }
299
336
  async getApiPlugins() {
337
+ const start = performance.now();
300
338
  const response = await this.server.fetch({ type: 'plugins_apipluginarray', src: 'frontend', dst: 'plugins', params: undefined });
339
+ if (this.diagnostic)
340
+ writeDiagnostic('Backend', `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
301
341
  return response.result.plugins;
302
342
  }
303
343
  async getApiDevices(pluginName) {
304
- const _response = await this.server.fetch({ type: 'devices_basearray', src: 'frontend', dst: 'devices', params: { pluginName } });
305
- return [];
344
+ const start = performance.now();
345
+ const response = await this.server.fetch({ type: 'devices_apidevicearray', src: 'frontend', dst: 'devices', params: { pluginName } });
346
+ if (this.diagnostic)
347
+ writeDiagnostic('Backend', `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
348
+ return response.result.devices;
349
+ }
350
+ async getApiCluster(pluginName, endpointNumber, serialNumber, uniqueId) {
351
+ const start = performance.now();
352
+ this.log.debug(`Retrieving API cluster for plugin: ${pluginName}, endpoint: ${endpointNumber}, serial: ${serialNumber}, uniqueId: ${uniqueId}`);
353
+ if (this.diagnostic)
354
+ writeDiagnostic('Backend', `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
355
+ return undefined;
356
+ }
357
+ async generateDiagnostic() {
358
+ const start = performance.now();
359
+ if (this.diagnostic)
360
+ writeDiagnostic('Backend', `generateDiagnostic() took ${(performance.now() - start).toFixed(2)} ms`);
361
+ }
362
+ async getApiMatter(id) {
363
+ const start = performance.now();
364
+ const response = await this.server.fetch({ type: 'matter_apimatter', src: 'frontend', dst: 'matter', params: { id } });
365
+ if (this.diagnostic)
366
+ writeDiagnostic('Backend', `getApiMatter() took ${(performance.now() - start).toFixed(2)} ms`);
367
+ return response.result.matter;
306
368
  }
307
- async generateDiagnostic() { }
308
369
  }
@@ -4,6 +4,7 @@ import type { Backend } from './backend.js';
4
4
  export declare class BackendExpress {
5
5
  private debug;
6
6
  private verbose;
7
+ private diagnostic;
7
8
  private log;
8
9
  private backend;
9
10
  private matterbridge;
@@ -1,12 +1,13 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { BroadcastServer } from '@matterbridge/thread';
3
+ import { BroadcastServer } from '@matterbridge/thread/server';
4
4
  import { MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_BACKUP_FILE, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, MATTERBRIDGE_PLUGIN_CONFIG_FILE, MATTERBRIDGE_PLUGIN_STORAGE_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
5
5
  import { isBun } from '@matterbridge/utils/bun';
6
- import { hasParameter } from '@matterbridge/utils/cli';
6
+ import { hasAnyParameter } from '@matterbridge/utils/cli';
7
7
  import { getErrorMessage } from '@matterbridge/utils/error';
8
8
  import { formatBytes } from '@matterbridge/utils/format';
9
9
  import { logModuleLoaded } from '@matterbridge/utils/loader';
10
+ import { fireAndForget } from '@matterbridge/utils/wait';
10
11
  import escapeHtml from 'escape-html';
11
12
  import express from 'express';
12
13
  import { rateLimit } from 'express-rate-limit';
@@ -16,6 +17,7 @@ logModuleLoaded('BackendExpress');
16
17
  export class BackendExpress {
17
18
  debug;
18
19
  verbose;
20
+ diagnostic;
19
21
  log;
20
22
  backend;
21
23
  matterbridge;
@@ -27,8 +29,9 @@ export class BackendExpress {
27
29
  expressApp;
28
30
  serverFetchTimeout = 5000;
29
31
  constructor(matterbridge, backend) {
30
- this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-frontend') || hasParameter('verbose-frontend');
31
- this.verbose = hasParameter('verbose') || hasParameter('verbose-frontend');
32
+ this.debug = hasAnyParameter('debug', 'verbose', 'debug-backend', 'verbose-backend');
33
+ this.verbose = hasAnyParameter('verbose', 'verbose-backend');
34
+ this.diagnostic = hasAnyParameter('diagnostic', 'diagnostic-backend');
32
35
  this.backend = backend;
33
36
  this.matterbridge = matterbridge;
34
37
  this.log = new AnsiLogger({
@@ -38,7 +41,7 @@ export class BackendExpress {
38
41
  logLevel: this.debug ? "debug" : "info",
39
42
  });
40
43
  this.server = new BroadcastServer('frontend', this.log);
41
- this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
44
+ this.server.on('broadcast_message', (msg) => fireAndForget(this.broadcastMsgHandler(msg), this.log, 'Broadcast message handler'));
42
45
  }
43
46
  destroy() {
44
47
  this.server.close();
@@ -53,7 +56,6 @@ export class BackendExpress {
53
56
  this.log.logLevel = msg.params.logLevel;
54
57
  this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
55
58
  break;
56
- default:
57
59
  }
58
60
  }
59
61
  }
@@ -5,6 +5,7 @@ import type { Backend } from './backend.js';
5
5
  export declare class BackendWsServer {
6
6
  private debug;
7
7
  private verbose;
8
+ private diagnostic;
8
9
  private log;
9
10
  private backend;
10
11
  private matterbridge;
@@ -1,6 +1,7 @@
1
1
  import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
2
- import { BroadcastServer } from '@matterbridge/thread';
3
- import { hasParameter } from '@matterbridge/utils/cli';
2
+ import { BroadcastServer } from '@matterbridge/thread/server';
3
+ import { hasAnyParameter } from '@matterbridge/utils/cli';
4
+ import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
4
5
  import { inspectError } from '@matterbridge/utils/error';
5
6
  import { logModuleLoaded } from '@matterbridge/utils/loader';
6
7
  import { isValidNumber, isValidString } from '@matterbridge/utils/validate';
@@ -11,14 +12,16 @@ logModuleLoaded('BackendWsServer');
11
12
  export class BackendWsServer {
12
13
  debug;
13
14
  verbose;
15
+ diagnostic;
14
16
  log;
15
17
  backend;
16
18
  matterbridge;
17
19
  server;
18
20
  webSocketServer;
19
21
  constructor(matterbridge, backend) {
20
- this.debug = hasParameter('debug') || hasParameter('verbose') || hasParameter('debug-frontend') || hasParameter('verbose-frontend');
21
- this.verbose = hasParameter('verbose') || hasParameter('verbose-frontend');
22
+ this.debug = hasAnyParameter('debug', 'verbose', 'debug-backend', 'verbose-backend');
23
+ this.verbose = hasAnyParameter('verbose', 'verbose-backend');
24
+ this.diagnostic = hasAnyParameter('diagnostic', 'diagnostic-backend');
22
25
  this.backend = backend;
23
26
  this.matterbridge = matterbridge;
24
27
  this.log = new AnsiLogger({
@@ -28,12 +31,12 @@ export class BackendWsServer {
28
31
  logLevel: this.debug ? "debug" : "info",
29
32
  });
30
33
  this.server = new BroadcastServer('frontend', this.log);
31
- this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
34
+ this.server.on('broadcast_message', (msg) => fireAndForget(this.broadcastMsgHandler(msg), this.log, 'Broadcast message handler'));
32
35
  }
33
36
  destroy() {
34
37
  this.server.close();
35
38
  }
36
- broadcastMsgHandler(msg) {
39
+ async broadcastMsgHandler(msg) {
37
40
  if (this.server.isWorkerRequest(msg)) {
38
41
  switch (msg.type) {
39
42
  case 'get_log_level':
@@ -43,14 +46,13 @@ export class BackendWsServer {
43
46
  this.log.logLevel = msg.params.logLevel;
44
47
  this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
45
48
  break;
46
- default:
47
49
  }
48
50
  }
49
51
  }
50
52
  async start() {
51
53
  this.log.debug(`Creating WebSocketServer...`);
52
54
  this.webSocketServer = new WebSocketServer({ noServer: true });
53
- this.backend.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
55
+ this.backend.emit('websocket_server_listening', this.backend.secure ? 'wss' : 'ws');
54
56
  this.webSocketServer.on('connection', (websocket, request) => {
55
57
  const clientIp = request.socket.remoteAddress;
56
58
  this.log.info(`WebSocketServer client ${CYAN}${clientIp}${nf} connected to Matterbridge`);
@@ -139,7 +141,7 @@ export class BackendWsServer {
139
141
  const { response, ...rest } = data;
140
142
  this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
141
143
  }
142
- else if ('error' in data) {
144
+ else {
143
145
  this.log.debug(`Sending api error message: ${debugStringify(data)}`);
144
146
  }
145
147
  const bigintReplacer = (_, v) => (typeof v === 'bigint' ? `${v}n` : v);
@@ -158,6 +160,73 @@ export class BackendWsServer {
158
160
  return;
159
161
  }
160
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
+ }
161
230
  }
162
231
  catch (error) {
163
232
  inspectError(this.log, `Error parsing message from websocket client`, error);
@@ -169,8 +238,6 @@ export class BackendWsServer {
169
238
  return;
170
239
  try {
171
240
  const stringifiedMsg = JSON.stringify(msg);
172
- if (this.verbose)
173
- this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
174
241
  this.webSocketServer?.clients.forEach((client) => {
175
242
  if (client.readyState === client.OPEN) {
176
243
  client.send(stringifiedMsg);
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,6 +102,11 @@ 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`);
105
+ if (hasParameter('experimental-backend')) {
106
+ log.debug(`***Starting experimental Backend thread...`);
107
+ manager.runThread('Backend');
108
+ log.debug(`***Started experimental Backend thread`);
109
+ }
103
110
  if (!instance || instance.shutdown) {
104
111
  await shutdown();
105
112
  }
@@ -1,4 +1,4 @@
1
- import type { BaseDevice } from '@matterbridge/types';
1
+ import type { ApiDevice, BaseDevice } from '@matterbridge/types';
2
2
  import { LogLevel } from 'node-ansi-logger';
3
3
  import type { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
4
4
  export declare function toBaseDevice(device: MatterbridgeEndpoint | BaseDevice): BaseDevice;
@@ -21,6 +21,7 @@ export declare class DeviceManager {
21
21
  private toBaseDevice;
22
22
  array(): MatterbridgeEndpoint[];
23
23
  baseArray(pluginName?: string): BaseDevice[];
24
+ apiDeviceArray(pluginName?: string): ApiDevice[];
24
25
  [Symbol.iterator](): IterableIterator<MatterbridgeEndpoint>;
25
26
  forEach(callback: (device: MatterbridgeEndpoint) => Promise<void>): Promise<void>;
26
27
  set logLevel(logLevel: LogLevel);