@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 +6 -1
- package/dist/backend.js +145 -84
- package/dist/backendExpress.d.ts +1 -0
- package/dist/backendExpress.js +8 -6
- package/dist/backendWsServer.d.ts +1 -0
- package/dist/backendWsServer.js +78 -11
- package/dist/cli.js +8 -1
- package/dist/deviceManager.d.ts +2 -1
- package/dist/deviceManager.js +29 -0
- package/dist/frontend.d.ts +1 -5
- package/dist/frontend.js +19 -300
- package/dist/matterNode.d.ts +3 -8
- package/dist/matterNode.js +62 -117
- package/dist/matterNodeHelpers.d.ts +15 -0
- package/dist/matterNodeHelpers.js +393 -0
- package/dist/matterbridge.d.ts +1 -6
- package/dist/matterbridge.js +106 -117
- package/dist/pluginManager.js +2 -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
package/dist/backend.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import EventEmitter from 'node:events';
|
|
2
|
-
import type
|
|
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 {
|
|
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 =
|
|
27
|
-
this.verbose =
|
|
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
|
|
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
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
66
|
-
this.httpServer = http.createServer(this.backendExpress.expressApp);
|
|
77
|
+
this.storedPassword = await nodeContext.get('password', '');
|
|
67
78
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
305
|
-
|
|
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
|
}
|
package/dist/backendExpress.d.ts
CHANGED
package/dist/backendExpress.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
31
|
-
this.verbose =
|
|
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
|
|
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
|
}
|
package/dist/backendWsServer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
|
|
2
|
-
import { BroadcastServer } from '@matterbridge/thread';
|
|
3
|
-
import {
|
|
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 =
|
|
21
|
-
this.verbose =
|
|
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
|
|
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',
|
|
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
|
|
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
|
}
|
package/dist/deviceManager.d.ts
CHANGED
|
@@ -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);
|