matterbridge 1.7.3 → 2.0.0-edge1
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/CHANGELOG.md +20 -1
- package/dist/cli.js +3 -13
- package/dist/cli.js.map +1 -1
- package/dist/deviceManager.d.ts +7 -7
- package/dist/deviceManager.d.ts.map +1 -1
- package/dist/deviceManager.js +2 -2
- package/dist/deviceManager.js.map +1 -1
- package/dist/frontend.d.ts +98 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +1377 -0
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/matter/export.d.ts.map +1 -1
- package/dist/matter/export.js +0 -1
- package/dist/matter/export.js.map +1 -1
- package/dist/matterbridge.d.ts +82 -208
- package/dist/matterbridge.d.ts.map +1 -1
- package/dist/matterbridge.js +777 -2310
- package/dist/matterbridge.js.map +1 -1
- package/dist/matterbridgeBehaviors.d.ts +32 -851
- package/dist/matterbridgeBehaviors.d.ts.map +1 -1
- package/dist/matterbridgeBehaviors.js +22 -2
- package/dist/matterbridgeBehaviors.js.map +1 -1
- package/dist/matterbridgeEndpoint.d.ts +100 -9089
- package/dist/matterbridgeEndpoint.d.ts.map +1 -1
- package/dist/matterbridgeEndpoint.js +59 -31
- package/dist/matterbridgeEndpoint.js.map +1 -1
- package/dist/matterbridgePlatform.d.ts +14 -28
- package/dist/matterbridgePlatform.d.ts.map +1 -1
- package/dist/matterbridgePlatform.js +7 -23
- package/dist/matterbridgePlatform.js.map +1 -1
- package/dist/matterbridgeTypes.d.ts.map +1 -1
- package/dist/matterbridgeTypes.js +4 -0
- package/dist/matterbridgeTypes.js.map +1 -1
- package/dist/pluginManager.d.ts +1 -1
- package/dist/pluginManager.d.ts.map +1 -1
- package/dist/pluginManager.js +5 -11
- package/dist/pluginManager.js.map +1 -1
- package/dist/utils/utils.d.ts +1 -1
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +6 -6
- package/dist/utils/utils.js.map +1 -1
- package/frontend/build/asset-manifest.json +3 -3
- package/frontend/build/index.html +1 -1
- package/frontend/build/static/js/{main.6bbd1772.js → main.ea28015b.js} +3 -3
- package/frontend/build/static/js/main.ea28015b.js.map +1 -0
- package/npm-shrinkwrap.json +9 -9
- package/package.json +2 -3
- package/dist/cli.d.ts +0 -25
- package/dist/cluster/export.d.ts +0 -2
- package/dist/defaultConfigSchema.d.ts +0 -27
- package/dist/index.d.ts +0 -40
- package/dist/logger/export.d.ts +0 -2
- package/dist/matter/export.d.ts +0 -11
- package/dist/matterbridgeAccessoryPlatform.d.ts +0 -39
- package/dist/matterbridgeDevice.d.ts +0 -7077
- package/dist/matterbridgeDevice.d.ts.map +0 -1
- package/dist/matterbridgeDevice.js +0 -2736
- package/dist/matterbridgeDevice.js.map +0 -1
- package/dist/matterbridgeDynamicPlatform.d.ts +0 -39
- package/dist/matterbridgeEdge.d.ts +0 -91
- package/dist/matterbridgeEdge.d.ts.map +0 -1
- package/dist/matterbridgeEdge.js +0 -1077
- package/dist/matterbridgeEdge.js.map +0 -1
- package/dist/matterbridgeTypes.d.ts +0 -172
- package/dist/matterbridgeWebsocket.d.ts +0 -49
- package/dist/matterbridgeWebsocket.d.ts.map +0 -1
- package/dist/matterbridgeWebsocket.js +0 -325
- package/dist/matterbridgeWebsocket.js.map +0 -1
- package/dist/storage/export.d.ts +0 -2
- package/dist/utils/colorUtils.d.ts +0 -61
- package/dist/utils/export.d.ts +0 -3
- package/frontend/build/static/js/main.6bbd1772.js.map +0 -1
- /package/frontend/build/static/js/{main.6bbd1772.js.LICENSE.txt → main.ea28015b.js.LICENSE.txt} +0 -0
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Frontend.
|
|
3
|
+
*
|
|
4
|
+
* @file frontend.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @date 2025-01-13
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*
|
|
9
|
+
* Copyright 2025, 2026, 2027 Luca Liguori.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License. *
|
|
22
|
+
*/
|
|
23
|
+
// @matter
|
|
24
|
+
import { EndpointServer, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat } from '@matter/main';
|
|
25
|
+
// Node modules
|
|
26
|
+
import { createServer } from 'http';
|
|
27
|
+
import https from 'https';
|
|
28
|
+
import express from 'express';
|
|
29
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
30
|
+
import os from 'os';
|
|
31
|
+
import path from 'path';
|
|
32
|
+
import { promises as fs } from 'fs';
|
|
33
|
+
// AnsiLogger module
|
|
34
|
+
import { AnsiLogger, CYAN, db, debugStringify, er, LogLevel, nf, rs, stringify, TimestampFormat, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
|
|
35
|
+
// Matterbridge
|
|
36
|
+
import { createZip, hasParameter, isValidNumber, isValidObject, isValidString } from './utils/utils.js';
|
|
37
|
+
import { plg } from './matterbridgeTypes.js';
|
|
38
|
+
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
39
|
+
/**
|
|
40
|
+
* Websocket message ID for logging.
|
|
41
|
+
* @constant {number}
|
|
42
|
+
*/
|
|
43
|
+
export const WS_ID_LOG = 0;
|
|
44
|
+
/**
|
|
45
|
+
* Websocket message ID indicating a refresh is needed.
|
|
46
|
+
* @constant {number}
|
|
47
|
+
*/
|
|
48
|
+
export const WS_ID_REFRESH_NEEDED = 1;
|
|
49
|
+
/**
|
|
50
|
+
* Websocket message ID indicating a restart is needed.
|
|
51
|
+
* @constant {number}
|
|
52
|
+
*/
|
|
53
|
+
export const WS_ID_RESTART_NEEDED = 2;
|
|
54
|
+
/**
|
|
55
|
+
* Initializes the frontend of Matterbridge.
|
|
56
|
+
*
|
|
57
|
+
* @param port The port number to run the frontend server on. Default is 8283.
|
|
58
|
+
*/
|
|
59
|
+
export class Frontend {
|
|
60
|
+
matterbridge;
|
|
61
|
+
log;
|
|
62
|
+
port = 8283;
|
|
63
|
+
initializeError = false;
|
|
64
|
+
expressApp;
|
|
65
|
+
httpServer;
|
|
66
|
+
httpsServer;
|
|
67
|
+
webSocketServer;
|
|
68
|
+
constructor(matterbridge) {
|
|
69
|
+
this.matterbridge = matterbridge;
|
|
70
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: hasParameter('debug') ? LogLevel.DEBUG : LogLevel.INFO });
|
|
71
|
+
}
|
|
72
|
+
async start(port = 8283) {
|
|
73
|
+
this.port = port;
|
|
74
|
+
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
75
|
+
// Create the express app that serves the frontend
|
|
76
|
+
this.expressApp = express();
|
|
77
|
+
// Log all requests to the server for debugging
|
|
78
|
+
/*
|
|
79
|
+
this.expressApp.use((req, res, next) => {
|
|
80
|
+
this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`);
|
|
81
|
+
next();
|
|
82
|
+
});
|
|
83
|
+
*/
|
|
84
|
+
// Serve static files from '/static' endpoint
|
|
85
|
+
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
86
|
+
if (!hasParameter('ssl')) {
|
|
87
|
+
// Create an HTTP server and attach the express app
|
|
88
|
+
this.httpServer = createServer(this.expressApp);
|
|
89
|
+
// Listen on the specified port
|
|
90
|
+
if (hasParameter('ingress')) {
|
|
91
|
+
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
92
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.httpServer.listen(this.port, () => {
|
|
97
|
+
if (this.matterbridge.systemInformation.ipv4Address !== '')
|
|
98
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
99
|
+
if (this.matterbridge.systemInformation.ipv6Address !== '')
|
|
100
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
this.httpServer.on('error', (error) => {
|
|
105
|
+
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
106
|
+
switch (error.code) {
|
|
107
|
+
case 'EACCES':
|
|
108
|
+
this.log.error(`Port ${this.port} requires elevated privileges`);
|
|
109
|
+
break;
|
|
110
|
+
case 'EADDRINUSE':
|
|
111
|
+
this.log.error(`Port ${this.port} is already in use`);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
this.initializeError = true;
|
|
115
|
+
return;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Load the SSL certificate, the private key and optionally the CA certificate
|
|
120
|
+
let cert;
|
|
121
|
+
try {
|
|
122
|
+
cert = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
123
|
+
this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let key;
|
|
130
|
+
try {
|
|
131
|
+
key = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
|
|
132
|
+
this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}`);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let ca;
|
|
139
|
+
try {
|
|
140
|
+
ca = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
|
|
141
|
+
this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')}`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
|
|
145
|
+
}
|
|
146
|
+
const serverOptions = { cert, key, ca };
|
|
147
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
148
|
+
this.httpsServer = https.createServer(serverOptions, this.expressApp);
|
|
149
|
+
// Listen on the specified port
|
|
150
|
+
if (hasParameter('ingress')) {
|
|
151
|
+
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
152
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.httpsServer.listen(this.port, () => {
|
|
157
|
+
if (this.matterbridge.systemInformation.ipv4Address !== '')
|
|
158
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
159
|
+
if (this.matterbridge.systemInformation.ipv6Address !== '')
|
|
160
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
this.httpsServer.on('error', (error) => {
|
|
165
|
+
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
166
|
+
switch (error.code) {
|
|
167
|
+
case 'EACCES':
|
|
168
|
+
this.log.error(`Port ${this.port} requires elevated privileges`);
|
|
169
|
+
break;
|
|
170
|
+
case 'EADDRINUSE':
|
|
171
|
+
this.log.error(`Port ${this.port} is already in use`);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
this.initializeError = true;
|
|
175
|
+
return;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (this.initializeError)
|
|
179
|
+
return;
|
|
180
|
+
// Createe a WebSocket server and attach it to the http or https server
|
|
181
|
+
const wssPort = this.port;
|
|
182
|
+
const wssHost = hasParameter('ssl') ? `wss://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.matterbridge.systemInformation.ipv4Address}:${wssPort}`;
|
|
183
|
+
this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
|
|
184
|
+
this.webSocketServer.on('connection', (ws, request) => {
|
|
185
|
+
const clientIp = request.socket.remoteAddress;
|
|
186
|
+
AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), LogLevel.DEBUG);
|
|
187
|
+
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
188
|
+
ws.on('message', (message) => {
|
|
189
|
+
this.wsMessageHandler(ws, message);
|
|
190
|
+
});
|
|
191
|
+
ws.on('ping', () => {
|
|
192
|
+
this.log.debug('WebSocket client ping');
|
|
193
|
+
ws.pong();
|
|
194
|
+
});
|
|
195
|
+
ws.on('pong', () => {
|
|
196
|
+
this.log.debug('WebSocket client pong');
|
|
197
|
+
});
|
|
198
|
+
ws.on('close', () => {
|
|
199
|
+
this.log.info('WebSocket client disconnected');
|
|
200
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
201
|
+
AnsiLogger.setGlobalCallback(undefined);
|
|
202
|
+
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
ws.on('error', (error) => {
|
|
206
|
+
this.log.error(`WebSocket client error: ${error}`);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
this.webSocketServer.on('close', () => {
|
|
210
|
+
this.log.debug(`WebSocketServer closed`);
|
|
211
|
+
});
|
|
212
|
+
this.webSocketServer.on('listening', () => {
|
|
213
|
+
this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
|
|
214
|
+
});
|
|
215
|
+
this.webSocketServer.on('error', (ws, error) => {
|
|
216
|
+
this.log.error(`WebSocketServer error: ${error}`);
|
|
217
|
+
});
|
|
218
|
+
// Endpoint to validate login code
|
|
219
|
+
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
220
|
+
const { password } = req.body;
|
|
221
|
+
this.log.debug('The frontend sent /api/login', password);
|
|
222
|
+
if (!this.matterbridge.nodeContext) {
|
|
223
|
+
this.log.error('/api/login nodeContext not found');
|
|
224
|
+
res.json({ valid: false });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const storedPassword = await this.matterbridge.nodeContext.get('password', '');
|
|
229
|
+
if (storedPassword === '' || password === storedPassword) {
|
|
230
|
+
this.log.debug('/api/login password valid');
|
|
231
|
+
res.json({ valid: true });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
this.log.warn('/api/login error wrong password');
|
|
235
|
+
res.json({ valid: false });
|
|
236
|
+
}
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.log.error('/api/login error getting password');
|
|
241
|
+
res.json({ valid: false });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
// Endpoint to provide health check
|
|
245
|
+
this.expressApp.get('/health', (req, res) => {
|
|
246
|
+
this.log.debug('Express received /health');
|
|
247
|
+
const healthStatus = {
|
|
248
|
+
status: 'ok', // Indicate service is healthy
|
|
249
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
250
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
251
|
+
};
|
|
252
|
+
res.status(200).json(healthStatus);
|
|
253
|
+
});
|
|
254
|
+
// Endpoint to provide settings
|
|
255
|
+
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
256
|
+
this.log.debug('The frontend sent /api/settings');
|
|
257
|
+
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
258
|
+
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
259
|
+
this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
|
|
260
|
+
this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
|
|
261
|
+
this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
|
|
262
|
+
this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
|
|
263
|
+
this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
|
|
264
|
+
this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
|
|
265
|
+
this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
|
|
266
|
+
this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
|
|
267
|
+
this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
|
|
268
|
+
this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
|
|
269
|
+
this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
|
|
270
|
+
this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
|
|
271
|
+
this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
|
|
272
|
+
this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
|
|
273
|
+
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
274
|
+
const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
275
|
+
// this.log.debug('Response:', debugStringify(response));
|
|
276
|
+
res.json(response);
|
|
277
|
+
});
|
|
278
|
+
// Endpoint to provide plugins
|
|
279
|
+
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
280
|
+
this.log.debug('The frontend sent /api/plugins');
|
|
281
|
+
const response = await this.getBaseRegisteredPlugins();
|
|
282
|
+
// this.log.debug('Response:', debugStringify(response));
|
|
283
|
+
res.json(response);
|
|
284
|
+
});
|
|
285
|
+
// Endpoint to provide devices
|
|
286
|
+
this.expressApp.get('/api/devices', (req, res) => {
|
|
287
|
+
this.log.debug('The frontend sent /api/devices');
|
|
288
|
+
const devices = [];
|
|
289
|
+
this.matterbridge.devices.forEach(async (device) => {
|
|
290
|
+
// Check if the device has the required properties
|
|
291
|
+
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
|
|
292
|
+
return;
|
|
293
|
+
const cluster = this.getClusterTextFromDevice(device);
|
|
294
|
+
devices.push({
|
|
295
|
+
pluginName: device.plugin,
|
|
296
|
+
type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
|
|
297
|
+
endpoint: device.number,
|
|
298
|
+
name: device.deviceName,
|
|
299
|
+
serial: device.serialNumber,
|
|
300
|
+
productUrl: device.productUrl,
|
|
301
|
+
configUrl: device.configUrl,
|
|
302
|
+
uniqueId: device.uniqueId,
|
|
303
|
+
cluster: cluster,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
// this.log.debug('Response:', debugStringify(data));
|
|
307
|
+
res.json(devices);
|
|
308
|
+
});
|
|
309
|
+
// Endpoint to provide the cluster servers of the devices
|
|
310
|
+
this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
|
|
311
|
+
const selectedPluginName = req.params.selectedPluginName;
|
|
312
|
+
const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
|
|
313
|
+
this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
|
|
314
|
+
if (selectedPluginName === 'none') {
|
|
315
|
+
res.json([]);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const data = [];
|
|
319
|
+
this.matterbridge.devices.forEach(async (device) => {
|
|
320
|
+
const pluginName = device.plugin;
|
|
321
|
+
if (pluginName === selectedPluginName && device.number === selectedDeviceEndpoint) {
|
|
322
|
+
const endpointServer = EndpointServer.forEndpoint(device);
|
|
323
|
+
const clusterServers = endpointServer.getAllClusterServers();
|
|
324
|
+
clusterServers.forEach((clusterServer) => {
|
|
325
|
+
Object.entries(clusterServer.attributes).forEach(([key, value]) => {
|
|
326
|
+
if (clusterServer.name === 'EveHistory')
|
|
327
|
+
return;
|
|
328
|
+
let attributeValue;
|
|
329
|
+
try {
|
|
330
|
+
if (typeof value.getLocal() === 'object')
|
|
331
|
+
attributeValue = stringify(value.getLocal());
|
|
332
|
+
else
|
|
333
|
+
attributeValue = value.getLocal().toString();
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
attributeValue = 'Fabric-Scoped';
|
|
337
|
+
this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
|
|
338
|
+
}
|
|
339
|
+
data.push({
|
|
340
|
+
endpoint: device.number ? device.number.toString() : '...',
|
|
341
|
+
clusterName: clusterServer.name,
|
|
342
|
+
clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
|
|
343
|
+
attributeName: key,
|
|
344
|
+
attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
|
|
345
|
+
attributeValue,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
350
|
+
const name = childEndpoint.name;
|
|
351
|
+
const clusterServers = childEndpoint.getAllClusterServers();
|
|
352
|
+
clusterServers.forEach((clusterServer) => {
|
|
353
|
+
Object.entries(clusterServer.attributes).forEach(([key, value]) => {
|
|
354
|
+
if (clusterServer.name === 'EveHistory')
|
|
355
|
+
return;
|
|
356
|
+
let attributeValue;
|
|
357
|
+
try {
|
|
358
|
+
if (typeof value.getLocal() === 'object')
|
|
359
|
+
attributeValue = stringify(value.getLocal());
|
|
360
|
+
else
|
|
361
|
+
attributeValue = value.getLocal().toString();
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
attributeValue = 'Fabric-Scoped';
|
|
365
|
+
this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
|
|
366
|
+
}
|
|
367
|
+
data.push({
|
|
368
|
+
endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''),
|
|
369
|
+
clusterName: clusterServer.name,
|
|
370
|
+
clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
|
|
371
|
+
attributeName: key,
|
|
372
|
+
attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
|
|
373
|
+
attributeValue,
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
res.json(data);
|
|
381
|
+
});
|
|
382
|
+
// Endpoint to view the log
|
|
383
|
+
this.expressApp.get('/api/view-log', async (req, res) => {
|
|
384
|
+
this.log.debug('The frontend sent /api/log');
|
|
385
|
+
try {
|
|
386
|
+
const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'utf8');
|
|
387
|
+
res.type('text/plain');
|
|
388
|
+
res.send(data);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
this.log.error(`Error reading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
392
|
+
res.status(500).send('Error reading log file');
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
// Endpoint to download the matterbridge log
|
|
396
|
+
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
397
|
+
this.log.debug('The frontend sent /api/download-mblog');
|
|
398
|
+
try {
|
|
399
|
+
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), fs.constants.F_OK);
|
|
400
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
|
|
404
|
+
}
|
|
405
|
+
res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), 'matterbridge.log', (error) => {
|
|
406
|
+
if (error) {
|
|
407
|
+
this.log.error(`Error downloading log file ${this.matterbridge.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
408
|
+
res.status(500).send('Error downloading the matterbridge log file');
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
// Endpoint to download the matter log
|
|
413
|
+
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
414
|
+
this.log.debug('The frontend sent /api/download-mjlog');
|
|
415
|
+
try {
|
|
416
|
+
await fs.access(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), fs.constants.F_OK);
|
|
417
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
fs.appendFile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
|
|
421
|
+
}
|
|
422
|
+
res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), 'matter.log', (error) => {
|
|
423
|
+
if (error) {
|
|
424
|
+
this.log.error(`Error downloading log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
|
|
425
|
+
res.status(500).send('Error downloading the matter log file');
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
// Endpoint to download the matter storage file
|
|
430
|
+
this.expressApp.get('/api/download-mjstorage', (req, res) => {
|
|
431
|
+
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
432
|
+
res.download(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterStorageName), 'matterbridge.json', (error) => {
|
|
433
|
+
if (error) {
|
|
434
|
+
this.log.error(`Error downloading log file ${this.matterbridge.matterStorageName}: ${error instanceof Error ? error.message : error}`);
|
|
435
|
+
res.status(500).send('Error downloading the matter storage file');
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
// Endpoint to download the matterbridge storage directory
|
|
440
|
+
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
441
|
+
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
442
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.nodeStorageName));
|
|
443
|
+
res.download(path.join(os.tmpdir(), `matterbridge.${this.matterbridge.nodeStorageName}.zip`), `matterbridge.${this.matterbridge.nodeStorageName}.zip`, (error) => {
|
|
444
|
+
if (error) {
|
|
445
|
+
this.log.error(`Error downloading file ${`matterbridge.${this.matterbridge.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
|
|
446
|
+
res.status(500).send('Error downloading the matterbridge storage file');
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
// Endpoint to download the matterbridge plugin directory
|
|
451
|
+
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
452
|
+
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
453
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
454
|
+
res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
|
|
455
|
+
if (error) {
|
|
456
|
+
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
457
|
+
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
// Endpoint to download the matterbridge plugin config files
|
|
462
|
+
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
463
|
+
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
464
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
465
|
+
// await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
466
|
+
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
467
|
+
if (error) {
|
|
468
|
+
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
469
|
+
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
// Endpoint to download the matterbridge plugin config files
|
|
474
|
+
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
475
|
+
this.log.debug('The frontend sent /api/download-backup');
|
|
476
|
+
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
477
|
+
if (error) {
|
|
478
|
+
this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
479
|
+
res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
// Endpoint to receive commands
|
|
484
|
+
this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
|
|
485
|
+
const command = req.params.command;
|
|
486
|
+
let param = req.params.param;
|
|
487
|
+
this.log.debug(`The frontend sent /api/command/${command}/${param}`);
|
|
488
|
+
if (!command) {
|
|
489
|
+
res.status(400).json({ error: 'No command provided' });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this.log.debug(`Received frontend command: ${command}:${param}`);
|
|
493
|
+
// Handle the command setpassword from Settings
|
|
494
|
+
if (command === 'setpassword') {
|
|
495
|
+
const password = param.slice(1, -1); // Remove the first and last characters
|
|
496
|
+
this.log.debug('setpassword', param, password);
|
|
497
|
+
await this.matterbridge.nodeContext?.set('password', password);
|
|
498
|
+
res.json({ message: 'Command received' });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Handle the command setbridgemode from Settings
|
|
502
|
+
if (command === 'setbridgemode') {
|
|
503
|
+
this.log.debug(`setbridgemode: ${param}`);
|
|
504
|
+
this.wssSendRestartRequired();
|
|
505
|
+
await this.matterbridge.nodeContext?.set('bridgeMode', param);
|
|
506
|
+
res.json({ message: 'Command received' });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
// Handle the command backup from Settings
|
|
510
|
+
if (command === 'backup') {
|
|
511
|
+
this.log.notice(`Prepairing the backup...`);
|
|
512
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory));
|
|
513
|
+
this.log.notice(`Backup ready to be downloaded.`);
|
|
514
|
+
res.json({ message: 'Command received' });
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Handle the command setmbloglevel from Settings
|
|
518
|
+
if (command === 'setmbloglevel') {
|
|
519
|
+
this.log.debug('Matterbridge log level:', param);
|
|
520
|
+
if (param === 'Debug') {
|
|
521
|
+
this.log.logLevel = LogLevel.DEBUG;
|
|
522
|
+
}
|
|
523
|
+
else if (param === 'Info') {
|
|
524
|
+
this.log.logLevel = LogLevel.INFO;
|
|
525
|
+
}
|
|
526
|
+
else if (param === 'Notice') {
|
|
527
|
+
this.log.logLevel = LogLevel.NOTICE;
|
|
528
|
+
}
|
|
529
|
+
else if (param === 'Warn') {
|
|
530
|
+
this.log.logLevel = LogLevel.WARN;
|
|
531
|
+
}
|
|
532
|
+
else if (param === 'Error') {
|
|
533
|
+
this.log.logLevel = LogLevel.ERROR;
|
|
534
|
+
}
|
|
535
|
+
else if (param === 'Fatal') {
|
|
536
|
+
this.log.logLevel = LogLevel.FATAL;
|
|
537
|
+
}
|
|
538
|
+
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
539
|
+
MatterbridgeEndpoint.logLevel = this.log.logLevel;
|
|
540
|
+
this.matterbridge.plugins.logLevel = this.log.logLevel;
|
|
541
|
+
for (const plugin of this.matterbridge.plugins) {
|
|
542
|
+
if (!plugin.platform || !plugin.platform.config)
|
|
543
|
+
continue;
|
|
544
|
+
plugin.platform.log.logLevel = plugin.platform.config.debug ? LogLevel.DEBUG : this.log.logLevel;
|
|
545
|
+
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? LogLevel.DEBUG : this.log.logLevel);
|
|
546
|
+
}
|
|
547
|
+
res.json({ message: 'Command received' });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Handle the command setmbloglevel from Settings
|
|
551
|
+
if (command === 'setmjloglevel') {
|
|
552
|
+
this.log.debug('Matter.js log level:', param);
|
|
553
|
+
if (param === 'Debug') {
|
|
554
|
+
Logger.defaultLogLevel = MatterLogLevel.DEBUG;
|
|
555
|
+
}
|
|
556
|
+
else if (param === 'Info') {
|
|
557
|
+
Logger.defaultLogLevel = MatterLogLevel.INFO;
|
|
558
|
+
}
|
|
559
|
+
else if (param === 'Notice') {
|
|
560
|
+
Logger.defaultLogLevel = MatterLogLevel.NOTICE;
|
|
561
|
+
}
|
|
562
|
+
else if (param === 'Warn') {
|
|
563
|
+
Logger.defaultLogLevel = MatterLogLevel.WARN;
|
|
564
|
+
}
|
|
565
|
+
else if (param === 'Error') {
|
|
566
|
+
Logger.defaultLogLevel = MatterLogLevel.ERROR;
|
|
567
|
+
}
|
|
568
|
+
else if (param === 'Fatal') {
|
|
569
|
+
Logger.defaultLogLevel = MatterLogLevel.FATAL;
|
|
570
|
+
}
|
|
571
|
+
await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel);
|
|
572
|
+
res.json({ message: 'Command received' });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// Handle the command setmdnsinterface from Settings
|
|
576
|
+
if (command === 'setmdnsinterface') {
|
|
577
|
+
param = param.slice(1, -1); // Remove the first and last characters *mdns*
|
|
578
|
+
this.matterbridge.matterbridgeInformation.mattermdnsinterface = param;
|
|
579
|
+
this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
|
|
580
|
+
await this.matterbridge.nodeContext?.set('mattermdnsinterface', param);
|
|
581
|
+
res.json({ message: 'Command received' });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// Handle the command setipv4address from Settings
|
|
585
|
+
if (command === 'setipv4address') {
|
|
586
|
+
param = param.slice(1, -1); // Remove the first and last characters *ip*
|
|
587
|
+
this.matterbridge.matterbridgeInformation.matteripv4address = param;
|
|
588
|
+
this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
|
|
589
|
+
await this.matterbridge.nodeContext?.set('matteripv4address', param);
|
|
590
|
+
res.json({ message: 'Command received' });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// Handle the command setipv6address from Settings
|
|
594
|
+
if (command === 'setipv6address') {
|
|
595
|
+
param = param.slice(1, -1); // Remove the first and last characters *ip*
|
|
596
|
+
this.matterbridge.matterbridgeInformation.matteripv6address = param;
|
|
597
|
+
this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
|
|
598
|
+
await this.matterbridge.nodeContext?.set('matteripv6address', param);
|
|
599
|
+
res.json({ message: 'Command received' });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// Handle the command setmatterport from Settings
|
|
603
|
+
if (command === 'setmatterport') {
|
|
604
|
+
const port = Math.min(Math.max(parseInt(param), 5540), 5560);
|
|
605
|
+
this.matterbridge.matterbridgeInformation.matterPort = port;
|
|
606
|
+
this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
|
|
607
|
+
await this.matterbridge.nodeContext?.set('matterport', port);
|
|
608
|
+
res.json({ message: 'Command received' });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// Handle the command setmatterdiscriminator from Settings
|
|
612
|
+
if (command === 'setmatterdiscriminator') {
|
|
613
|
+
const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
|
|
614
|
+
this.matterbridge.matterbridgeInformation.matterDiscriminator = discriminator;
|
|
615
|
+
this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
|
|
616
|
+
await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator);
|
|
617
|
+
res.json({ message: 'Command received' });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Handle the command setmatterpasscode from Settings
|
|
621
|
+
if (command === 'setmatterpasscode') {
|
|
622
|
+
const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
|
|
623
|
+
this.matterbridge.matterbridgeInformation.matterPasscode = passcode;
|
|
624
|
+
this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
|
|
625
|
+
await this.matterbridge.nodeContext?.set('matterpasscode', passcode);
|
|
626
|
+
res.json({ message: 'Command received' });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
// Handle the command setmbloglevel from Settings
|
|
630
|
+
if (command === 'setmblogfile') {
|
|
631
|
+
this.log.debug('Matterbridge file log:', param);
|
|
632
|
+
this.matterbridge.matterbridgeInformation.fileLogger = param === 'true';
|
|
633
|
+
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', param === 'true');
|
|
634
|
+
// Create the file logger for matterbridge
|
|
635
|
+
if (param === 'true')
|
|
636
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterbrideLoggerFile), LogLevel.DEBUG, true);
|
|
637
|
+
else
|
|
638
|
+
AnsiLogger.setGlobalLogfile(undefined);
|
|
639
|
+
res.json({ message: 'Command received' });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Handle the command setmbloglevel from Settings
|
|
643
|
+
if (command === 'setmjlogfile') {
|
|
644
|
+
this.log.debug('Matter file log:', param);
|
|
645
|
+
this.matterbridge.matterbridgeInformation.matterFileLogger = param === 'true';
|
|
646
|
+
await this.matterbridge.nodeContext?.set('matterFileLog', param === 'true');
|
|
647
|
+
if (param === 'true') {
|
|
648
|
+
try {
|
|
649
|
+
Logger.addLogger('matterfilelogger', await this.matterbridge.createMatterFileLogger(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), true), {
|
|
650
|
+
defaultLogLevel: MatterLogLevel.DEBUG,
|
|
651
|
+
logFormat: MatterLogFormat.PLAIN,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
try {
|
|
660
|
+
Logger.removeLogger('matterfilelogger');
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
res.json({ message: 'Command received' });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
// Handle the command unregister from Settings
|
|
670
|
+
if (command === 'unregister') {
|
|
671
|
+
await this.matterbridge.unregisterAndShutdownProcess();
|
|
672
|
+
res.json({ message: 'Command received' });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// Handle the command reset from Settings
|
|
676
|
+
if (command === 'reset') {
|
|
677
|
+
await this.matterbridge.shutdownProcessAndReset();
|
|
678
|
+
res.json({ message: 'Command received' });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// Handle the command factoryreset from Settings
|
|
682
|
+
if (command === 'factoryreset') {
|
|
683
|
+
await this.matterbridge.shutdownProcessAndFactoryReset();
|
|
684
|
+
res.json({ message: 'Command received' });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
// Handle the command shutdown from Header
|
|
688
|
+
if (command === 'shutdown') {
|
|
689
|
+
await this.matterbridge.shutdownProcess();
|
|
690
|
+
res.json({ message: 'Command received' });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
// Handle the command restart from Header
|
|
694
|
+
if (command === 'restart') {
|
|
695
|
+
await this.matterbridge.restartProcess();
|
|
696
|
+
res.json({ message: 'Command received' });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
// Handle the command update from Header
|
|
700
|
+
if (command === 'update') {
|
|
701
|
+
this.log.info('Updating matterbridge...');
|
|
702
|
+
try {
|
|
703
|
+
await this.matterbridge.spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
|
|
704
|
+
this.log.info('Matterbridge has been updated. Full restart required.');
|
|
705
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
this.log.error('Error updating matterbridge');
|
|
709
|
+
}
|
|
710
|
+
await this.matterbridge.updateProcess();
|
|
711
|
+
this.wssSendRestartRequired();
|
|
712
|
+
res.json({ message: 'Command received' });
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
// Handle the command saveconfig from Home
|
|
716
|
+
if (command === 'saveconfig') {
|
|
717
|
+
param = param.replace(/\*/g, '\\');
|
|
718
|
+
this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
|
|
719
|
+
// console.log('Req.body:', JSON.stringify(req.body, null, 2));
|
|
720
|
+
if (!this.matterbridge.plugins.has(param)) {
|
|
721
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
const plugin = this.matterbridge.plugins.get(param);
|
|
725
|
+
if (!plugin)
|
|
726
|
+
return;
|
|
727
|
+
this.matterbridge.plugins.saveConfigFromJson(plugin, req.body);
|
|
728
|
+
}
|
|
729
|
+
this.wssSendRestartRequired();
|
|
730
|
+
res.json({ message: 'Command received' });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Handle the command installplugin from Home
|
|
734
|
+
if (command === 'installplugin') {
|
|
735
|
+
param = param.replace(/\*/g, '\\');
|
|
736
|
+
this.log.info(`Installing plugin ${plg}${param}${nf}...`);
|
|
737
|
+
try {
|
|
738
|
+
await this.matterbridge.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
|
|
739
|
+
this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
|
|
740
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
this.log.error(`Error installing plugin ${plg}${param}${er}`);
|
|
744
|
+
}
|
|
745
|
+
this.wssSendRestartRequired();
|
|
746
|
+
param = param.split('@')[0];
|
|
747
|
+
// Also add the plugin to matterbridge so no return!
|
|
748
|
+
if (param === 'matterbridge') {
|
|
749
|
+
// If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
|
|
750
|
+
res.json({ message: 'Command received' });
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Handle the command addplugin from Home
|
|
755
|
+
if (command === 'addplugin' || command === 'installplugin') {
|
|
756
|
+
param = param.replace(/\*/g, '\\');
|
|
757
|
+
const plugin = await this.matterbridge.plugins.add(param);
|
|
758
|
+
if (plugin) {
|
|
759
|
+
if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
760
|
+
// We don't know now if the plugin is a dynamic platform or an accessory platform so we create the server node and the aggregator node
|
|
761
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
762
|
+
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
763
|
+
}
|
|
764
|
+
this.matterbridge.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
|
|
765
|
+
}
|
|
766
|
+
res.json({ message: 'Command received' });
|
|
767
|
+
this.wssSendRefreshRequired();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// Handle the command removeplugin from Home
|
|
771
|
+
if (command === 'removeplugin') {
|
|
772
|
+
if (!this.matterbridge.plugins.has(param)) {
|
|
773
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
const plugin = this.matterbridge.plugins.get(param);
|
|
777
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true); // This will also close the server node in childbridge mode
|
|
778
|
+
await this.matterbridge.plugins.remove(param);
|
|
779
|
+
}
|
|
780
|
+
res.json({ message: 'Command received' });
|
|
781
|
+
this.wssSendRefreshRequired();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
// Handle the command enableplugin from Home
|
|
785
|
+
if (command === 'enableplugin') {
|
|
786
|
+
if (!this.matterbridge.plugins.has(param)) {
|
|
787
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
const plugin = this.matterbridge.plugins.get(param);
|
|
791
|
+
if (plugin && !plugin.enabled) {
|
|
792
|
+
plugin.locked = undefined;
|
|
793
|
+
plugin.error = undefined;
|
|
794
|
+
plugin.loaded = undefined;
|
|
795
|
+
plugin.started = undefined;
|
|
796
|
+
plugin.configured = undefined;
|
|
797
|
+
plugin.connected = undefined;
|
|
798
|
+
plugin.platform = undefined;
|
|
799
|
+
plugin.registeredDevices = undefined;
|
|
800
|
+
plugin.addedDevices = undefined;
|
|
801
|
+
await this.matterbridge.plugins.enable(param);
|
|
802
|
+
if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
|
|
803
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
804
|
+
this.matterbridge.createDynamicPlugin(plugin, true);
|
|
805
|
+
}
|
|
806
|
+
this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background since the server node is already started
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
res.json({ message: 'Command received' });
|
|
810
|
+
this.wssSendRefreshRequired();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
// Handle the command disableplugin from Home
|
|
814
|
+
if (command === 'disableplugin') {
|
|
815
|
+
if (!this.matterbridge.plugins.has(param)) {
|
|
816
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
const plugin = this.matterbridge.plugins.get(param);
|
|
820
|
+
if (plugin && plugin.enabled) {
|
|
821
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true); // This will also close the server node in childbridge mode
|
|
822
|
+
await this.matterbridge.plugins.disable(param);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
res.json({ message: 'Command received' });
|
|
826
|
+
this.wssSendRefreshRequired();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
// Fallback for routing (must be the last route)
|
|
831
|
+
this.expressApp.get('*', (req, res) => {
|
|
832
|
+
this.log.debug('The frontend sent:', req.url);
|
|
833
|
+
this.log.debug('Response send file:', path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
834
|
+
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
835
|
+
});
|
|
836
|
+
this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
|
|
837
|
+
}
|
|
838
|
+
async stop() {
|
|
839
|
+
// Close the http server
|
|
840
|
+
if (this.httpServer) {
|
|
841
|
+
this.httpServer.close();
|
|
842
|
+
this.httpServer.removeAllListeners();
|
|
843
|
+
this.httpServer = undefined;
|
|
844
|
+
this.log.debug('Frontend http server closed successfully');
|
|
845
|
+
}
|
|
846
|
+
// Close the https server
|
|
847
|
+
if (this.httpsServer) {
|
|
848
|
+
this.httpsServer.close();
|
|
849
|
+
this.httpsServer.removeAllListeners();
|
|
850
|
+
this.httpsServer = undefined;
|
|
851
|
+
this.log.debug('Frontend https server closed successfully');
|
|
852
|
+
}
|
|
853
|
+
// Remove listeners from the express app
|
|
854
|
+
if (this.expressApp) {
|
|
855
|
+
this.expressApp.removeAllListeners();
|
|
856
|
+
this.expressApp = undefined;
|
|
857
|
+
this.log.debug('Frontend app closed successfully');
|
|
858
|
+
}
|
|
859
|
+
// Close the WebSocket server
|
|
860
|
+
if (this.webSocketServer) {
|
|
861
|
+
// Close all active connections
|
|
862
|
+
this.webSocketServer.clients.forEach((client) => {
|
|
863
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
864
|
+
client.close();
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
this.webSocketServer.close((error) => {
|
|
868
|
+
if (error) {
|
|
869
|
+
this.log.error(`Error closing WebSocket server: ${error}`);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
this.log.debug('WebSocket server closed successfully');
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
this.webSocketServer = undefined;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Retrieves the cluster text description from a given device.
|
|
880
|
+
* @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
|
|
881
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
882
|
+
*/
|
|
883
|
+
getClusterTextFromDevice(device) {
|
|
884
|
+
const getAttribute = (device, cluster, attribute) => {
|
|
885
|
+
let value = undefined;
|
|
886
|
+
Object.entries(device.state)
|
|
887
|
+
.filter(([clusterName]) => clusterName.toLowerCase() === cluster.toLowerCase())
|
|
888
|
+
.forEach(([, clusterAttributes]) => {
|
|
889
|
+
Object.entries(clusterAttributes)
|
|
890
|
+
.filter(([attributeName]) => attributeName.toLowerCase() === attribute.toLowerCase())
|
|
891
|
+
.forEach(([, attributeValue]) => {
|
|
892
|
+
value = attributeValue;
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
if (value === undefined)
|
|
896
|
+
this.log.error(`Cluster ${cluster} or attribute ${attribute} not found in device ${device.deviceName}`);
|
|
897
|
+
return value;
|
|
898
|
+
};
|
|
899
|
+
const getUserLabel = (device) => {
|
|
900
|
+
const labelList = getAttribute(device, 'userLabel', 'labelList');
|
|
901
|
+
if (!labelList)
|
|
902
|
+
return;
|
|
903
|
+
const composed = labelList.find((entry) => entry.label === 'composed');
|
|
904
|
+
if (composed)
|
|
905
|
+
return 'Composed: ' + composed.value;
|
|
906
|
+
else
|
|
907
|
+
return '';
|
|
908
|
+
};
|
|
909
|
+
const getFixedLabel = (device) => {
|
|
910
|
+
const labelList = getAttribute(device, 'fixedLabel', 'labelList');
|
|
911
|
+
if (!labelList)
|
|
912
|
+
return;
|
|
913
|
+
const composed = labelList.find((entry) => entry.label === 'composed');
|
|
914
|
+
if (composed)
|
|
915
|
+
return 'Composed: ' + composed.value;
|
|
916
|
+
else
|
|
917
|
+
return '';
|
|
918
|
+
};
|
|
919
|
+
let attributes = '';
|
|
920
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
921
|
+
Object.entries(device.state).forEach(([clusterName, clusterAttributes]) => {
|
|
922
|
+
Object.entries(clusterAttributes).forEach(([attributeName, attributeValue]) => {
|
|
923
|
+
// console.log(`Cluster: ${clusterName} Attribute: ${attributeName} Value: ${attributeValue}`);
|
|
924
|
+
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
925
|
+
attributes += `OnOff: ${attributeValue} `;
|
|
926
|
+
if (clusterName === 'switch' && attributeName === 'currentPosition')
|
|
927
|
+
attributes += `Position: ${attributeValue} `;
|
|
928
|
+
if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths')
|
|
929
|
+
attributes += `Cover position: ${attributeValue / 100}% `;
|
|
930
|
+
if (clusterName === 'doorLock' && attributeName === 'lockState')
|
|
931
|
+
attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
|
|
932
|
+
if (clusterName === 'thermostat' && attributeName === 'localTemperature')
|
|
933
|
+
attributes += `Temperature: ${attributeValue / 100}°C `;
|
|
934
|
+
if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint')
|
|
935
|
+
attributes += `Heat to: ${attributeValue / 100}°C `;
|
|
936
|
+
if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint')
|
|
937
|
+
attributes += `Cool to: ${attributeValue / 100}°C `;
|
|
938
|
+
if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
|
|
939
|
+
attributes += `Mode: ${attributeValue} `;
|
|
940
|
+
if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
|
|
941
|
+
attributes += `State: ${attributeValue} `;
|
|
942
|
+
if (clusterName === 'levelControl' && attributeName === 'currentLevel')
|
|
943
|
+
attributes += `Level: ${attributeValue}% `;
|
|
944
|
+
if (clusterName === 'colorControl' && attributeName === 'colorMode')
|
|
945
|
+
attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
|
|
946
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue')
|
|
947
|
+
attributes += `Hue: ${Math.round(attributeValue)} `;
|
|
948
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation')
|
|
949
|
+
attributes += `Saturation: ${Math.round(attributeValue)} `;
|
|
950
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX')
|
|
951
|
+
attributes += `X: ${Math.round(attributeValue)} `;
|
|
952
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY')
|
|
953
|
+
attributes += `Y: ${Math.round(attributeValue)} `;
|
|
954
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds')
|
|
955
|
+
attributes += `ColorTemp: ${Math.round(attributeValue)} `;
|
|
956
|
+
if (clusterName === 'booleanState' && attributeName === 'stateValue')
|
|
957
|
+
attributes += `Contact: ${attributeValue} `;
|
|
958
|
+
if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive')
|
|
959
|
+
attributes += `Active alarms: ${stringify(attributeValue)} `;
|
|
960
|
+
if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
|
|
961
|
+
attributes += `Smoke: ${attributeValue} `;
|
|
962
|
+
if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
|
|
963
|
+
attributes += `Co: ${attributeValue} `;
|
|
964
|
+
if (clusterName === 'fanControl' && attributeName === 'fanMode')
|
|
965
|
+
attributes += `Mode: ${attributeValue} `;
|
|
966
|
+
if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
|
|
967
|
+
attributes += `Percent: ${attributeValue} `;
|
|
968
|
+
if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
|
|
969
|
+
attributes += `Speed: ${attributeValue} `;
|
|
970
|
+
if (clusterName === 'occupancySensing' && attributeName === 'occupancy')
|
|
971
|
+
attributes += `Occupancy: ${attributeValue.occupied} `;
|
|
972
|
+
if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue')
|
|
973
|
+
attributes += `Illuminance: ${attributeValue} `;
|
|
974
|
+
if (clusterName === 'airQuality' && attributeName === 'airQuality')
|
|
975
|
+
attributes += `Air quality: ${attributeValue} `;
|
|
976
|
+
if (clusterName === 'tvocMeasurement' && attributeName === 'measuredValue')
|
|
977
|
+
attributes += `Voc: ${attributeValue} `;
|
|
978
|
+
if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue')
|
|
979
|
+
attributes += `Temperature: ${attributeValue / 100}°C `;
|
|
980
|
+
if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue')
|
|
981
|
+
attributes += `Humidity: ${attributeValue / 100}% `;
|
|
982
|
+
if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
|
|
983
|
+
attributes += `Pressure: ${attributeValue} `;
|
|
984
|
+
if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
|
|
985
|
+
attributes += `Flow: ${attributeValue} `;
|
|
986
|
+
if (clusterName === 'fixedLabel' && attributeName === 'labelList')
|
|
987
|
+
attributes += `${getFixedLabel(device)} `;
|
|
988
|
+
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
989
|
+
attributes += `${getUserLabel(device)} `;
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
return attributes.trimStart().trimEnd();
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
996
|
+
* @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
|
|
997
|
+
*/
|
|
998
|
+
async getBaseRegisteredPlugins() {
|
|
999
|
+
const baseRegisteredPlugins = [];
|
|
1000
|
+
for (const plugin of this.matterbridge.plugins) {
|
|
1001
|
+
baseRegisteredPlugins.push({
|
|
1002
|
+
path: plugin.path,
|
|
1003
|
+
type: plugin.type,
|
|
1004
|
+
name: plugin.name,
|
|
1005
|
+
version: plugin.version,
|
|
1006
|
+
description: plugin.description,
|
|
1007
|
+
author: plugin.author,
|
|
1008
|
+
latestVersion: plugin.latestVersion,
|
|
1009
|
+
locked: plugin.locked,
|
|
1010
|
+
error: plugin.error,
|
|
1011
|
+
enabled: plugin.enabled,
|
|
1012
|
+
loaded: plugin.loaded,
|
|
1013
|
+
started: plugin.started,
|
|
1014
|
+
configured: plugin.configured,
|
|
1015
|
+
paired: plugin.paired,
|
|
1016
|
+
connected: plugin.connected,
|
|
1017
|
+
fabricInformations: plugin.fabricInformations,
|
|
1018
|
+
sessionInformations: plugin.sessionInformations,
|
|
1019
|
+
registeredDevices: plugin.registeredDevices,
|
|
1020
|
+
addedDevices: plugin.addedDevices,
|
|
1021
|
+
qrPairingCode: plugin.qrPairingCode,
|
|
1022
|
+
manualPairingCode: plugin.manualPairingCode,
|
|
1023
|
+
configJson: plugin.configJson,
|
|
1024
|
+
schemaJson: plugin.schemaJson,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return baseRegisteredPlugins;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Handles incoming websocket messages for the Matterbridge.
|
|
1031
|
+
*
|
|
1032
|
+
* @param {Matterbridge} this - The Matterbridge instance.
|
|
1033
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1034
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1035
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1036
|
+
*/
|
|
1037
|
+
async wsMessageHandler(client, message) {
|
|
1038
|
+
let data;
|
|
1039
|
+
try {
|
|
1040
|
+
data = JSON.parse(message.toString());
|
|
1041
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || !isValidObject(data.params) || data.dst !== 'Matterbridge') {
|
|
1042
|
+
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
1043
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' }));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
|
|
1047
|
+
if (data.method === 'ping') {
|
|
1048
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: 'pong' }));
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
else if (data.method === '/api/login') {
|
|
1052
|
+
if (!this.matterbridge.nodeContext) {
|
|
1053
|
+
this.log.error('Login nodeContext not found');
|
|
1054
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: nodeContext not found' }));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const storedPassword = await this.matterbridge.nodeContext.get('password', '');
|
|
1058
|
+
if (storedPassword === '' || storedPassword === data.params.password) {
|
|
1059
|
+
this.log.debug('Login password valid');
|
|
1060
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: { valid: true } }));
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
this.log.debug('Error wrong password');
|
|
1065
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' }));
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
else if (data.method === '/api/install') {
|
|
1070
|
+
if (!isValidString(data.params.packageName, 10)) {
|
|
1071
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/install' }));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.matterbridge
|
|
1075
|
+
.spawnCommand('npm', ['install', '-g', data.params.packageName, '--omit=dev', '--verbose'])
|
|
1076
|
+
.then((response) => {
|
|
1077
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
1078
|
+
})
|
|
1079
|
+
.catch((error) => {
|
|
1080
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
|
|
1081
|
+
});
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
else if (data.method === '/api/uninstall') {
|
|
1085
|
+
if (!isValidString(data.params.packageName, 10)) {
|
|
1086
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' }));
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.matterbridge
|
|
1090
|
+
.spawnCommand('npm', ['uninstall', '-g', data.params.packageName, '--verbose'])
|
|
1091
|
+
.then((response) => {
|
|
1092
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
1093
|
+
})
|
|
1094
|
+
.catch((error) => {
|
|
1095
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: error instanceof Error ? error.message : error }));
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
else if (data.method === '/api/restart') {
|
|
1100
|
+
await this.matterbridge.restartProcess();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
else if (data.method === '/api/shutdown') {
|
|
1104
|
+
await this.matterbridge.shutdownProcess();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
else if (data.method === '/api/settings') {
|
|
1108
|
+
this.matterbridge.matterbridgeInformation.bridgeMode = this.matterbridge.bridgeMode;
|
|
1109
|
+
this.matterbridge.matterbridgeInformation.restartMode = this.matterbridge.restartMode;
|
|
1110
|
+
this.matterbridge.matterbridgeInformation.loggerLevel = this.log.logLevel;
|
|
1111
|
+
this.matterbridge.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
|
|
1112
|
+
this.matterbridge.matterbridgeInformation.mattermdnsinterface = (await this.matterbridge.nodeContext?.get('mattermdnsinterface', '')) || '';
|
|
1113
|
+
this.matterbridge.matterbridgeInformation.matteripv4address = (await this.matterbridge.nodeContext?.get('matteripv4address', '')) || '';
|
|
1114
|
+
this.matterbridge.matterbridgeInformation.matteripv6address = (await this.matterbridge.nodeContext?.get('matteripv6address', '')) || '';
|
|
1115
|
+
this.matterbridge.matterbridgeInformation.matterPort = (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540;
|
|
1116
|
+
this.matterbridge.matterbridgeInformation.matterDiscriminator = await this.matterbridge.nodeContext?.get('matterdiscriminator');
|
|
1117
|
+
this.matterbridge.matterbridgeInformation.matterPasscode = await this.matterbridge.nodeContext?.get('matterpasscode');
|
|
1118
|
+
this.matterbridge.matterbridgeInformation.matterbridgePaired = this.matterbridge.matterbridgePaired;
|
|
1119
|
+
this.matterbridge.matterbridgeInformation.matterbridgeConnected = this.matterbridge.matterbridgeConnected;
|
|
1120
|
+
this.matterbridge.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridge.matterbridgeQrPairingCode;
|
|
1121
|
+
this.matterbridge.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridge.matterbridgeManualPairingCode;
|
|
1122
|
+
this.matterbridge.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridge.matterbridgeFabricInformations;
|
|
1123
|
+
this.matterbridge.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridge.matterbridgeSessionInformations.values());
|
|
1124
|
+
this.matterbridge.matterbridgeInformation.profile = this.matterbridge.profile;
|
|
1125
|
+
const response = { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: this.matterbridge.matterbridgeInformation };
|
|
1126
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
else if (data.method === '/api/plugins') {
|
|
1130
|
+
const response = await this.getBaseRegisteredPlugins();
|
|
1131
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response }));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
else if (data.method === '/api/devices') {
|
|
1135
|
+
const devices = [];
|
|
1136
|
+
this.matterbridge.devices.forEach(async (device) => {
|
|
1137
|
+
// Filter by pluginName if provided
|
|
1138
|
+
if (data.params.pluginName && data.params.pluginName !== device.plugin)
|
|
1139
|
+
return;
|
|
1140
|
+
// Check if the device has the required properties
|
|
1141
|
+
if (!device.plugin || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId)
|
|
1142
|
+
return;
|
|
1143
|
+
const cluster = this.getClusterTextFromDevice(device);
|
|
1144
|
+
devices.push({
|
|
1145
|
+
pluginName: device.plugin,
|
|
1146
|
+
type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
|
|
1147
|
+
endpoint: device.number,
|
|
1148
|
+
name: device.deviceName,
|
|
1149
|
+
serial: device.serialNumber,
|
|
1150
|
+
productUrl: device.productUrl,
|
|
1151
|
+
configUrl: device.configUrl,
|
|
1152
|
+
uniqueId: device.uniqueId,
|
|
1153
|
+
cluster: cluster,
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: devices }));
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
else if (data.method === '/api/clusters') {
|
|
1160
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1161
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' }));
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (!isValidNumber(data.params.endpoint, 1)) {
|
|
1165
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' }));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const clusters = [];
|
|
1169
|
+
let deviceName = '';
|
|
1170
|
+
let serialNumber = '';
|
|
1171
|
+
let deviceTypes = [];
|
|
1172
|
+
this.matterbridge.devices.forEach(async (device) => {
|
|
1173
|
+
if (data.params.plugin !== device.plugin)
|
|
1174
|
+
return;
|
|
1175
|
+
if (data.params.endpoint !== device.number)
|
|
1176
|
+
return;
|
|
1177
|
+
deviceName = device.deviceName ?? 'Unknown';
|
|
1178
|
+
serialNumber = device.serialNumber ?? 'Unknown';
|
|
1179
|
+
deviceTypes = [];
|
|
1180
|
+
const endpointServer = EndpointServer.forEndpoint(device);
|
|
1181
|
+
const clusterServers = endpointServer.getAllClusterServers();
|
|
1182
|
+
clusterServers.forEach((clusterServer) => {
|
|
1183
|
+
Object.entries(clusterServer.attributes).forEach(([key, value]) => {
|
|
1184
|
+
if (clusterServer.name === 'EveHistory')
|
|
1185
|
+
return;
|
|
1186
|
+
if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
|
|
1187
|
+
value.getLocal().forEach((deviceType) => {
|
|
1188
|
+
deviceTypes.push(deviceType.deviceType);
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
let attributeValue;
|
|
1192
|
+
let attributeLocalValue;
|
|
1193
|
+
try {
|
|
1194
|
+
if (typeof value.getLocal() === 'object')
|
|
1195
|
+
attributeValue = stringify(value.getLocal());
|
|
1196
|
+
else
|
|
1197
|
+
attributeValue = value.getLocal().toString();
|
|
1198
|
+
attributeLocalValue = value.getLocal();
|
|
1199
|
+
}
|
|
1200
|
+
catch (error) {
|
|
1201
|
+
attributeValue = 'Fabric-Scoped';
|
|
1202
|
+
attributeLocalValue = 'Fabric-Scoped';
|
|
1203
|
+
this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
|
|
1204
|
+
}
|
|
1205
|
+
clusters.push({
|
|
1206
|
+
endpoint: device.number ? device.number.toString() : '...',
|
|
1207
|
+
id: 'main',
|
|
1208
|
+
deviceTypes,
|
|
1209
|
+
clusterName: clusterServer.name,
|
|
1210
|
+
clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
|
|
1211
|
+
attributeName: key,
|
|
1212
|
+
attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
|
|
1213
|
+
attributeValue,
|
|
1214
|
+
attributeLocalValue,
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
endpointServer.getChildEndpoints().forEach((childEndpoint) => {
|
|
1219
|
+
deviceTypes = [];
|
|
1220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1221
|
+
const name = childEndpoint.endpoint?.id;
|
|
1222
|
+
const clusterServers = childEndpoint.getAllClusterServers();
|
|
1223
|
+
clusterServers.forEach((clusterServer) => {
|
|
1224
|
+
Object.entries(clusterServer.attributes).forEach(([key, value]) => {
|
|
1225
|
+
if (clusterServer.name === 'EveHistory')
|
|
1226
|
+
return;
|
|
1227
|
+
if (clusterServer.name === 'Descriptor' && key === 'deviceTypeList') {
|
|
1228
|
+
value.getLocal().forEach((deviceType) => {
|
|
1229
|
+
deviceTypes.push(deviceType.deviceType);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
let attributeValue;
|
|
1233
|
+
let attributeLocalValue;
|
|
1234
|
+
try {
|
|
1235
|
+
if (typeof value.getLocal() === 'object')
|
|
1236
|
+
attributeValue = stringify(value.getLocal());
|
|
1237
|
+
else
|
|
1238
|
+
attributeValue = value.getLocal().toString();
|
|
1239
|
+
attributeLocalValue = value.getLocal();
|
|
1240
|
+
}
|
|
1241
|
+
catch (error) {
|
|
1242
|
+
attributeValue = 'Fabric-Scoped';
|
|
1243
|
+
attributeLocalValue = 'Fabric-Scoped';
|
|
1244
|
+
this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
|
|
1245
|
+
}
|
|
1246
|
+
clusters.push({
|
|
1247
|
+
endpoint: childEndpoint.number ? childEndpoint.number.toString() : '...',
|
|
1248
|
+
id: name,
|
|
1249
|
+
deviceTypes,
|
|
1250
|
+
clusterName: clusterServer.name,
|
|
1251
|
+
clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
|
|
1252
|
+
attributeName: key,
|
|
1253
|
+
attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
|
|
1254
|
+
attributeValue,
|
|
1255
|
+
attributeLocalValue,
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, deviceName, serialNumber, endpoint: data.params.endpoint, deviceTypes, response: clusters }));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
else if (data.method === '/api/select') {
|
|
1265
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1266
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select' }));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
1270
|
+
if (!plugin) {
|
|
1271
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select' }));
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const selectDeviceValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectDevice.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
|
|
1275
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectDeviceValues }));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
else if (data.method === '/api/select/entities') {
|
|
1279
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1280
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/entities' }));
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
1284
|
+
if (!plugin) {
|
|
1285
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' }));
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const selectEntityValues = plugin.platform?.selectDevice ? Array.from(plugin.platform.selectEntity.values()).sort((keyA, keyB) => keyA.name.localeCompare(keyB.name)) : [];
|
|
1289
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, plugin: data.params.plugin, response: selectEntityValues }));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
else {
|
|
1293
|
+
this.log.error(`Invalid method from websocket client: ${debugStringify(data)}`);
|
|
1294
|
+
client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid method' }));
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Sends a WebSocket message to all connected clients.
|
|
1305
|
+
*
|
|
1306
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
1307
|
+
* @param {string} time - The time string of the message
|
|
1308
|
+
* @param {string} name - The logger name of the message
|
|
1309
|
+
* @param {string} message - The content of the message.
|
|
1310
|
+
*/
|
|
1311
|
+
wssSendMessage(level, time, name, message) {
|
|
1312
|
+
if (!level || !time || !name || !message)
|
|
1313
|
+
return;
|
|
1314
|
+
// Remove ANSI escape codes from the message
|
|
1315
|
+
// eslint-disable-next-line no-control-regex
|
|
1316
|
+
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
1317
|
+
// Remove leading asterisks from the message
|
|
1318
|
+
message = message.replace(/^\*+/, '');
|
|
1319
|
+
// Replace all occurrences of \t and \n
|
|
1320
|
+
message = message.replace(/[\t\n]/g, '');
|
|
1321
|
+
// Remove non-printable characters
|
|
1322
|
+
// eslint-disable-next-line no-control-regex
|
|
1323
|
+
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
1324
|
+
// Replace all occurrences of \" with "
|
|
1325
|
+
message = message.replace(/\\"/g, '"');
|
|
1326
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1327
|
+
const maxContinuousLength = 100;
|
|
1328
|
+
const keepStartLength = 20;
|
|
1329
|
+
const keepEndLength = 20;
|
|
1330
|
+
// Split the message into words
|
|
1331
|
+
message = message
|
|
1332
|
+
.split(' ')
|
|
1333
|
+
.map((word) => {
|
|
1334
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1335
|
+
if (word.length > maxContinuousLength) {
|
|
1336
|
+
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1337
|
+
}
|
|
1338
|
+
return word;
|
|
1339
|
+
})
|
|
1340
|
+
.join(' ');
|
|
1341
|
+
// Send the message to all connected clients
|
|
1342
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
1343
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1344
|
+
client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
1350
|
+
*
|
|
1351
|
+
*/
|
|
1352
|
+
wssSendRefreshRequired() {
|
|
1353
|
+
this.log.debug('Sending a refresh required message to all connected clients');
|
|
1354
|
+
this.matterbridge.matterbridgeInformation.refreshRequired = true;
|
|
1355
|
+
// Send the message to all connected clients
|
|
1356
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
1357
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1358
|
+
client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
1364
|
+
*
|
|
1365
|
+
*/
|
|
1366
|
+
wssSendRestartRequired() {
|
|
1367
|
+
this.log.debug('Sending a restart required message to all connected clients');
|
|
1368
|
+
this.matterbridge.matterbridgeInformation.restartRequired = true;
|
|
1369
|
+
// Send the message to all connected clients
|
|
1370
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
1371
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
1372
|
+
client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
//# sourceMappingURL=frontend.js.map
|