@matterbridge/core 3.7.2-dev-20260330-bb55c39 → 3.7.2-dev-20260402-c12a10e

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,307 @@
1
+ import { Logger, LogLevel as MatterLogLevel } from '@matter/general';
2
+ import { BroadcastServer } from '@matterbridge/thread';
3
+ import { hasParameter } from '@matterbridge/utils/cli';
4
+ import { inspectError } from '@matterbridge/utils/error';
5
+ import { isValidNumber, isValidString } from '@matterbridge/utils/validate';
6
+ import { withTimeout } from '@matterbridge/utils/wait';
7
+ import { AnsiLogger, CYAN, debugStringify, nf } from 'node-ansi-logger';
8
+ import WebSocket, { WebSocketServer } from 'ws';
9
+ if (hasParameter('loader'))
10
+ console.log('\u001B[32mBackendWsServer loaded.\u001B[40;0m');
11
+ export class BackendsWsServer {
12
+ debug;
13
+ verbose;
14
+ webSocketServer;
15
+ log;
16
+ backend;
17
+ matterbridge;
18
+ server;
19
+ 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.backend = backend;
23
+ this.matterbridge = matterbridge;
24
+ this.log = new AnsiLogger({
25
+ logName: 'BackendWsServer',
26
+ logNameColor: '\x1b[38;5;97m',
27
+ logTimestampFormat: 4,
28
+ logLevel: this.debug ? "debug" : "info",
29
+ });
30
+ this.server = new BroadcastServer('frontend', this.log);
31
+ this.server.on('broadcast_message', this.broadcastMsgHandler.bind(this));
32
+ }
33
+ destroy() {
34
+ this.server.off('broadcast_message', this.broadcastMsgHandler.bind(this));
35
+ this.server.close();
36
+ }
37
+ async broadcastMsgHandler(msg) {
38
+ if (this.server.isWorkerRequest(msg)) {
39
+ switch (msg.type) {
40
+ case 'get_log_level':
41
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
42
+ break;
43
+ case 'set_log_level':
44
+ this.log.logLevel = msg.params.logLevel;
45
+ this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ async start() {
51
+ this.log.debug(`Creating WebSocketServer...`);
52
+ this.webSocketServer = new WebSocketServer({ noServer: true });
53
+ this.backend.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
54
+ this.webSocketServer.on('connection', (websocket, request) => {
55
+ const clientIp = request.socket.remoteAddress;
56
+ this.log.info(`WebSocketServer client ${CYAN}${clientIp}${nf} connected to Matterbridge`);
57
+ let callbackLogLevel = "notice";
58
+ if (this.log.logLevel === "info" || Logger.level === MatterLogLevel.INFO)
59
+ callbackLogLevel = "info";
60
+ if (this.log.logLevel === "debug" || Logger.level === MatterLogLevel.DEBUG)
61
+ callbackLogLevel = "debug";
62
+ AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
63
+ this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
64
+ websocket.on('message', (data) => {
65
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} sent a message`);
66
+ this.wsMessageHandler(websocket, data);
67
+ });
68
+ websocket.on('ping', () => {
69
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} ping received`);
70
+ websocket.pong();
71
+ });
72
+ websocket.on('pong', () => {
73
+ this.log.debug(`WebSocket client ${CYAN}${clientIp}${nf} pong received`);
74
+ });
75
+ websocket.on('close', (code, reason) => {
76
+ this.log.info(`WebSocket client ${CYAN}${clientIp}${nf} disconnected code ${code} reason ${reason.toString()}`);
77
+ if (this.webSocketServer?.clients.size === 0) {
78
+ AnsiLogger.setGlobalCallback(undefined);
79
+ this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
80
+ setTimeout(() => {
81
+ if (this.webSocketServer?.clients.size === 0) {
82
+ this.log.debug('All WebSocket clients disconnected. Auth clients list cleared');
83
+ this.backend.authClients.clear();
84
+ }
85
+ }, 1000).unref();
86
+ }
87
+ else {
88
+ this.log.debug(`WebSocketServer still has ${this.webSocketServer?.clients.size} connected clients`);
89
+ }
90
+ });
91
+ websocket.on('error', (error) => {
92
+ inspectError(this.log, `WebSocket client error`, error);
93
+ });
94
+ });
95
+ this.webSocketServer.on('close', () => {
96
+ this.log.debug(`WebSocketServer closed`);
97
+ });
98
+ this.webSocketServer.on('error', (ws, error) => {
99
+ inspectError(this.log, `WebSocketServer error`, error);
100
+ });
101
+ return this.webSocketServer;
102
+ }
103
+ async stop() {
104
+ if (this.webSocketServer) {
105
+ this.log.debug('Closing WebSocket server...');
106
+ this.webSocketServer.clients.forEach((client) => {
107
+ if (client.readyState === WebSocket.OPEN) {
108
+ client.close();
109
+ }
110
+ });
111
+ await withTimeout(new Promise((resolve) => {
112
+ this.webSocketServer?.close((error) => {
113
+ if (error) {
114
+ inspectError(this.log, `Error closing WebSocket server`, error);
115
+ }
116
+ else {
117
+ this.log.debug('WebSocket server closed successfully');
118
+ this.backend.emit('websocket_server_stopped');
119
+ }
120
+ resolve();
121
+ });
122
+ }), 10000, false);
123
+ this.webSocketServer.removeAllListeners();
124
+ this.webSocketServer = undefined;
125
+ }
126
+ else {
127
+ this.log.debug('WebSocket server is not running');
128
+ }
129
+ return this.webSocketServer;
130
+ }
131
+ hasActiveClients() {
132
+ return this.webSocketServer !== undefined && this.webSocketServer.clients.size > 0;
133
+ }
134
+ async wsMessageHandler(client, rawData) {
135
+ let data;
136
+ const sendResponse = (data) => {
137
+ if (client.readyState === client.OPEN) {
138
+ if ('response' in data) {
139
+ const { response, ...rest } = data;
140
+ this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
141
+ }
142
+ else if ('error' in data) {
143
+ this.log.debug(`Sending api error message: ${debugStringify(data)}`);
144
+ }
145
+ client.send(JSON.stringify(data));
146
+ }
147
+ else {
148
+ this.log.error('Cannot send api response, client not connected');
149
+ }
150
+ };
151
+ try {
152
+ data = JSON.parse(rawData.toString());
153
+ if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
154
+ this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
155
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
156
+ return;
157
+ }
158
+ this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
159
+ }
160
+ catch (error) {
161
+ inspectError(this.log, `Error parsing message from websocket client`, error);
162
+ return;
163
+ }
164
+ }
165
+ wssBroadcastMessage(msg) {
166
+ if (!this.hasActiveClients())
167
+ return;
168
+ try {
169
+ const stringifiedMsg = JSON.stringify(msg);
170
+ if (this.verbose)
171
+ this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
172
+ this.webSocketServer?.clients.forEach((client) => {
173
+ if (client.readyState === client.OPEN) {
174
+ client.send(stringifiedMsg);
175
+ }
176
+ });
177
+ }
178
+ catch (error) {
179
+ inspectError(this.log, `Error broadcasting message to websocket clients`, error);
180
+ }
181
+ }
182
+ wssSendLogMessage(level, time, name, message) {
183
+ if (!this.hasActiveClients())
184
+ return;
185
+ if (!level || !time || !name || !message)
186
+ return;
187
+ message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
188
+ message = message.replace(/^\*+/, '');
189
+ message = message.replace(/[\t\n]/g, '');
190
+ message = message.replace(/[\x00-\x1F\x7F]/g, '');
191
+ message = message.replace(/\\"/g, '"');
192
+ const maxContinuousLength = 100;
193
+ const keepStartLength = 20;
194
+ const keepEndLength = 20;
195
+ if (level !== 'spawn') {
196
+ message = message
197
+ .split(' ')
198
+ .map((word) => {
199
+ if (word.length > maxContinuousLength) {
200
+ return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
201
+ }
202
+ return word;
203
+ })
204
+ .join(' ');
205
+ }
206
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
207
+ }
208
+ wssSendRefreshRequired(changed, params) {
209
+ if (!this.hasActiveClients())
210
+ return;
211
+ if (this.verbose)
212
+ this.log.debug('Sending a refresh required message to all connected clients');
213
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, lock: params?.lock, ...params } });
214
+ }
215
+ wssSendRestartRequired(snackbar = true, fixed = false) {
216
+ if (!this.hasActiveClients())
217
+ return;
218
+ if (this.verbose)
219
+ this.log.debug('Sending a restart required message to all connected clients');
220
+ this.backend.restartRequired = true;
221
+ this.backend.fixedRestartRequired = fixed;
222
+ if (snackbar === true)
223
+ this.wssSendSnackbarMessage(`Restart required`, 0);
224
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
225
+ }
226
+ wssSendRestartNotRequired(snackbar = true) {
227
+ if (!this.hasActiveClients())
228
+ return;
229
+ if (this.verbose)
230
+ this.log.debug('Sending a restart not required message to all connected clients');
231
+ this.backend.restartRequired = false;
232
+ if (snackbar === true)
233
+ this.wssSendCloseSnackbarMessage(`Restart required`);
234
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
235
+ }
236
+ wssSendUpdateRequired(devVersion = false) {
237
+ if (!this.hasActiveClients())
238
+ return;
239
+ if (this.verbose)
240
+ this.log.debug('Sending an update required message to all connected clients');
241
+ this.backend.updateRequired = true;
242
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
243
+ }
244
+ wssSendCpuUpdate(cpuUsage, processCpuUsage) {
245
+ if (!this.hasActiveClients())
246
+ return;
247
+ if (this.verbose)
248
+ this.log.debug('Sending a cpu update message to all connected clients');
249
+ this.wssBroadcastMessage({
250
+ id: 0,
251
+ src: 'Matterbridge',
252
+ dst: 'Frontend',
253
+ method: 'cpu_update',
254
+ success: true,
255
+ response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
256
+ });
257
+ }
258
+ wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
259
+ if (!this.hasActiveClients())
260
+ return;
261
+ if (this.verbose)
262
+ this.log.debug('Sending a memory update message to all connected clients');
263
+ this.wssBroadcastMessage({
264
+ id: 0,
265
+ src: 'Matterbridge',
266
+ dst: 'Frontend',
267
+ method: 'memory_update',
268
+ success: true,
269
+ response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
270
+ });
271
+ }
272
+ wssSendUptimeUpdate(systemUptime, processUptime) {
273
+ if (!this.hasActiveClients())
274
+ return;
275
+ if (this.verbose)
276
+ this.log.debug('Sending a uptime update message to all connected clients');
277
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
278
+ }
279
+ wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
280
+ if (!this.hasActiveClients())
281
+ return;
282
+ if (this.verbose)
283
+ this.log.debug('Sending a snackbar message to all connected clients');
284
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
285
+ }
286
+ wssSendCloseSnackbarMessage(message) {
287
+ if (!this.hasActiveClients())
288
+ return;
289
+ if (this.verbose)
290
+ this.log.debug('Sending a close snackbar message to all connected clients');
291
+ this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
292
+ }
293
+ wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
294
+ if (!this.hasActiveClients())
295
+ return;
296
+ if (this.verbose)
297
+ this.log.debug('Sending an attribute update message to all connected clients');
298
+ this.wssBroadcastMessage({
299
+ id: 0,
300
+ src: 'Matterbridge',
301
+ dst: 'Frontend',
302
+ method: 'state_update',
303
+ success: true,
304
+ response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
305
+ });
306
+ }
307
+ }
@@ -1,6 +1,7 @@
1
1
  import { DoorLockServer } from '@matter/node/behaviors/door-lock';
2
2
  import { DoorLock } from '@matter/types/clusters/door-lock';
3
- declare const MatterbridgeDoorLockServer_base: import("@matter/node").ClusterBehavior.Type<import("@matter/types").ClusterTypeModifier.WithAlterations<DoorLock.Cluster, import("@matter/types").ClusterTypeModifier.ElementFlagAlterations<{
3
+ import { FabricIndex } from '@matter/types/datatype';
4
+ declare const MatterbridgeDoorLockServer_base: import("@matter/node").ClusterBehavior.Type<import("@matter/types").ClusterTypeModifier.WithAlterations<import("@matter/types").ClusterComposer.WithFeatures<DoorLock.Cluster, readonly [DoorLock.Feature.User, DoorLock.Feature.PinCredential]>, import("@matter/types").ClusterTypeModifier.ElementFlagAlterations<{
4
5
  readonly events: {
5
6
  readonly doorLockAlarm: true;
6
7
  readonly lockOperation: true;
@@ -11,18 +12,51 @@ declare const MatterbridgeDoorLockServer_base: import("@matter/node").ClusterBeh
11
12
  readonly unlockDoor: true;
12
13
  readonly unlockWithTimeout: true;
13
14
  };
14
- }>>, typeof DoorLockServer, import("@matter/node/behaviors/door-lock").DoorLockInterface>;
15
+ }>>, import("@matter/node").ClusterBehavior.Type<import("@matter/types").ClusterComposer.WithFeatures<DoorLock.Cluster, readonly [DoorLock.Feature.User, DoorLock.Feature.PinCredential]>, typeof DoorLockServer, import("@matter/node/behaviors/door-lock").DoorLockInterface>, import("@matter/node/behaviors/door-lock").DoorLockInterface>;
15
16
  export declare class MatterbridgeDoorLockServer extends MatterbridgeDoorLockServer_base {
16
17
  protected internal: MatterbridgeDoorLockServer.Internal;
17
18
  initialize(): Promise<void>;
18
19
  lockDoor(request: DoorLock.LockDoorRequest): Promise<void>;
19
20
  unlockDoor(request: DoorLock.UnlockDoorRequest): Promise<void>;
20
21
  unlockWithTimeout(request: DoorLock.UnlockWithTimeoutRequest): Promise<void>;
22
+ setUser(request: DoorLock.SetUserRequest): Promise<void>;
23
+ getUser(request: DoorLock.GetUserRequest): Promise<DoorLock.GetUserResponse>;
24
+ clearUser(request: DoorLock.ClearUserRequest): Promise<void>;
25
+ setCredential(request: DoorLock.SetCredentialRequest): Promise<DoorLock.SetCredentialResponse>;
26
+ getCredentialStatus(request: DoorLock.GetCredentialStatusRequest): Promise<DoorLock.GetCredentialStatusResponse>;
27
+ clearCredential(request: DoorLock.ClearCredentialRequest): Promise<void>;
28
+ private validateUserIndex;
29
+ private getNextUserIndex;
30
+ private getAccessingFabricIndex;
31
+ private getAccessingNodeId;
32
+ private getOperationSource;
33
+ private getLockDataTypeForCredentialType;
34
+ private getCredentialDataIndex;
35
+ private getStoredCredentialTypes;
36
+ private findStoredCredential;
37
+ private getNextOccupiedCredentialIndex;
21
38
  }
22
39
  export declare namespace MatterbridgeDoorLockServer {
40
+ type StoredCredential = DoorLock.Credential & {
41
+ creatorFabricIndex: FabricIndex | null;
42
+ lastModifiedFabricIndex: FabricIndex | null;
43
+ credentialData: Uint8Array;
44
+ };
45
+ type StoredUser = {
46
+ userIndex: number;
47
+ userName: string | null;
48
+ userUniqueId: number | null;
49
+ userStatus: DoorLock.UserStatus | null;
50
+ userType: DoorLock.UserType | null;
51
+ credentialRule: DoorLock.CredentialRule | null;
52
+ credentials: StoredCredential[] | null;
53
+ creatorFabricIndex: FabricIndex | null;
54
+ lastModifiedFabricIndex: FabricIndex | null;
55
+ };
23
56
  class Internal {
24
57
  enableTimeout: boolean;
25
58
  unlockTimeout: NodeJS.Timeout | undefined;
59
+ users: StoredUser[];
26
60
  }
27
61
  }
28
62
  export {};