matterbridge 3.3.5-dev-20251031-3482891 → 3.3.5
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 +4 -3
- package/README-SERVICE-LOCAL.md +13 -13
- package/README.md +3 -1
- package/dist/broadcastServer.d.ts +112 -0
- package/dist/broadcastServer.d.ts.map +1 -0
- package/dist/broadcastServer.js +92 -1
- package/dist/broadcastServer.js.map +1 -0
- package/dist/broadcastServerTypes.d.ts +803 -0
- package/dist/broadcastServerTypes.d.ts.map +1 -0
- package/dist/broadcastServerTypes.js +24 -0
- package/dist/broadcastServerTypes.js.map +1 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +97 -1
- package/dist/cli.js.map +1 -0
- package/dist/cliEmitter.d.ts +50 -0
- package/dist/cliEmitter.d.ts.map +1 -0
- package/dist/cliEmitter.js +37 -0
- package/dist/cliEmitter.js.map +1 -0
- package/dist/cliHistory.d.ts +48 -0
- package/dist/cliHistory.d.ts.map +1 -0
- package/dist/cliHistory.js +38 -0
- package/dist/cliHistory.js.map +1 -0
- package/dist/clusters/export.d.ts +2 -0
- package/dist/clusters/export.d.ts.map +1 -0
- package/dist/clusters/export.js +2 -0
- package/dist/clusters/export.js.map +1 -0
- package/dist/defaultConfigSchema.d.ts +28 -0
- package/dist/defaultConfigSchema.d.ts.map +1 -0
- package/dist/defaultConfigSchema.js +24 -0
- package/dist/defaultConfigSchema.js.map +1 -0
- package/dist/deviceManager.d.ts +117 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +124 -1
- package/dist/deviceManager.js.map +1 -0
- package/dist/devices/airConditioner.d.ts +98 -0
- package/dist/devices/airConditioner.d.ts.map +1 -0
- package/dist/devices/airConditioner.js +57 -0
- package/dist/devices/airConditioner.js.map +1 -0
- package/dist/devices/batteryStorage.d.ts +48 -0
- package/dist/devices/batteryStorage.d.ts.map +1 -0
- package/dist/devices/batteryStorage.js +48 -1
- package/dist/devices/batteryStorage.js.map +1 -0
- package/dist/devices/cooktop.d.ts +60 -0
- package/dist/devices/cooktop.d.ts.map +1 -0
- package/dist/devices/cooktop.js +55 -0
- package/dist/devices/cooktop.js.map +1 -0
- package/dist/devices/dishwasher.d.ts +71 -0
- package/dist/devices/dishwasher.d.ts.map +1 -0
- package/dist/devices/dishwasher.js +57 -0
- package/dist/devices/dishwasher.js.map +1 -0
- package/dist/devices/evse.d.ts +76 -0
- package/dist/devices/evse.d.ts.map +1 -0
- package/dist/devices/evse.js +74 -10
- package/dist/devices/evse.js.map +1 -0
- package/dist/devices/export.d.ts +17 -0
- package/dist/devices/export.d.ts.map +1 -0
- package/dist/devices/export.js +5 -0
- package/dist/devices/export.js.map +1 -0
- package/dist/devices/extractorHood.d.ts +46 -0
- package/dist/devices/extractorHood.d.ts.map +1 -0
- package/dist/devices/extractorHood.js +42 -0
- package/dist/devices/extractorHood.js.map +1 -0
- package/dist/devices/heatPump.d.ts +47 -0
- package/dist/devices/heatPump.d.ts.map +1 -0
- package/dist/devices/heatPump.js +50 -2
- package/dist/devices/heatPump.js.map +1 -0
- package/dist/devices/laundryDryer.d.ts +67 -0
- package/dist/devices/laundryDryer.d.ts.map +1 -0
- package/dist/devices/laundryDryer.js +62 -3
- package/dist/devices/laundryDryer.js.map +1 -0
- package/dist/devices/laundryWasher.d.ts +81 -0
- package/dist/devices/laundryWasher.d.ts.map +1 -0
- package/dist/devices/laundryWasher.js +70 -4
- package/dist/devices/laundryWasher.js.map +1 -0
- package/dist/devices/microwaveOven.d.ts +168 -0
- package/dist/devices/microwaveOven.d.ts.map +1 -0
- package/dist/devices/microwaveOven.js +88 -5
- package/dist/devices/microwaveOven.js.map +1 -0
- package/dist/devices/oven.d.ts +105 -0
- package/dist/devices/oven.d.ts.map +1 -0
- package/dist/devices/oven.js +85 -0
- package/dist/devices/oven.js.map +1 -0
- package/dist/devices/refrigerator.d.ts +118 -0
- package/dist/devices/refrigerator.d.ts.map +1 -0
- package/dist/devices/refrigerator.js +102 -0
- package/dist/devices/refrigerator.js.map +1 -0
- package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
- package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/devices/roboticVacuumCleaner.js +100 -9
- package/dist/devices/roboticVacuumCleaner.js.map +1 -0
- package/dist/devices/solarPower.d.ts +40 -0
- package/dist/devices/solarPower.d.ts.map +1 -0
- package/dist/devices/solarPower.js +38 -0
- package/dist/devices/solarPower.js.map +1 -0
- package/dist/devices/speaker.d.ts +87 -0
- package/dist/devices/speaker.d.ts.map +1 -0
- package/dist/devices/speaker.js +84 -0
- package/dist/devices/speaker.js.map +1 -0
- package/dist/devices/temperatureControl.d.ts +166 -0
- package/dist/devices/temperatureControl.d.ts.map +1 -0
- package/dist/devices/temperatureControl.js +24 -3
- package/dist/devices/temperatureControl.js.map +1 -0
- package/dist/devices/waterHeater.d.ts +111 -0
- package/dist/devices/waterHeater.d.ts.map +1 -0
- package/dist/devices/waterHeater.js +82 -2
- package/dist/devices/waterHeater.js.map +1 -0
- package/dist/dgram/coap.d.ts +205 -0
- package/dist/dgram/coap.d.ts.map +1 -0
- package/dist/dgram/coap.js +126 -13
- package/dist/dgram/coap.js.map +1 -0
- package/dist/dgram/dgram.d.ts +141 -0
- package/dist/dgram/dgram.d.ts.map +1 -0
- package/dist/dgram/dgram.js +114 -2
- package/dist/dgram/dgram.js.map +1 -0
- package/dist/dgram/mb_coap.d.ts +24 -0
- package/dist/dgram/mb_coap.d.ts.map +1 -0
- package/dist/dgram/mb_coap.js +41 -3
- package/dist/dgram/mb_coap.js.map +1 -0
- package/dist/dgram/mb_mdns.d.ts +24 -0
- package/dist/dgram/mb_mdns.d.ts.map +1 -0
- package/dist/dgram/mb_mdns.js +80 -15
- package/dist/dgram/mb_mdns.js.map +1 -0
- package/dist/dgram/mdns.d.ts +290 -0
- package/dist/dgram/mdns.d.ts.map +1 -0
- package/dist/dgram/mdns.js +299 -137
- package/dist/dgram/mdns.js.map +1 -0
- package/dist/dgram/multicast.d.ts +67 -0
- package/dist/dgram/multicast.d.ts.map +1 -0
- package/dist/dgram/multicast.js +62 -1
- package/dist/dgram/multicast.js.map +1 -0
- package/dist/dgram/unicast.d.ts +56 -0
- package/dist/dgram/unicast.d.ts.map +1 -0
- package/dist/dgram/unicast.js +54 -0
- package/dist/dgram/unicast.js.map +1 -0
- package/dist/frontend.d.ts +236 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +431 -34
- package/dist/frontend.js.map +1 -0
- package/dist/frontendTypes.d.ts +529 -0
- package/dist/frontendTypes.d.ts.map +1 -0
- package/dist/frontendTypes.js +45 -0
- package/dist/frontendTypes.js.map +1 -0
- package/dist/helpers.d.ts +48 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +53 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/export.d.ts +2 -0
- package/dist/logger/export.d.ts.map +1 -0
- package/dist/logger/export.js +1 -0
- package/dist/logger/export.js.map +1 -0
- package/dist/matter/behaviors.d.ts +2 -0
- package/dist/matter/behaviors.d.ts.map +1 -0
- package/dist/matter/behaviors.js +2 -0
- package/dist/matter/behaviors.js.map +1 -0
- package/dist/matter/clusters.d.ts +2 -0
- package/dist/matter/clusters.d.ts.map +1 -0
- package/dist/matter/clusters.js +2 -0
- package/dist/matter/clusters.js.map +1 -0
- package/dist/matter/devices.d.ts +2 -0
- package/dist/matter/devices.d.ts.map +1 -0
- package/dist/matter/devices.js +2 -0
- package/dist/matter/devices.js.map +1 -0
- package/dist/matter/endpoints.d.ts +2 -0
- package/dist/matter/endpoints.d.ts.map +1 -0
- package/dist/matter/endpoints.js +2 -0
- package/dist/matter/endpoints.js.map +1 -0
- package/dist/matter/export.d.ts +5 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +3 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/types.d.ts +3 -0
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/types.js +3 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matterbridge.d.ts +476 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +828 -46
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +42 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +37 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +2404 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +68 -5
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +770 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +638 -17
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +42 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +37 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +1556 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +1408 -52
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +758 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +464 -19
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +402 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +341 -1
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +226 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +26 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/pluginManager.d.ts +347 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +319 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/shelly.d.ts +174 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +168 -7
- package/dist/shelly.js.map +1 -0
- package/dist/storage/export.d.ts +2 -0
- package/dist/storage/export.d.ts.map +1 -0
- package/dist/storage/export.js +1 -0
- package/dist/storage/export.js.map +1 -0
- package/dist/update.d.ts +75 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +69 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/colorUtils.d.ts +101 -0
- package/dist/utils/colorUtils.d.ts.map +1 -0
- package/dist/utils/colorUtils.js +97 -2
- package/dist/utils/colorUtils.js.map +1 -0
- package/dist/utils/commandLine.d.ts +66 -0
- package/dist/utils/commandLine.d.ts.map +1 -0
- package/dist/utils/commandLine.js +60 -0
- package/dist/utils/commandLine.js.map +1 -0
- package/dist/utils/copyDirectory.d.ts +33 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +38 -1
- package/dist/utils/copyDirectory.js.map +1 -0
- package/dist/utils/createDirectory.d.ts +34 -0
- package/dist/utils/createDirectory.d.ts.map +1 -0
- package/dist/utils/createDirectory.js +33 -0
- package/dist/utils/createDirectory.js.map +1 -0
- package/dist/utils/createZip.d.ts +39 -0
- package/dist/utils/createZip.d.ts.map +1 -0
- package/dist/utils/createZip.js +47 -2
- package/dist/utils/createZip.js.map +1 -0
- package/dist/utils/deepCopy.d.ts +32 -0
- package/dist/utils/deepCopy.d.ts.map +1 -0
- package/dist/utils/deepCopy.js +39 -0
- package/dist/utils/deepCopy.js.map +1 -0
- package/dist/utils/deepEqual.d.ts +54 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/deepEqual.js +72 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/error.d.ts +44 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +41 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/export.d.ts +13 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +1 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/utils/format.d.ts +53 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +49 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/hex.d.ts +89 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/hex.js +124 -0
- package/dist/utils/hex.js.map +1 -0
- package/dist/utils/inspector.d.ts +87 -0
- package/dist/utils/inspector.d.ts.map +1 -0
- package/dist/utils/inspector.js +69 -1
- package/dist/utils/inspector.js.map +1 -0
- package/dist/utils/isvalid.d.ts +103 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +101 -0
- package/dist/utils/isvalid.js.map +1 -0
- package/dist/utils/jestHelpers.d.ts +139 -0
- package/dist/utils/jestHelpers.d.ts.map +1 -0
- package/dist/utils/jestHelpers.js +153 -3
- package/dist/utils/jestHelpers.js.map +1 -0
- package/dist/utils/network.d.ts +101 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +96 -5
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/spawn.d.ts +35 -0
- package/dist/utils/spawn.d.ts.map +1 -0
- package/dist/utils/spawn.js +71 -0
- package/dist/utils/spawn.js.map +1 -0
- package/dist/utils/tracker.d.ts +108 -0
- package/dist/utils/tracker.d.ts.map +1 -0
- package/dist/utils/tracker.js +64 -1
- package/dist/utils/tracker.js.map +1 -0
- package/dist/utils/wait.d.ts +54 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +60 -8
- package/dist/utils/wait.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/frontend.js
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Frontend.
|
|
3
|
+
*
|
|
4
|
+
* @file frontend.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @created 2025-01-13
|
|
7
|
+
* @version 1.3.0
|
|
8
|
+
* @license Apache-2.0
|
|
9
|
+
*
|
|
10
|
+
* Copyright 2025, 2026, 2027 Luca Liguori.
|
|
11
|
+
*
|
|
12
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
* you may not use this file except in compliance with the License.
|
|
14
|
+
* You may obtain a copy of the License at
|
|
15
|
+
*
|
|
16
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
*
|
|
18
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
* See the License for the specific language governing permissions and
|
|
22
|
+
* limitations under the License.
|
|
23
|
+
*/
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
1
25
|
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
2
26
|
console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
|
|
27
|
+
// Node modules
|
|
3
28
|
import os from 'node:os';
|
|
4
29
|
import path from 'node:path';
|
|
5
30
|
import EventEmitter from 'node:events';
|
|
31
|
+
// AnsiLogger module
|
|
6
32
|
import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
|
|
7
33
|
import { Logger, Diagnostic, LogDestination, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/general';
|
|
8
34
|
import { DeviceAdvertiser, DeviceCommissioner, FabricManager } from '@matter/protocol';
|
|
@@ -35,7 +61,7 @@ export class Frontend extends EventEmitter {
|
|
|
35
61
|
constructor(matterbridge) {
|
|
36
62
|
super();
|
|
37
63
|
this.matterbridge = matterbridge;
|
|
38
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
64
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
39
65
|
this.log.logNameColor = '\x1b[38;5;97m';
|
|
40
66
|
this.server = new BroadcastServer('frontend', this.log);
|
|
41
67
|
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
@@ -130,23 +156,53 @@ export class Frontend extends EventEmitter {
|
|
|
130
156
|
this.port = port;
|
|
131
157
|
this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
|
|
132
158
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
159
|
+
// Initialize multer with the upload directory
|
|
133
160
|
const multer = await import('multer');
|
|
134
|
-
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
161
|
+
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
|
|
135
162
|
const upload = multer.default({ dest: uploadDir });
|
|
163
|
+
// Create the express app that serves the frontend
|
|
136
164
|
const express = await import('express');
|
|
137
165
|
this.expressApp = express.default();
|
|
166
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
167
|
+
/*
|
|
168
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
169
|
+
for (const method of methods) {
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
174
|
+
try {
|
|
175
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
176
|
+
return original(path, ...rest);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
*/
|
|
184
|
+
// Log all requests to the server for debugging
|
|
185
|
+
/*
|
|
186
|
+
this.expressApp.use((req, res, next) => {
|
|
187
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
188
|
+
next();
|
|
189
|
+
});
|
|
190
|
+
*/
|
|
191
|
+
// Serve static files from 'frontend/build' directory
|
|
138
192
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend/build')));
|
|
193
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
139
194
|
this.log.debug(`Creating WebSocketServer...`);
|
|
140
195
|
const ws = await import('ws');
|
|
141
196
|
this.webSocketServer = new ws.WebSocketServer({ noServer: true });
|
|
142
197
|
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
143
198
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
144
199
|
const clientIp = request.socket.remoteAddress;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
// Set the global logger callback for the WebSocketServer
|
|
201
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
202
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
203
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
204
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
205
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
150
206
|
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
151
207
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
152
208
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -168,16 +224,25 @@ export class Frontend extends EventEmitter {
|
|
|
168
224
|
}
|
|
169
225
|
});
|
|
170
226
|
ws.on('error', (error) => {
|
|
227
|
+
// istanbul ignore next
|
|
171
228
|
this.log.error(`WebSocket client error: ${error}`);
|
|
172
229
|
});
|
|
173
230
|
});
|
|
174
231
|
this.webSocketServer.on('close', () => {
|
|
175
232
|
this.log.debug(`WebSocketServer closed`);
|
|
176
233
|
});
|
|
234
|
+
/* With { noServer: true } it never fires
|
|
235
|
+
this.webSocketServer.on('listening', () => {
|
|
236
|
+
this.log.info(`The WebSocketServer is listening`);
|
|
237
|
+
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
238
|
+
});
|
|
239
|
+
*/
|
|
240
|
+
// istanbul ignore next
|
|
177
241
|
this.webSocketServer.on('error', (ws, error) => {
|
|
178
242
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
179
243
|
});
|
|
180
244
|
if (!hasParameter('ssl')) {
|
|
245
|
+
// Create an HTTP server and attach the express app
|
|
181
246
|
const http = await import('node:http');
|
|
182
247
|
try {
|
|
183
248
|
this.log.debug(`Creating HTTP server...`);
|
|
@@ -188,6 +253,7 @@ export class Frontend extends EventEmitter {
|
|
|
188
253
|
this.emit('server_error', error);
|
|
189
254
|
return;
|
|
190
255
|
}
|
|
256
|
+
// Listen on the specified port
|
|
191
257
|
if (hasParameter('ingress')) {
|
|
192
258
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
193
259
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -207,23 +273,29 @@ export class Frontend extends EventEmitter {
|
|
|
207
273
|
}
|
|
208
274
|
this.httpServer.on('upgrade', async (req, socket, head) => {
|
|
209
275
|
try {
|
|
276
|
+
// Only proceed for real WebSocket upgrades
|
|
277
|
+
// istanbul ignore next cause is only a safety check
|
|
210
278
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
211
279
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
212
280
|
return socket.destroy();
|
|
213
281
|
}
|
|
282
|
+
// Build a URL so we can read ?password=...
|
|
214
283
|
const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
|
|
284
|
+
// Validate WebSocket password
|
|
215
285
|
const password = url.searchParams.get('password') ?? '';
|
|
216
286
|
if (password !== this.storedPassword) {
|
|
217
287
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
218
288
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
219
289
|
return socket.destroy();
|
|
220
290
|
}
|
|
291
|
+
// Complete the WebSocket handshake
|
|
221
292
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
222
293
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
223
294
|
this.webSocketServer?.emit('connection', ws, req);
|
|
224
295
|
});
|
|
225
296
|
}
|
|
226
297
|
catch (err) {
|
|
298
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
227
299
|
{
|
|
228
300
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
229
301
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -246,6 +318,7 @@ export class Frontend extends EventEmitter {
|
|
|
246
318
|
});
|
|
247
319
|
}
|
|
248
320
|
else {
|
|
321
|
+
// SSL is enabled, load the certificate and the private key
|
|
249
322
|
let cert;
|
|
250
323
|
let key;
|
|
251
324
|
let ca;
|
|
@@ -255,6 +328,7 @@ export class Frontend extends EventEmitter {
|
|
|
255
328
|
let httpsServerOptions = {};
|
|
256
329
|
const fs = await import('node:fs');
|
|
257
330
|
if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'))) {
|
|
331
|
+
// Load the p12 certificate and the passphrase
|
|
258
332
|
try {
|
|
259
333
|
pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12'));
|
|
260
334
|
this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.p12')}`);
|
|
@@ -266,7 +340,7 @@ export class Frontend extends EventEmitter {
|
|
|
266
340
|
}
|
|
267
341
|
try {
|
|
268
342
|
passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass'), 'utf8');
|
|
269
|
-
passphrase = passphrase.trim();
|
|
343
|
+
passphrase = passphrase.trim(); // Ensure no extra characters
|
|
270
344
|
this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pass')}`);
|
|
271
345
|
}
|
|
272
346
|
catch (error) {
|
|
@@ -277,6 +351,7 @@ export class Frontend extends EventEmitter {
|
|
|
277
351
|
httpsServerOptions = { pfx, passphrase };
|
|
278
352
|
}
|
|
279
353
|
else {
|
|
354
|
+
// Load the SSL certificate, the private key and optionally the CA certificate. If the CA certificate is present, it will be used to create a full chain certificate.
|
|
280
355
|
try {
|
|
281
356
|
cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
|
|
282
357
|
this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs/cert.pem')}`);
|
|
@@ -306,9 +381,10 @@ export class Frontend extends EventEmitter {
|
|
|
306
381
|
httpsServerOptions = { cert: fullChain ?? cert, key, ca };
|
|
307
382
|
}
|
|
308
383
|
if (hasParameter('mtls')) {
|
|
309
|
-
httpsServerOptions.requestCert = true;
|
|
310
|
-
httpsServerOptions.rejectUnauthorized = true;
|
|
384
|
+
httpsServerOptions.requestCert = true; // Request client certificate
|
|
385
|
+
httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
|
|
311
386
|
}
|
|
387
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
312
388
|
const https = await import('node:https');
|
|
313
389
|
try {
|
|
314
390
|
this.log.debug(`Creating HTTPS server...`);
|
|
@@ -319,6 +395,7 @@ export class Frontend extends EventEmitter {
|
|
|
319
395
|
this.emit('server_error', error);
|
|
320
396
|
return;
|
|
321
397
|
}
|
|
398
|
+
// Listen on the specified port
|
|
322
399
|
if (hasParameter('ingress')) {
|
|
323
400
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
324
401
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -338,23 +415,29 @@ export class Frontend extends EventEmitter {
|
|
|
338
415
|
}
|
|
339
416
|
this.httpsServer.on('upgrade', async (req, socket, head) => {
|
|
340
417
|
try {
|
|
418
|
+
// Only proceed for real WebSocket upgrades
|
|
419
|
+
// istanbul ignore next cause is only a safety check
|
|
341
420
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
342
421
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
343
422
|
return socket.destroy();
|
|
344
423
|
}
|
|
424
|
+
// Build a URL so we can read ?password=...
|
|
345
425
|
const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
|
|
426
|
+
// Validate WebSocket password
|
|
346
427
|
const password = url.searchParams.get('password') ?? '';
|
|
347
428
|
if (password !== this.storedPassword) {
|
|
348
429
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
349
430
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
350
431
|
return socket.destroy();
|
|
351
432
|
}
|
|
433
|
+
// Complete the WebSocket handshake
|
|
352
434
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
353
435
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
354
436
|
this.webSocketServer?.emit('connection', ws, req);
|
|
355
437
|
});
|
|
356
438
|
}
|
|
357
439
|
catch (err) {
|
|
440
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
358
441
|
{
|
|
359
442
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
360
443
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -376,6 +459,7 @@ export class Frontend extends EventEmitter {
|
|
|
376
459
|
return;
|
|
377
460
|
});
|
|
378
461
|
}
|
|
462
|
+
// Subscribe to cli events
|
|
379
463
|
cliEmitter.removeAllListeners();
|
|
380
464
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
381
465
|
this.wssSendUptimeUpdate(systemUptime, processUptime);
|
|
@@ -386,6 +470,8 @@ export class Frontend extends EventEmitter {
|
|
|
386
470
|
cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
|
|
387
471
|
this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
|
|
388
472
|
});
|
|
473
|
+
// Endpoint to validate login code
|
|
474
|
+
// curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
|
|
389
475
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
390
476
|
const { password } = req.body;
|
|
391
477
|
this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
|
|
@@ -398,17 +484,20 @@ export class Frontend extends EventEmitter {
|
|
|
398
484
|
res.json({ valid: false });
|
|
399
485
|
}
|
|
400
486
|
});
|
|
487
|
+
// Endpoint to provide health check for docker
|
|
401
488
|
this.expressApp.get('/health', (req, res) => {
|
|
402
489
|
this.log.debug('Express received /health');
|
|
403
490
|
const healthStatus = {
|
|
404
|
-
status: 'ok',
|
|
405
|
-
uptime: process.uptime(),
|
|
406
|
-
timestamp: new Date().toISOString(),
|
|
491
|
+
status: 'ok', // Indicate service is healthy
|
|
492
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
493
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
407
494
|
};
|
|
408
495
|
res.status(200).json(healthStatus);
|
|
409
496
|
});
|
|
497
|
+
// Endpoint to provide memory usage details
|
|
410
498
|
this.expressApp.get('/memory', async (req, res) => {
|
|
411
499
|
this.log.debug('Express received /memory');
|
|
500
|
+
// Memory usage from process
|
|
412
501
|
const memoryUsageRaw = process.memoryUsage();
|
|
413
502
|
const memoryUsage = {
|
|
414
503
|
rss: formatBytes(memoryUsageRaw.rss),
|
|
@@ -417,10 +506,13 @@ export class Frontend extends EventEmitter {
|
|
|
417
506
|
external: formatBytes(memoryUsageRaw.external),
|
|
418
507
|
arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
|
|
419
508
|
};
|
|
509
|
+
// V8 heap statistics
|
|
420
510
|
const { default: v8 } = await import('node:v8');
|
|
421
511
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
422
512
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
513
|
+
// Format heapStats
|
|
423
514
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
|
|
515
|
+
// Format heapSpaces
|
|
424
516
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
425
517
|
...space,
|
|
426
518
|
space_size: formatBytes(space.space_size),
|
|
@@ -439,18 +531,22 @@ export class Frontend extends EventEmitter {
|
|
|
439
531
|
};
|
|
440
532
|
res.status(200).json(memoryReport);
|
|
441
533
|
});
|
|
534
|
+
// Endpoint to provide settings
|
|
442
535
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
443
536
|
this.log.debug('The frontend sent /api/settings');
|
|
444
537
|
res.json(await this.getApiSettings());
|
|
445
538
|
});
|
|
539
|
+
// Endpoint to provide plugins
|
|
446
540
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
447
541
|
this.log.debug('The frontend sent /api/plugins');
|
|
448
542
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
|
|
449
543
|
});
|
|
544
|
+
// Endpoint to provide devices
|
|
450
545
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
451
546
|
this.log.debug('The frontend sent /api/devices');
|
|
452
547
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
|
|
453
548
|
});
|
|
549
|
+
// Endpoint to view the matterbridge log
|
|
454
550
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
455
551
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
456
552
|
try {
|
|
@@ -464,6 +560,7 @@ export class Frontend extends EventEmitter {
|
|
|
464
560
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
465
561
|
}
|
|
466
562
|
});
|
|
563
|
+
// Endpoint to view the matter.js log
|
|
467
564
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
468
565
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
469
566
|
try {
|
|
@@ -477,6 +574,7 @@ export class Frontend extends EventEmitter {
|
|
|
477
574
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
478
575
|
}
|
|
479
576
|
});
|
|
577
|
+
// Endpoint to view the diagnostic.log
|
|
480
578
|
this.expressApp.get('/api/view-diagnostic', async (req, res) => {
|
|
481
579
|
this.log.debug('The frontend sent /api/view-diagnostic');
|
|
482
580
|
await this.generateDiagnostic();
|
|
@@ -487,10 +585,13 @@ export class Frontend extends EventEmitter {
|
|
|
487
585
|
res.send(data.slice(29));
|
|
488
586
|
}
|
|
489
587
|
catch (error) {
|
|
588
|
+
// istanbul ignore next
|
|
490
589
|
this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
590
|
+
// istanbul ignore next
|
|
491
591
|
res.status(500).send('Error reading diagnostic log file.');
|
|
492
592
|
}
|
|
493
593
|
});
|
|
594
|
+
// Endpoint to download the diagnostic.log
|
|
494
595
|
this.expressApp.get('/api/download-diagnostic', async (req, res) => {
|
|
495
596
|
this.log.debug(`The frontend sent /api/download-diagnostic`);
|
|
496
597
|
await this.generateDiagnostic();
|
|
@@ -501,16 +602,19 @@ export class Frontend extends EventEmitter {
|
|
|
501
602
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
|
|
502
603
|
}
|
|
503
604
|
catch (error) {
|
|
605
|
+
// istanbul ignore next
|
|
504
606
|
this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
|
|
505
607
|
}
|
|
506
608
|
res.type('text/plain');
|
|
507
609
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
|
|
610
|
+
/* istanbul ignore if */
|
|
508
611
|
if (error) {
|
|
509
612
|
this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
510
613
|
res.status(500).send('Error downloading the diagnostic log file');
|
|
511
614
|
}
|
|
512
615
|
});
|
|
513
616
|
});
|
|
617
|
+
// Endpoint to view the history.html
|
|
514
618
|
this.expressApp.get('/api/viewhistory', async (req, res) => {
|
|
515
619
|
this.log.debug('The frontend sent /api/viewhistory');
|
|
516
620
|
try {
|
|
@@ -524,6 +628,7 @@ export class Frontend extends EventEmitter {
|
|
|
524
628
|
res.status(500).send('Error reading history file.');
|
|
525
629
|
}
|
|
526
630
|
});
|
|
631
|
+
// Endpoint to download the history.html
|
|
527
632
|
this.expressApp.get('/api/downloadhistory', async (req, res) => {
|
|
528
633
|
this.log.debug(`The frontend sent /api/downloadhistory`);
|
|
529
634
|
try {
|
|
@@ -533,6 +638,7 @@ export class Frontend extends EventEmitter {
|
|
|
533
638
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
|
|
534
639
|
res.type('text/plain');
|
|
535
640
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
|
|
641
|
+
/* istanbul ignore if */
|
|
536
642
|
if (error) {
|
|
537
643
|
this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
538
644
|
res.status(500).send('Error downloading history file');
|
|
@@ -544,6 +650,7 @@ export class Frontend extends EventEmitter {
|
|
|
544
650
|
res.status(500).send('Error reading history file.');
|
|
545
651
|
}
|
|
546
652
|
});
|
|
653
|
+
// Endpoint to view the shelly log
|
|
547
654
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
548
655
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
549
656
|
try {
|
|
@@ -557,6 +664,7 @@ export class Frontend extends EventEmitter {
|
|
|
557
664
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
558
665
|
}
|
|
559
666
|
});
|
|
667
|
+
// Endpoint to download the matterbridge log
|
|
560
668
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
561
669
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
562
670
|
const fs = await import('node:fs');
|
|
@@ -571,12 +679,14 @@ export class Frontend extends EventEmitter {
|
|
|
571
679
|
}
|
|
572
680
|
res.type('text/plain');
|
|
573
681
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
|
|
682
|
+
/* istanbul ignore if */
|
|
574
683
|
if (error) {
|
|
575
684
|
this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
576
685
|
res.status(500).send('Error downloading the matterbridge log file');
|
|
577
686
|
}
|
|
578
687
|
});
|
|
579
688
|
});
|
|
689
|
+
// Endpoint to download the matter log
|
|
580
690
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
581
691
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
582
692
|
const fs = await import('node:fs');
|
|
@@ -591,12 +701,14 @@ export class Frontend extends EventEmitter {
|
|
|
591
701
|
}
|
|
592
702
|
res.type('text/plain');
|
|
593
703
|
res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
|
|
704
|
+
/* istanbul ignore if */
|
|
594
705
|
if (error) {
|
|
595
706
|
this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
596
707
|
res.status(500).send('Error downloading the matter log file');
|
|
597
708
|
}
|
|
598
709
|
});
|
|
599
710
|
});
|
|
711
|
+
// Endpoint to download the shelly log
|
|
600
712
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
601
713
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
602
714
|
const fs = await import('node:fs');
|
|
@@ -611,75 +723,91 @@ export class Frontend extends EventEmitter {
|
|
|
611
723
|
}
|
|
612
724
|
res.type('text/plain');
|
|
613
725
|
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
726
|
+
/* istanbul ignore if */
|
|
614
727
|
if (error) {
|
|
615
728
|
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
616
729
|
res.status(500).send('Error downloading Shelly system log file');
|
|
617
730
|
}
|
|
618
731
|
});
|
|
619
732
|
});
|
|
733
|
+
// Endpoint to download the matterbridge storage directory
|
|
620
734
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
621
735
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
622
736
|
await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
|
|
623
737
|
res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
|
|
738
|
+
/* istanbul ignore if */
|
|
624
739
|
if (error) {
|
|
625
740
|
this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
|
|
626
741
|
res.status(500).send('Error downloading the matterbridge storage file');
|
|
627
742
|
}
|
|
628
743
|
});
|
|
629
744
|
});
|
|
745
|
+
// Endpoint to download the matter storage file
|
|
630
746
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
631
747
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
632
748
|
await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
633
749
|
res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
|
|
750
|
+
/* istanbul ignore if */
|
|
634
751
|
if (error) {
|
|
635
752
|
this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
|
|
636
753
|
res.status(500).send('Error downloading the matter storage zip file');
|
|
637
754
|
}
|
|
638
755
|
});
|
|
639
756
|
});
|
|
757
|
+
// Endpoint to download the matterbridge plugin directory
|
|
640
758
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
641
759
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
642
760
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
643
761
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
|
|
762
|
+
/* istanbul ignore if */
|
|
644
763
|
if (error) {
|
|
645
764
|
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
646
765
|
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
647
766
|
}
|
|
648
767
|
});
|
|
649
768
|
});
|
|
769
|
+
// Endpoint to download the matterbridge plugin config files
|
|
650
770
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
651
771
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
652
772
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
653
773
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
774
|
+
/* istanbul ignore if */
|
|
654
775
|
if (error) {
|
|
655
776
|
this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
|
|
656
777
|
res.status(500).send('Error downloading the matterbridge plugin config file');
|
|
657
778
|
}
|
|
658
779
|
});
|
|
659
780
|
});
|
|
781
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
660
782
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
661
783
|
this.log.debug('The frontend sent /api/download-backup');
|
|
662
784
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
785
|
+
/* istanbul ignore if */
|
|
663
786
|
if (error) {
|
|
664
787
|
this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
665
788
|
res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
666
789
|
}
|
|
667
790
|
});
|
|
668
791
|
});
|
|
792
|
+
// Endpoint to upload a package
|
|
669
793
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
670
794
|
const { filename } = req.body;
|
|
671
795
|
const file = req.file;
|
|
796
|
+
/* istanbul ignore if */
|
|
672
797
|
if (!file || !filename) {
|
|
673
798
|
this.log.error(`uploadpackage: invalid request: file and filename are required`);
|
|
674
799
|
res.status(400).send('Invalid request: file and filename are required');
|
|
675
800
|
return;
|
|
676
801
|
}
|
|
677
802
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
803
|
+
// Define the path where the plugin file will be saved
|
|
678
804
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
679
805
|
try {
|
|
806
|
+
// Move the uploaded file to the specified path
|
|
680
807
|
const fs = await import('node:fs');
|
|
681
808
|
await fs.promises.rename(file.path, filePath);
|
|
682
809
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
810
|
+
// Install the plugin package
|
|
683
811
|
if (filename.endsWith('.tgz')) {
|
|
684
812
|
const { spawnCommand } = await import('./utils/spawn.js');
|
|
685
813
|
await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename);
|
|
@@ -699,6 +827,7 @@ export class Frontend extends EventEmitter {
|
|
|
699
827
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
700
828
|
}
|
|
701
829
|
});
|
|
830
|
+
// Fallback for routing (must be the last route)
|
|
702
831
|
this.expressApp.use((req, res) => {
|
|
703
832
|
this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html as fallback`);
|
|
704
833
|
res.sendFile(path.join(this.matterbridge.rootDirectory, 'frontend/build/index.html'));
|
|
@@ -708,13 +837,16 @@ export class Frontend extends EventEmitter {
|
|
|
708
837
|
async stop() {
|
|
709
838
|
this.log.debug('Stopping the frontend...');
|
|
710
839
|
const ws = await import('ws');
|
|
840
|
+
// Remove listeners from the express app
|
|
711
841
|
if (this.expressApp) {
|
|
712
842
|
this.expressApp.removeAllListeners();
|
|
713
843
|
this.expressApp = undefined;
|
|
714
844
|
this.log.debug('Frontend app closed successfully');
|
|
715
845
|
}
|
|
846
|
+
// Close the WebSocket server
|
|
716
847
|
if (this.webSocketServer) {
|
|
717
848
|
this.log.debug('Closing WebSocket server...');
|
|
849
|
+
// Close all active connections
|
|
718
850
|
this.webSocketServer.clients.forEach((client) => {
|
|
719
851
|
if (client.readyState === ws.WebSocket.OPEN) {
|
|
720
852
|
client.close();
|
|
@@ -723,6 +855,7 @@ export class Frontend extends EventEmitter {
|
|
|
723
855
|
await withTimeout(new Promise((resolve) => {
|
|
724
856
|
this.webSocketServer?.close((error) => {
|
|
725
857
|
if (error) {
|
|
858
|
+
// istanbul ignore next
|
|
726
859
|
this.log.error(`Error closing WebSocket server: ${error}`);
|
|
727
860
|
}
|
|
728
861
|
else {
|
|
@@ -735,8 +868,27 @@ export class Frontend extends EventEmitter {
|
|
|
735
868
|
this.webSocketServer.removeAllListeners();
|
|
736
869
|
this.webSocketServer = undefined;
|
|
737
870
|
}
|
|
871
|
+
// Close the http server
|
|
738
872
|
if (this.httpServer) {
|
|
739
873
|
this.log.debug('Closing http server...');
|
|
874
|
+
/*
|
|
875
|
+
await withTimeout(
|
|
876
|
+
new Promise<void>((resolve) => {
|
|
877
|
+
this.httpServer?.close((error) => {
|
|
878
|
+
if (error) {
|
|
879
|
+
// istanbul ignore next
|
|
880
|
+
this.log.error(`Error closing http server: ${error}`);
|
|
881
|
+
} else {
|
|
882
|
+
this.log.debug('Http server closed successfully');
|
|
883
|
+
this.emit('server_stopped');
|
|
884
|
+
}
|
|
885
|
+
resolve();
|
|
886
|
+
});
|
|
887
|
+
}),
|
|
888
|
+
5000,
|
|
889
|
+
false,
|
|
890
|
+
);
|
|
891
|
+
*/
|
|
740
892
|
this.httpServer.close();
|
|
741
893
|
this.log.debug('Http server closed successfully');
|
|
742
894
|
this.listening = false;
|
|
@@ -745,8 +897,27 @@ export class Frontend extends EventEmitter {
|
|
|
745
897
|
this.httpServer = undefined;
|
|
746
898
|
this.log.debug('Frontend http server closed successfully');
|
|
747
899
|
}
|
|
900
|
+
// Close the https server
|
|
748
901
|
if (this.httpsServer) {
|
|
749
902
|
this.log.debug('Closing https server...');
|
|
903
|
+
/*
|
|
904
|
+
await withTimeout(
|
|
905
|
+
new Promise<void>((resolve) => {
|
|
906
|
+
this.httpsServer?.close((error) => {
|
|
907
|
+
if (error) {
|
|
908
|
+
// istanbul ignore next
|
|
909
|
+
this.log.error(`Error closing https server: ${error}`);
|
|
910
|
+
} else {
|
|
911
|
+
this.log.debug('Https server closed successfully');
|
|
912
|
+
this.emit('server_stopped');
|
|
913
|
+
}
|
|
914
|
+
resolve();
|
|
915
|
+
});
|
|
916
|
+
}),
|
|
917
|
+
5000,
|
|
918
|
+
false,
|
|
919
|
+
);
|
|
920
|
+
*/
|
|
750
921
|
this.httpsServer.close();
|
|
751
922
|
this.log.debug('Https server closed successfully');
|
|
752
923
|
this.listening = false;
|
|
@@ -757,7 +928,13 @@ export class Frontend extends EventEmitter {
|
|
|
757
928
|
}
|
|
758
929
|
this.log.debug('Frontend stopped successfully');
|
|
759
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Retrieves the api settings data.
|
|
933
|
+
*
|
|
934
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
935
|
+
*/
|
|
760
936
|
async getApiSettings() {
|
|
937
|
+
// Update the variable system information properties
|
|
761
938
|
this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
762
939
|
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
763
940
|
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
@@ -767,6 +944,7 @@ export class Frontend extends EventEmitter {
|
|
|
767
944
|
this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
|
|
768
945
|
this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
769
946
|
this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
947
|
+
// Create the matterbridge information
|
|
770
948
|
const info = {
|
|
771
949
|
homeDirectory: this.matterbridge.homeDirectory,
|
|
772
950
|
rootDirectory: this.matterbridge.rootDirectory,
|
|
@@ -802,9 +980,15 @@ export class Frontend extends EventEmitter {
|
|
|
802
980
|
};
|
|
803
981
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
|
|
804
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Retrieves the reachable attribute.
|
|
985
|
+
*
|
|
986
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
|
|
987
|
+
* @returns {boolean} The reachable attribute.
|
|
988
|
+
*/
|
|
805
989
|
getReachability(device) {
|
|
806
990
|
if (this.matterbridge.hasCleanupStarted)
|
|
807
|
-
return false;
|
|
991
|
+
return false; // Skip if cleanup has started
|
|
808
992
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
809
993
|
return false;
|
|
810
994
|
if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
|
|
@@ -815,9 +999,15 @@ export class Frontend extends EventEmitter {
|
|
|
815
999
|
return true;
|
|
816
1000
|
return false;
|
|
817
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Retrieves the power source attribute.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
|
|
1006
|
+
* @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
|
|
1007
|
+
*/
|
|
818
1008
|
getPowerSource(endpoint) {
|
|
819
1009
|
if (this.matterbridge.hasCleanupStarted)
|
|
820
|
-
return;
|
|
1010
|
+
return; // Skip if cleanup has started
|
|
821
1011
|
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
822
1012
|
return undefined;
|
|
823
1013
|
const powerSource = (device) => {
|
|
@@ -832,16 +1022,25 @@ export class Frontend extends EventEmitter {
|
|
|
832
1022
|
}
|
|
833
1023
|
return;
|
|
834
1024
|
};
|
|
1025
|
+
// Root endpoint
|
|
835
1026
|
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
836
1027
|
return powerSource(endpoint);
|
|
1028
|
+
// Child endpoints
|
|
837
1029
|
for (const child of endpoint.getChildEndpoints()) {
|
|
838
1030
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
839
1031
|
return powerSource(child);
|
|
840
1032
|
}
|
|
841
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Retrieves the cluster text description from a given device.
|
|
1036
|
+
* The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
|
|
1037
|
+
*
|
|
1038
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
|
|
1039
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1040
|
+
*/
|
|
842
1041
|
getClusterTextFromDevice(device) {
|
|
843
1042
|
if (this.matterbridge.hasCleanupStarted)
|
|
844
|
-
return '';
|
|
1043
|
+
return ''; // Skip if cleanup has started
|
|
845
1044
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
846
1045
|
return '';
|
|
847
1046
|
const getUserLabel = (device) => {
|
|
@@ -851,6 +1050,7 @@ export class Frontend extends EventEmitter {
|
|
|
851
1050
|
if (composed)
|
|
852
1051
|
return 'Composed: ' + composed.value;
|
|
853
1052
|
}
|
|
1053
|
+
// istanbul ignore next cause is not reachable
|
|
854
1054
|
return '';
|
|
855
1055
|
};
|
|
856
1056
|
const getFixedLabel = (device) => {
|
|
@@ -860,11 +1060,13 @@ export class Frontend extends EventEmitter {
|
|
|
860
1060
|
if (composed)
|
|
861
1061
|
return 'Composed: ' + composed.value;
|
|
862
1062
|
}
|
|
1063
|
+
// istanbul ignore next cause is not reacheable
|
|
863
1064
|
return '';
|
|
864
1065
|
};
|
|
865
1066
|
let attributes = '';
|
|
866
1067
|
let supportedModes = [];
|
|
867
1068
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1069
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
868
1070
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
869
1071
|
return;
|
|
870
1072
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -954,11 +1156,17 @@ export class Frontend extends EventEmitter {
|
|
|
954
1156
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
955
1157
|
attributes += `${getUserLabel(device)} `;
|
|
956
1158
|
});
|
|
1159
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
957
1160
|
return attributes.trimStart().trimEnd();
|
|
958
1161
|
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Retrieves the registered plugins sanitized for res.json().
|
|
1164
|
+
*
|
|
1165
|
+
* @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
|
|
1166
|
+
*/
|
|
959
1167
|
getPlugins() {
|
|
960
1168
|
if (this.matterbridge.hasCleanupStarted)
|
|
961
|
-
return [];
|
|
1169
|
+
return []; // Skip if cleanup has started
|
|
962
1170
|
const plugins = [];
|
|
963
1171
|
for (const plugin of this.matterbridge.plugins.array()) {
|
|
964
1172
|
plugins.push({
|
|
@@ -986,18 +1194,27 @@ export class Frontend extends EventEmitter {
|
|
|
986
1194
|
schemaJson: plugin.schemaJson,
|
|
987
1195
|
hasWhiteList: plugin.configJson?.whiteList !== undefined,
|
|
988
1196
|
hasBlackList: plugin.configJson?.blackList !== undefined,
|
|
1197
|
+
// Childbridge mode specific data
|
|
989
1198
|
matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
|
|
990
1199
|
});
|
|
991
1200
|
}
|
|
992
1201
|
return plugins;
|
|
993
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Retrieves the devices from Matterbridge.
|
|
1205
|
+
*
|
|
1206
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
1207
|
+
* @returns {ApiDevice[]} An array of ApiDevices for the frontend.
|
|
1208
|
+
*/
|
|
994
1209
|
getDevices(pluginName) {
|
|
995
1210
|
if (this.matterbridge.hasCleanupStarted)
|
|
996
|
-
return [];
|
|
1211
|
+
return []; // Skip if cleanup has started
|
|
997
1212
|
const devices = [];
|
|
998
1213
|
for (const device of this.matterbridge.devices.array()) {
|
|
1214
|
+
// Filter by pluginName if provided
|
|
999
1215
|
if (pluginName && pluginName !== device.plugin)
|
|
1000
1216
|
continue;
|
|
1217
|
+
// Check if the device has the required properties
|
|
1001
1218
|
if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
1002
1219
|
continue;
|
|
1003
1220
|
devices.push({
|
|
@@ -1017,24 +1234,39 @@ export class Frontend extends EventEmitter {
|
|
|
1017
1234
|
}
|
|
1018
1235
|
return devices;
|
|
1019
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Retrieves the clusters from a given plugin and endpoint number.
|
|
1239
|
+
*
|
|
1240
|
+
* Response for /api/clusters
|
|
1241
|
+
*
|
|
1242
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1243
|
+
* @param {number} endpointNumber - The endpoint number.
|
|
1244
|
+
* @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
|
|
1245
|
+
*/
|
|
1020
1246
|
getClusters(pluginName, endpointNumber) {
|
|
1021
1247
|
if (this.matterbridge.hasCleanupStarted)
|
|
1022
|
-
return;
|
|
1248
|
+
return; // Skip if cleanup has started
|
|
1023
1249
|
const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
|
|
1024
1250
|
if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
|
|
1025
1251
|
this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1026
1252
|
return;
|
|
1027
1253
|
}
|
|
1254
|
+
// this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
|
|
1255
|
+
// Get the device types from the main endpoint
|
|
1028
1256
|
const deviceTypes = [];
|
|
1029
1257
|
const clusters = [];
|
|
1030
1258
|
endpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1031
1259
|
deviceTypes.push(d.deviceType);
|
|
1032
1260
|
});
|
|
1261
|
+
// Get the clusters from the main endpoint
|
|
1033
1262
|
endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1034
1263
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1035
1264
|
return;
|
|
1036
1265
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1037
1266
|
return;
|
|
1267
|
+
// console.log(
|
|
1268
|
+
// `${idn}${endpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
|
|
1269
|
+
// );
|
|
1038
1270
|
clusters.push({
|
|
1039
1271
|
endpoint: endpoint.number.toString(),
|
|
1040
1272
|
number: endpoint.number,
|
|
@@ -1048,12 +1280,19 @@ export class Frontend extends EventEmitter {
|
|
|
1048
1280
|
attributeLocalValue: attributeValue,
|
|
1049
1281
|
});
|
|
1050
1282
|
});
|
|
1283
|
+
// Get the child endpoints
|
|
1051
1284
|
const childEndpoints = endpoint.getChildEndpoints();
|
|
1285
|
+
// if (childEndpoints.length === 0) {
|
|
1286
|
+
// this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1287
|
+
// }
|
|
1052
1288
|
childEndpoints.forEach((childEndpoint) => {
|
|
1289
|
+
// istanbul ignore if cause is not reachable: should never happen but ...
|
|
1053
1290
|
if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
|
|
1054
1291
|
this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1055
1292
|
return;
|
|
1056
1293
|
}
|
|
1294
|
+
// this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
|
|
1295
|
+
// Get the device types of the child endpoint
|
|
1057
1296
|
const deviceTypes = [];
|
|
1058
1297
|
childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1059
1298
|
deviceTypes.push(d.deviceType);
|
|
@@ -1063,6 +1302,9 @@ export class Frontend extends EventEmitter {
|
|
|
1063
1302
|
return;
|
|
1064
1303
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1065
1304
|
return;
|
|
1305
|
+
// console.log(
|
|
1306
|
+
// `${idn}${childEndpoint.deviceName}${rs}${nf} => Cluster: ${CYAN}${clusterName} (0x${clusterId.toString(16).padStart(2, '0')})${nf} Attribute: ${CYAN}${attributeName} (0x${attributeId.toString(16).padStart(2, '0')})${nf} Value: ${YELLOW}${typeof attributeValue === 'object' ? stringify(attributeValue as object) : attributeValue}${nf}`,
|
|
1307
|
+
// );
|
|
1066
1308
|
clusters.push({
|
|
1067
1309
|
endpoint: childEndpoint.number.toString(),
|
|
1068
1310
|
number: childEndpoint.number,
|
|
@@ -1082,6 +1324,7 @@ export class Frontend extends EventEmitter {
|
|
|
1082
1324
|
async generateDiagnostic() {
|
|
1083
1325
|
this.log.debug('Generating diagnostic...');
|
|
1084
1326
|
const serverNodes = [];
|
|
1327
|
+
// istanbul ignore else
|
|
1085
1328
|
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
1086
1329
|
if (this.matterbridge.serverNode)
|
|
1087
1330
|
serverNodes.push(this.matterbridge.serverNode);
|
|
@@ -1092,6 +1335,7 @@ export class Frontend extends EventEmitter {
|
|
|
1092
1335
|
serverNodes.push(plugin.serverNode);
|
|
1093
1336
|
}
|
|
1094
1337
|
}
|
|
1338
|
+
// istanbul ignore next
|
|
1095
1339
|
for (const device of this.matterbridge.devices.array()) {
|
|
1096
1340
|
if (device.serverNode)
|
|
1097
1341
|
serverNodes.push(device.serverNode);
|
|
@@ -1115,8 +1359,15 @@ export class Frontend extends EventEmitter {
|
|
|
1115
1359
|
values: [...serverNodes],
|
|
1116
1360
|
})));
|
|
1117
1361
|
delete Logger.destinations.diagnostic;
|
|
1118
|
-
await wait(500);
|
|
1362
|
+
await wait(500); // Wait for the log to be written
|
|
1119
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Handles incoming websocket api request messages from the Matterbridge frontend.
|
|
1366
|
+
*
|
|
1367
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1368
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1369
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1370
|
+
*/
|
|
1120
1371
|
async wsMessageHandler(client, message) {
|
|
1121
1372
|
let data;
|
|
1122
1373
|
const sendResponse = (data) => {
|
|
@@ -1136,7 +1387,7 @@ export class Frontend extends EventEmitter {
|
|
|
1136
1387
|
};
|
|
1137
1388
|
try {
|
|
1138
1389
|
data = JSON.parse(message.toString());
|
|
1139
|
-
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
|
|
1390
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
|
|
1140
1391
|
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
1141
1392
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
1142
1393
|
return;
|
|
@@ -1210,6 +1461,7 @@ export class Frontend extends EventEmitter {
|
|
|
1210
1461
|
return;
|
|
1211
1462
|
})
|
|
1212
1463
|
.catch((_error) => {
|
|
1464
|
+
//
|
|
1213
1465
|
});
|
|
1214
1466
|
}
|
|
1215
1467
|
else {
|
|
@@ -1257,6 +1509,7 @@ export class Frontend extends EventEmitter {
|
|
|
1257
1509
|
return;
|
|
1258
1510
|
})
|
|
1259
1511
|
.catch((_error) => {
|
|
1512
|
+
//
|
|
1260
1513
|
});
|
|
1261
1514
|
}
|
|
1262
1515
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1282,6 +1535,7 @@ export class Frontend extends EventEmitter {
|
|
|
1282
1535
|
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1283
1536
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
|
|
1284
1537
|
if (plugin.serverNode) {
|
|
1538
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1285
1539
|
await this.matterbridge.stopServerNode(plugin.serverNode);
|
|
1286
1540
|
plugin.serverNode = undefined;
|
|
1287
1541
|
}
|
|
@@ -1291,18 +1545,20 @@ export class Frontend extends EventEmitter {
|
|
|
1291
1545
|
this.matterbridge.devices.remove(device);
|
|
1292
1546
|
}
|
|
1293
1547
|
}
|
|
1548
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1294
1549
|
if (plugin.type === 'DynamicPlatform' && !plugin.locked)
|
|
1295
1550
|
await this.matterbridge.createDynamicPlugin(plugin);
|
|
1296
1551
|
await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
|
|
1297
|
-
plugin.restartRequired = false;
|
|
1552
|
+
plugin.restartRequired = false; // Reset plugin restartRequired
|
|
1298
1553
|
let needRestart = 0;
|
|
1299
1554
|
for (const plugin of this.matterbridge.plugins) {
|
|
1300
1555
|
if (plugin.restartRequired)
|
|
1301
1556
|
needRestart++;
|
|
1302
1557
|
}
|
|
1303
1558
|
if (needRestart === 0) {
|
|
1304
|
-
this.wssSendRestartNotRequired(true);
|
|
1559
|
+
this.wssSendRestartNotRequired(true); // Reset global restart required message
|
|
1305
1560
|
}
|
|
1561
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1306
1562
|
if (plugin.serverNode)
|
|
1307
1563
|
await this.matterbridge.startServerNode(plugin.serverNode);
|
|
1308
1564
|
this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
|
|
@@ -1568,22 +1824,22 @@ export class Frontend extends EventEmitter {
|
|
|
1568
1824
|
if (isValidString(data.params.value, 4)) {
|
|
1569
1825
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1570
1826
|
if (data.params.value === 'Debug') {
|
|
1571
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1827
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1572
1828
|
}
|
|
1573
1829
|
else if (data.params.value === 'Info') {
|
|
1574
|
-
await this.matterbridge.setLogLevel("info");
|
|
1830
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1575
1831
|
}
|
|
1576
1832
|
else if (data.params.value === 'Notice') {
|
|
1577
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1833
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1578
1834
|
}
|
|
1579
1835
|
else if (data.params.value === 'Warn') {
|
|
1580
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1836
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1581
1837
|
}
|
|
1582
1838
|
else if (data.params.value === 'Error') {
|
|
1583
|
-
await this.matterbridge.setLogLevel("error");
|
|
1839
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1584
1840
|
}
|
|
1585
1841
|
else if (data.params.value === 'Fatal') {
|
|
1586
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1842
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1587
1843
|
}
|
|
1588
1844
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1589
1845
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1594,6 +1850,7 @@ export class Frontend extends EventEmitter {
|
|
|
1594
1850
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1595
1851
|
this.matterbridge.fileLogger = data.params.value;
|
|
1596
1852
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1853
|
+
// Create the file logger for matterbridge
|
|
1597
1854
|
if (data.params.value)
|
|
1598
1855
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
|
|
1599
1856
|
else
|
|
@@ -1622,11 +1879,12 @@ export class Frontend extends EventEmitter {
|
|
|
1622
1879
|
else if (data.params.value === 'Fatal') {
|
|
1623
1880
|
Logger.level = MatterLogLevel.FATAL;
|
|
1624
1881
|
}
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1882
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
1883
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
1884
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
1885
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
1886
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
1887
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
1630
1888
|
AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
|
|
1631
1889
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
1632
1890
|
await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
|
|
@@ -1678,6 +1936,7 @@ export class Frontend extends EventEmitter {
|
|
|
1678
1936
|
}
|
|
1679
1937
|
break;
|
|
1680
1938
|
case 'setmatterport':
|
|
1939
|
+
// eslint-disable-next-line no-case-declarations
|
|
1681
1940
|
const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1682
1941
|
if (isValidNumber(port, 5540, 5600)) {
|
|
1683
1942
|
this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
|
|
@@ -1697,6 +1956,7 @@ export class Frontend extends EventEmitter {
|
|
|
1697
1956
|
}
|
|
1698
1957
|
break;
|
|
1699
1958
|
case 'setmatterdiscriminator':
|
|
1959
|
+
// eslint-disable-next-line no-case-declarations
|
|
1700
1960
|
const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1701
1961
|
if (isValidNumber(discriminator, 0, 4095)) {
|
|
1702
1962
|
this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
|
|
@@ -1716,6 +1976,7 @@ export class Frontend extends EventEmitter {
|
|
|
1716
1976
|
}
|
|
1717
1977
|
break;
|
|
1718
1978
|
case 'setmatterpasscode':
|
|
1979
|
+
// eslint-disable-next-line no-case-declarations
|
|
1719
1980
|
const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1720
1981
|
if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
|
|
1721
1982
|
this.matterbridge.passcode = passcode;
|
|
@@ -1761,15 +2022,19 @@ export class Frontend extends EventEmitter {
|
|
|
1761
2022
|
return;
|
|
1762
2023
|
}
|
|
1763
2024
|
const config = plugin.configJson;
|
|
2025
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1764
2026
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2027
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1765
2028
|
if (select === 'serial')
|
|
1766
2029
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1767
2030
|
if (select === 'name')
|
|
1768
2031
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1769
2032
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
2033
|
+
// Remove postfix from the serial if it exists
|
|
1770
2034
|
if (config.postfix) {
|
|
1771
2035
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1772
2036
|
}
|
|
2037
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1773
2038
|
if (isValidArray(config.whiteList, 1)) {
|
|
1774
2039
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1775
2040
|
config.whiteList.push(data.params.serial);
|
|
@@ -1778,6 +2043,7 @@ export class Frontend extends EventEmitter {
|
|
|
1778
2043
|
config.whiteList.push(data.params.name);
|
|
1779
2044
|
}
|
|
1780
2045
|
}
|
|
2046
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1781
2047
|
if (isValidArray(config.blackList, 1)) {
|
|
1782
2048
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1783
2049
|
config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
|
|
@@ -1805,7 +2071,9 @@ export class Frontend extends EventEmitter {
|
|
|
1805
2071
|
return;
|
|
1806
2072
|
}
|
|
1807
2073
|
const config = plugin.configJson;
|
|
2074
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1808
2075
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2076
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1809
2077
|
if (select === 'serial')
|
|
1810
2078
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1811
2079
|
if (select === 'name')
|
|
@@ -1814,6 +2082,7 @@ export class Frontend extends EventEmitter {
|
|
|
1814
2082
|
if (config.postfix) {
|
|
1815
2083
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1816
2084
|
}
|
|
2085
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1817
2086
|
if (isValidArray(config.whiteList, 1)) {
|
|
1818
2087
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1819
2088
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
|
|
@@ -1822,6 +2091,7 @@ export class Frontend extends EventEmitter {
|
|
|
1822
2091
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
|
|
1823
2092
|
}
|
|
1824
2093
|
}
|
|
2094
|
+
// Add the serial to the blackList
|
|
1825
2095
|
if (isValidArray(config.blackList)) {
|
|
1826
2096
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1827
2097
|
config.blackList.push(data.params.serial);
|
|
@@ -1844,6 +2114,7 @@ export class Frontend extends EventEmitter {
|
|
|
1844
2114
|
}
|
|
1845
2115
|
}
|
|
1846
2116
|
else {
|
|
2117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1847
2118
|
const localData = data;
|
|
1848
2119
|
this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
|
|
1849
2120
|
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
|
|
@@ -1853,23 +2124,46 @@ export class Frontend extends EventEmitter {
|
|
|
1853
2124
|
inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
|
|
1854
2125
|
}
|
|
1855
2126
|
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
2129
|
+
*
|
|
2130
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
2131
|
+
* @param {string} time - The time string of the message
|
|
2132
|
+
* @param {string} name - The logger name of the message
|
|
2133
|
+
* @param {string} message - The content of the message.
|
|
2134
|
+
*
|
|
2135
|
+
* @remarks
|
|
2136
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
2137
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
2138
|
+
* The function sends the message to all connected clients.
|
|
2139
|
+
*/
|
|
1856
2140
|
wssSendLogMessage(level, time, name, message) {
|
|
1857
2141
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1858
2142
|
return;
|
|
1859
2143
|
if (!level || !time || !name || !message)
|
|
1860
2144
|
return;
|
|
2145
|
+
// Remove ANSI escape codes from the message
|
|
2146
|
+
// eslint-disable-next-line no-control-regex
|
|
1861
2147
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
2148
|
+
// Remove leading asterisks from the message
|
|
1862
2149
|
message = message.replace(/^\*+/, '');
|
|
2150
|
+
// Replace all occurrences of \t and \n
|
|
1863
2151
|
message = message.replace(/[\t\n]/g, '');
|
|
2152
|
+
// Remove non-printable characters
|
|
2153
|
+
// eslint-disable-next-line no-control-regex
|
|
1864
2154
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
2155
|
+
// Replace all occurrences of \" with "
|
|
1865
2156
|
message = message.replace(/\\"/g, '"');
|
|
2157
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1866
2158
|
const maxContinuousLength = 100;
|
|
1867
2159
|
const keepStartLength = 20;
|
|
1868
2160
|
const keepEndLength = 20;
|
|
2161
|
+
// Split the message into words
|
|
1869
2162
|
if (level !== 'spawn') {
|
|
1870
2163
|
message = message
|
|
1871
2164
|
.split(' ')
|
|
1872
2165
|
.map((word) => {
|
|
2166
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1873
2167
|
if (word.length > maxContinuousLength) {
|
|
1874
2168
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1875
2169
|
}
|
|
@@ -1877,14 +2171,34 @@ export class Frontend extends EventEmitter {
|
|
|
1877
2171
|
})
|
|
1878
2172
|
.join(' ');
|
|
1879
2173
|
}
|
|
2174
|
+
// Send the message to all connected clients
|
|
1880
2175
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
1881
2176
|
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
2179
|
+
*
|
|
2180
|
+
* @param {string} changed - The changed value.
|
|
2181
|
+
* @param {Record<string, unknown>} params - Additional parameters to send with the message.
|
|
2182
|
+
* possible values for changed:
|
|
2183
|
+
* - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
|
|
2184
|
+
* - 'plugins'
|
|
2185
|
+
* - 'devices'
|
|
2186
|
+
* - 'matter' with param 'matter' (QRDiv component)
|
|
2187
|
+
* @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
|
|
2188
|
+
*/
|
|
1882
2189
|
wssSendRefreshRequired(changed, params) {
|
|
1883
2190
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1884
2191
|
return;
|
|
1885
2192
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
2193
|
+
// Send the message to all connected clients
|
|
1886
2194
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
|
|
1887
2195
|
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
2198
|
+
*
|
|
2199
|
+
* @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
|
|
2200
|
+
* @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
|
|
2201
|
+
*/
|
|
1888
2202
|
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
1889
2203
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1890
2204
|
return;
|
|
@@ -1893,8 +2207,14 @@ export class Frontend extends EventEmitter {
|
|
|
1893
2207
|
this.matterbridge.fixedRestartRequired = fixed;
|
|
1894
2208
|
if (snackbar === true)
|
|
1895
2209
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
2210
|
+
// Send the message to all connected clients
|
|
1896
2211
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
1897
2212
|
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Sends a no need to restart WebSocket message to all connected clients.
|
|
2215
|
+
*
|
|
2216
|
+
* @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
|
|
2217
|
+
*/
|
|
1898
2218
|
wssSendRestartNotRequired(snackbar = true) {
|
|
1899
2219
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1900
2220
|
return;
|
|
@@ -1902,57 +2222,133 @@ export class Frontend extends EventEmitter {
|
|
|
1902
2222
|
this.matterbridge.restartRequired = false;
|
|
1903
2223
|
if (snackbar === true)
|
|
1904
2224
|
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
2225
|
+
// Send the message to all connected clients
|
|
1905
2226
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
1906
2227
|
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
2230
|
+
*
|
|
2231
|
+
* @param {boolean} devVersion - If true, the update is for a development version. Default is false.
|
|
2232
|
+
*/
|
|
1907
2233
|
wssSendUpdateRequired(devVersion = false) {
|
|
1908
2234
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1909
2235
|
return;
|
|
1910
2236
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1911
2237
|
this.matterbridge.updateRequired = true;
|
|
2238
|
+
// Send the message to all connected clients
|
|
1912
2239
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
1913
2240
|
}
|
|
2241
|
+
/**
|
|
2242
|
+
* Sends a cpu update message to all connected clients.
|
|
2243
|
+
*
|
|
2244
|
+
* @param {number} cpuUsage - The CPU usage percentage to send.
|
|
2245
|
+
* @param {number} processCpuUsage - The CPU usage percentage of the process to send.
|
|
2246
|
+
*/
|
|
1914
2247
|
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
1915
2248
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1916
2249
|
return;
|
|
1917
2250
|
if (hasParameter('debug'))
|
|
1918
2251
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
2252
|
+
// Send the message to all connected clients
|
|
1919
2253
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'cpu_update', success: true, response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 } });
|
|
1920
2254
|
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Sends a memory update message to all connected clients.
|
|
2257
|
+
*
|
|
2258
|
+
* @param {string} totalMemory - The total memory in bytes.
|
|
2259
|
+
* @param {string} freeMemory - The free memory in bytes.
|
|
2260
|
+
* @param {string} rss - The resident set size in bytes.
|
|
2261
|
+
* @param {string} heapTotal - The total heap memory in bytes.
|
|
2262
|
+
* @param {string} heapUsed - The used heap memory in bytes.
|
|
2263
|
+
* @param {string} external - The external memory in bytes.
|
|
2264
|
+
* @param {string} arrayBuffers - The array buffers memory in bytes.
|
|
2265
|
+
*/
|
|
1921
2266
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1922
2267
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1923
2268
|
return;
|
|
1924
2269
|
if (hasParameter('debug'))
|
|
1925
2270
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
2271
|
+
// Send the message to all connected clients
|
|
1926
2272
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
|
|
1927
2273
|
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Sends an uptime update message to all connected clients.
|
|
2276
|
+
*
|
|
2277
|
+
* @param {string} systemUptime - The system uptime in a human-readable format.
|
|
2278
|
+
* @param {string} processUptime - The process uptime in a human-readable format.
|
|
2279
|
+
*/
|
|
1928
2280
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1929
2281
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1930
2282
|
return;
|
|
1931
2283
|
if (hasParameter('debug'))
|
|
1932
2284
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
2285
|
+
// Send the message to all connected clients
|
|
1933
2286
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
1934
2287
|
}
|
|
2288
|
+
/**
|
|
2289
|
+
* Sends an open snackbar message to all connected clients.
|
|
2290
|
+
*
|
|
2291
|
+
* @param {string} message - The message to send.
|
|
2292
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
|
|
2293
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
|
|
2294
|
+
* possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
|
|
2295
|
+
*
|
|
2296
|
+
* @remarks
|
|
2297
|
+
* If timeout is 0, the snackbar message will be displayed until closed by the user.
|
|
2298
|
+
*/
|
|
1935
2299
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1936
2300
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1937
2301
|
return;
|
|
1938
2302
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
2303
|
+
// Send the message to all connected clients
|
|
1939
2304
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
1940
2305
|
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Sends a close snackbar message to all connected clients.
|
|
2308
|
+
* It will close the snackbar message with the same message and timeout = 0.
|
|
2309
|
+
*
|
|
2310
|
+
* @param {string} message - The message to send.
|
|
2311
|
+
*/
|
|
1941
2312
|
wssSendCloseSnackbarMessage(message) {
|
|
1942
2313
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1943
2314
|
return;
|
|
1944
2315
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
2316
|
+
// Send the message to all connected clients
|
|
1945
2317
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
1946
2318
|
}
|
|
2319
|
+
/**
|
|
2320
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
2321
|
+
*
|
|
2322
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
2323
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
2324
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
2325
|
+
* @param {EndpointNumber} number - The endpoint number where the attribute belongs.
|
|
2326
|
+
* @param {string} id - The endpoint id where the attribute belongs.
|
|
2327
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
2328
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
2329
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
2330
|
+
*
|
|
2331
|
+
* @remarks
|
|
2332
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
2333
|
+
* with the updated attribute information.
|
|
2334
|
+
*/
|
|
1947
2335
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
1948
2336
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1949
2337
|
return;
|
|
1950
2338
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
2339
|
+
// Send the message to all connected clients
|
|
1951
2340
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
|
|
1952
2341
|
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Sends a message to all connected clients.
|
|
2344
|
+
* This is an helper function to send a broadcast message to all connected clients.
|
|
2345
|
+
*
|
|
2346
|
+
* @param {WsMessageBroadcast} msg - The message to send.
|
|
2347
|
+
*/
|
|
1953
2348
|
wssBroadcastMessage(msg) {
|
|
1954
2349
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1955
2350
|
return;
|
|
2351
|
+
// Send the message to all connected clients
|
|
1956
2352
|
const stringifiedMsg = JSON.stringify(msg);
|
|
1957
2353
|
if (msg.method !== 'log')
|
|
1958
2354
|
this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
|
|
@@ -1963,3 +2359,4 @@ export class Frontend extends EventEmitter {
|
|
|
1963
2359
|
});
|
|
1964
2360
|
}
|
|
1965
2361
|
}
|
|
2362
|
+
//# sourceMappingURL=frontend.js.map
|