matterbridge 3.3.8-dev-20251115-ca5ff21 → 3.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/broadcastServer.d.ts +115 -0
- package/dist/broadcastServer.d.ts.map +1 -0
- package/dist/broadcastServer.js +93 -1
- package/dist/broadcastServer.js.map +1 -0
- package/dist/broadcastServerTypes.d.ts +806 -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 +128 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +105 -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 +43 -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 +92 -11
- 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 +111 -20
- 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 +238 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +454 -35
- 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 +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/jestutils/export.d.ts +2 -0
- package/dist/jestutils/export.d.ts.map +1 -0
- package/dist/jestutils/export.js +1 -0
- package/dist/jestutils/export.js.map +1 -0
- package/dist/jestutils/jestHelpers.d.ts +250 -0
- package/dist/jestutils/jestHelpers.d.ts.map +1 -0
- package/dist/jestutils/jestHelpers.js +275 -4
- package/dist/jestutils/jestHelpers.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 +469 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +788 -46
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +41 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +38 -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 +77 -14
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +698 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +635 -14
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +41 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +38 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +1490 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +1432 -53
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +787 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +488 -25
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgeEndpointTypes.d.ts +197 -0
- package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
- package/dist/matterbridgeEndpointTypes.js +25 -0
- package/dist/matterbridgeEndpointTypes.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +415 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +354 -1
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +239 -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 +371 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +339 -4
- 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/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';
|
|
@@ -37,7 +63,7 @@ export class Frontend extends EventEmitter {
|
|
|
37
63
|
constructor(matterbridge) {
|
|
38
64
|
super();
|
|
39
65
|
this.matterbridge = matterbridge;
|
|
40
|
-
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4
|
|
66
|
+
this.log = new AnsiLogger({ logName: 'Frontend', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
41
67
|
this.log.logNameColor = '\x1b[38;5;97m';
|
|
42
68
|
this.server = new BroadcastServer('frontend', this.log);
|
|
43
69
|
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
@@ -136,23 +162,53 @@ export class Frontend extends EventEmitter {
|
|
|
136
162
|
this.port = port;
|
|
137
163
|
this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
|
|
138
164
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
165
|
+
// Initialize multer with the upload directory
|
|
139
166
|
const multer = await import('multer');
|
|
140
|
-
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
167
|
+
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
|
|
141
168
|
const upload = multer.default({ dest: uploadDir });
|
|
169
|
+
// Create the express app that serves the frontend
|
|
142
170
|
const express = await import('express');
|
|
143
171
|
this.expressApp = express.default();
|
|
172
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
173
|
+
/*
|
|
174
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
175
|
+
for (const method of methods) {
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
180
|
+
try {
|
|
181
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
182
|
+
return original(path, ...rest);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
*/
|
|
190
|
+
// Log all requests to the server for debugging
|
|
191
|
+
/*
|
|
192
|
+
this.expressApp.use((req, res, next) => {
|
|
193
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
194
|
+
next();
|
|
195
|
+
});
|
|
196
|
+
*/
|
|
197
|
+
// Serve static files from 'frontend/build' directory
|
|
144
198
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend', 'build')));
|
|
199
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
145
200
|
this.log.debug(`Creating WebSocketServer...`);
|
|
146
201
|
const ws = await import('ws');
|
|
147
202
|
this.webSocketServer = new ws.WebSocketServer({ noServer: true });
|
|
148
203
|
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
149
204
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
150
205
|
const clientIp = request.socket.remoteAddress;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
206
|
+
// Set the global logger callback for the WebSocketServer
|
|
207
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
208
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
209
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
210
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
211
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
156
212
|
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
157
213
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
158
214
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -174,16 +230,25 @@ export class Frontend extends EventEmitter {
|
|
|
174
230
|
}
|
|
175
231
|
});
|
|
176
232
|
ws.on('error', (error) => {
|
|
233
|
+
// istanbul ignore next
|
|
177
234
|
this.log.error(`WebSocket client error: ${error}`);
|
|
178
235
|
});
|
|
179
236
|
});
|
|
180
237
|
this.webSocketServer.on('close', () => {
|
|
181
238
|
this.log.debug(`WebSocketServer closed`);
|
|
182
239
|
});
|
|
240
|
+
/* With { noServer: true } it never fires
|
|
241
|
+
this.webSocketServer.on('listening', () => {
|
|
242
|
+
this.log.info(`The WebSocketServer is listening`);
|
|
243
|
+
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
244
|
+
});
|
|
245
|
+
*/
|
|
246
|
+
// istanbul ignore next
|
|
183
247
|
this.webSocketServer.on('error', (ws, error) => {
|
|
184
248
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
185
249
|
});
|
|
186
250
|
if (!hasParameter('ssl')) {
|
|
251
|
+
// Create an HTTP server and attach the express app
|
|
187
252
|
const http = await import('node:http');
|
|
188
253
|
try {
|
|
189
254
|
this.log.debug(`Creating HTTP server...`);
|
|
@@ -194,6 +259,7 @@ export class Frontend extends EventEmitter {
|
|
|
194
259
|
this.emit('server_error', error);
|
|
195
260
|
return;
|
|
196
261
|
}
|
|
262
|
+
// Listen on the specified port
|
|
197
263
|
if (hasParameter('ingress')) {
|
|
198
264
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
199
265
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -213,23 +279,29 @@ export class Frontend extends EventEmitter {
|
|
|
213
279
|
}
|
|
214
280
|
this.httpServer.on('upgrade', async (req, socket, head) => {
|
|
215
281
|
try {
|
|
282
|
+
// Only proceed for real WebSocket upgrades
|
|
283
|
+
// istanbul ignore next cause is only a safety check
|
|
216
284
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
217
285
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
218
286
|
return socket.destroy();
|
|
219
287
|
}
|
|
288
|
+
// Build a URL so we can read ?password=...
|
|
220
289
|
const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
|
|
290
|
+
// Validate WebSocket password
|
|
221
291
|
const password = url.searchParams.get('password') ?? '';
|
|
222
292
|
if (password !== this.storedPassword) {
|
|
223
293
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
224
294
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
225
295
|
return socket.destroy();
|
|
226
296
|
}
|
|
297
|
+
// Complete the WebSocket handshake
|
|
227
298
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
228
299
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
229
300
|
this.webSocketServer?.emit('connection', ws, req);
|
|
230
301
|
});
|
|
231
302
|
}
|
|
232
303
|
catch (err) {
|
|
304
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
233
305
|
{
|
|
234
306
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
235
307
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -252,6 +324,7 @@ export class Frontend extends EventEmitter {
|
|
|
252
324
|
});
|
|
253
325
|
}
|
|
254
326
|
else {
|
|
327
|
+
// SSL is enabled, load the certificate and the private key
|
|
255
328
|
let cert;
|
|
256
329
|
let key;
|
|
257
330
|
let ca;
|
|
@@ -261,6 +334,7 @@ export class Frontend extends EventEmitter {
|
|
|
261
334
|
let httpsServerOptions = {};
|
|
262
335
|
const fs = await import('node:fs');
|
|
263
336
|
if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
|
|
337
|
+
// Load the p12 certificate and the passphrase
|
|
264
338
|
try {
|
|
265
339
|
pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
|
|
266
340
|
this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
|
|
@@ -272,7 +346,7 @@ export class Frontend extends EventEmitter {
|
|
|
272
346
|
}
|
|
273
347
|
try {
|
|
274
348
|
passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
|
|
275
|
-
passphrase = passphrase.trim();
|
|
349
|
+
passphrase = passphrase.trim(); // Ensure no extra characters
|
|
276
350
|
this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
|
|
277
351
|
}
|
|
278
352
|
catch (error) {
|
|
@@ -283,6 +357,7 @@ export class Frontend extends EventEmitter {
|
|
|
283
357
|
httpsServerOptions = { pfx, passphrase };
|
|
284
358
|
}
|
|
285
359
|
else {
|
|
360
|
+
// 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.
|
|
286
361
|
try {
|
|
287
362
|
cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
|
|
288
363
|
this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
|
|
@@ -312,9 +387,10 @@ export class Frontend extends EventEmitter {
|
|
|
312
387
|
httpsServerOptions = { cert: fullChain ?? cert, key, ca };
|
|
313
388
|
}
|
|
314
389
|
if (hasParameter('mtls')) {
|
|
315
|
-
httpsServerOptions.requestCert = true;
|
|
316
|
-
httpsServerOptions.rejectUnauthorized = true;
|
|
390
|
+
httpsServerOptions.requestCert = true; // Request client certificate
|
|
391
|
+
httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
|
|
317
392
|
}
|
|
393
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
318
394
|
const https = await import('node:https');
|
|
319
395
|
try {
|
|
320
396
|
this.log.debug(`Creating HTTPS server...`);
|
|
@@ -325,6 +401,7 @@ export class Frontend extends EventEmitter {
|
|
|
325
401
|
this.emit('server_error', error);
|
|
326
402
|
return;
|
|
327
403
|
}
|
|
404
|
+
// Listen on the specified port
|
|
328
405
|
if (hasParameter('ingress')) {
|
|
329
406
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
330
407
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -344,23 +421,29 @@ export class Frontend extends EventEmitter {
|
|
|
344
421
|
}
|
|
345
422
|
this.httpsServer.on('upgrade', async (req, socket, head) => {
|
|
346
423
|
try {
|
|
424
|
+
// Only proceed for real WebSocket upgrades
|
|
425
|
+
// istanbul ignore next cause is only a safety check
|
|
347
426
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
348
427
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
349
428
|
return socket.destroy();
|
|
350
429
|
}
|
|
430
|
+
// Build a URL so we can read ?password=...
|
|
351
431
|
const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
|
|
432
|
+
// Validate WebSocket password
|
|
352
433
|
const password = url.searchParams.get('password') ?? '';
|
|
353
434
|
if (password !== this.storedPassword) {
|
|
354
435
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
355
436
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
356
437
|
return socket.destroy();
|
|
357
438
|
}
|
|
439
|
+
// Complete the WebSocket handshake
|
|
358
440
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
359
441
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
360
442
|
this.webSocketServer?.emit('connection', ws, req);
|
|
361
443
|
});
|
|
362
444
|
}
|
|
363
445
|
catch (err) {
|
|
446
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
364
447
|
{
|
|
365
448
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
366
449
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -382,6 +465,7 @@ export class Frontend extends EventEmitter {
|
|
|
382
465
|
return;
|
|
383
466
|
});
|
|
384
467
|
}
|
|
468
|
+
// Subscribe to cli events
|
|
385
469
|
cliEmitter.removeAllListeners();
|
|
386
470
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
387
471
|
this.wssSendUptimeUpdate(systemUptime, processUptime);
|
|
@@ -392,6 +476,8 @@ export class Frontend extends EventEmitter {
|
|
|
392
476
|
cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
|
|
393
477
|
this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
|
|
394
478
|
});
|
|
479
|
+
// Endpoint to validate login code
|
|
480
|
+
// curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
|
|
395
481
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
396
482
|
const { password } = req.body;
|
|
397
483
|
this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
|
|
@@ -404,17 +490,20 @@ export class Frontend extends EventEmitter {
|
|
|
404
490
|
res.json({ valid: false });
|
|
405
491
|
}
|
|
406
492
|
});
|
|
493
|
+
// Endpoint to provide health check for docker
|
|
407
494
|
this.expressApp.get('/health', (req, res) => {
|
|
408
495
|
this.log.debug('Express received /health');
|
|
409
496
|
const healthStatus = {
|
|
410
|
-
status: 'ok',
|
|
411
|
-
uptime: process.uptime(),
|
|
412
|
-
timestamp: new Date().toISOString(),
|
|
497
|
+
status: 'ok', // Indicate service is healthy
|
|
498
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
499
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
413
500
|
};
|
|
414
501
|
res.status(200).json(healthStatus);
|
|
415
502
|
});
|
|
503
|
+
// Endpoint to provide memory usage details
|
|
416
504
|
this.expressApp.get('/memory', async (req, res) => {
|
|
417
505
|
this.log.debug('Express received /memory');
|
|
506
|
+
// Memory usage from process
|
|
418
507
|
const memoryUsageRaw = process.memoryUsage();
|
|
419
508
|
const memoryUsage = {
|
|
420
509
|
rss: formatBytes(memoryUsageRaw.rss),
|
|
@@ -423,10 +512,13 @@ export class Frontend extends EventEmitter {
|
|
|
423
512
|
external: formatBytes(memoryUsageRaw.external),
|
|
424
513
|
arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
|
|
425
514
|
};
|
|
515
|
+
// V8 heap statistics
|
|
426
516
|
const { default: v8 } = await import('node:v8');
|
|
427
517
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
428
518
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
519
|
+
// Format heapStats
|
|
429
520
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
|
|
521
|
+
// Format heapSpaces
|
|
430
522
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
431
523
|
...space,
|
|
432
524
|
space_size: formatBytes(space.space_size),
|
|
@@ -445,18 +537,22 @@ export class Frontend extends EventEmitter {
|
|
|
445
537
|
};
|
|
446
538
|
res.status(200).json(memoryReport);
|
|
447
539
|
});
|
|
540
|
+
// Endpoint to provide settings
|
|
448
541
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
449
542
|
this.log.debug('The frontend sent /api/settings');
|
|
450
543
|
res.json(await this.getApiSettings());
|
|
451
544
|
});
|
|
545
|
+
// Endpoint to provide plugins
|
|
452
546
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
453
547
|
this.log.debug('The frontend sent /api/plugins');
|
|
454
548
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
|
|
455
549
|
});
|
|
550
|
+
// Endpoint to provide devices
|
|
456
551
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
457
552
|
this.log.debug('The frontend sent /api/devices');
|
|
458
553
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
|
|
459
554
|
});
|
|
555
|
+
// Endpoint to view the matterbridge log
|
|
460
556
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
461
557
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
462
558
|
try {
|
|
@@ -470,6 +566,7 @@ export class Frontend extends EventEmitter {
|
|
|
470
566
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
471
567
|
}
|
|
472
568
|
});
|
|
569
|
+
// Endpoint to view the matter.js log
|
|
473
570
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
474
571
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
475
572
|
try {
|
|
@@ -483,6 +580,7 @@ export class Frontend extends EventEmitter {
|
|
|
483
580
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
484
581
|
}
|
|
485
582
|
});
|
|
583
|
+
// Endpoint to view the diagnostic.log
|
|
486
584
|
this.expressApp.get('/api/view-diagnostic', async (req, res) => {
|
|
487
585
|
this.log.debug('The frontend sent /api/view-diagnostic');
|
|
488
586
|
await this.generateDiagnostic();
|
|
@@ -493,10 +591,13 @@ export class Frontend extends EventEmitter {
|
|
|
493
591
|
res.send(data.slice(29));
|
|
494
592
|
}
|
|
495
593
|
catch (error) {
|
|
594
|
+
// istanbul ignore next
|
|
496
595
|
this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
596
|
+
// istanbul ignore next
|
|
497
597
|
res.status(500).send('Error reading diagnostic log file.');
|
|
498
598
|
}
|
|
499
599
|
});
|
|
600
|
+
// Endpoint to download the diagnostic.log
|
|
500
601
|
this.expressApp.get('/api/download-diagnostic', async (req, res) => {
|
|
501
602
|
this.log.debug(`The frontend sent /api/download-diagnostic`);
|
|
502
603
|
await this.generateDiagnostic();
|
|
@@ -507,16 +608,19 @@ export class Frontend extends EventEmitter {
|
|
|
507
608
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
|
|
508
609
|
}
|
|
509
610
|
catch (error) {
|
|
611
|
+
// istanbul ignore next
|
|
510
612
|
this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
|
|
511
613
|
}
|
|
512
614
|
res.type('text/plain');
|
|
513
615
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
|
|
616
|
+
/* istanbul ignore if */
|
|
514
617
|
if (error) {
|
|
515
618
|
this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
516
619
|
res.status(500).send('Error downloading the diagnostic log file');
|
|
517
620
|
}
|
|
518
621
|
});
|
|
519
622
|
});
|
|
623
|
+
// Endpoint to view the history.html
|
|
520
624
|
this.expressApp.get('/api/viewhistory', async (req, res) => {
|
|
521
625
|
this.log.debug('The frontend sent /api/viewhistory');
|
|
522
626
|
try {
|
|
@@ -530,6 +634,7 @@ export class Frontend extends EventEmitter {
|
|
|
530
634
|
res.status(500).send('Error reading history file.');
|
|
531
635
|
}
|
|
532
636
|
});
|
|
637
|
+
// Endpoint to download the history.html
|
|
533
638
|
this.expressApp.get('/api/downloadhistory', async (req, res) => {
|
|
534
639
|
this.log.debug(`The frontend sent /api/downloadhistory`);
|
|
535
640
|
try {
|
|
@@ -539,6 +644,7 @@ export class Frontend extends EventEmitter {
|
|
|
539
644
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
|
|
540
645
|
res.type('text/plain');
|
|
541
646
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
|
|
647
|
+
/* istanbul ignore if */
|
|
542
648
|
if (error) {
|
|
543
649
|
this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
544
650
|
res.status(500).send('Error downloading history file');
|
|
@@ -550,6 +656,7 @@ export class Frontend extends EventEmitter {
|
|
|
550
656
|
res.status(500).send('Error reading history file.');
|
|
551
657
|
}
|
|
552
658
|
});
|
|
659
|
+
// Endpoint to view the shelly log
|
|
553
660
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
554
661
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
555
662
|
try {
|
|
@@ -563,6 +670,7 @@ export class Frontend extends EventEmitter {
|
|
|
563
670
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
564
671
|
}
|
|
565
672
|
});
|
|
673
|
+
// Endpoint to download the matterbridge log
|
|
566
674
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
567
675
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
568
676
|
const fs = await import('node:fs');
|
|
@@ -577,12 +685,14 @@ export class Frontend extends EventEmitter {
|
|
|
577
685
|
}
|
|
578
686
|
res.type('text/plain');
|
|
579
687
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
|
|
688
|
+
/* istanbul ignore if */
|
|
580
689
|
if (error) {
|
|
581
690
|
this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
582
691
|
res.status(500).send('Error downloading the matterbridge log file');
|
|
583
692
|
}
|
|
584
693
|
});
|
|
585
694
|
});
|
|
695
|
+
// Endpoint to download the matter log
|
|
586
696
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
587
697
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
588
698
|
const fs = await import('node:fs');
|
|
@@ -597,12 +707,14 @@ export class Frontend extends EventEmitter {
|
|
|
597
707
|
}
|
|
598
708
|
res.type('text/plain');
|
|
599
709
|
res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
|
|
710
|
+
/* istanbul ignore if */
|
|
600
711
|
if (error) {
|
|
601
712
|
this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
602
713
|
res.status(500).send('Error downloading the matter log file');
|
|
603
714
|
}
|
|
604
715
|
});
|
|
605
716
|
});
|
|
717
|
+
// Endpoint to download the shelly log
|
|
606
718
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
607
719
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
608
720
|
const fs = await import('node:fs');
|
|
@@ -617,75 +729,91 @@ export class Frontend extends EventEmitter {
|
|
|
617
729
|
}
|
|
618
730
|
res.type('text/plain');
|
|
619
731
|
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
732
|
+
/* istanbul ignore if */
|
|
620
733
|
if (error) {
|
|
621
734
|
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
622
735
|
res.status(500).send('Error downloading Shelly system log file');
|
|
623
736
|
}
|
|
624
737
|
});
|
|
625
738
|
});
|
|
739
|
+
// Endpoint to download the matterbridge storage directory
|
|
626
740
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
627
741
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
628
742
|
await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
|
|
629
743
|
res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
|
|
744
|
+
/* istanbul ignore if */
|
|
630
745
|
if (error) {
|
|
631
746
|
this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
|
|
632
747
|
res.status(500).send('Error downloading the matterbridge storage file');
|
|
633
748
|
}
|
|
634
749
|
});
|
|
635
750
|
});
|
|
751
|
+
// Endpoint to download the matter storage file
|
|
636
752
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
637
753
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
638
754
|
await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
639
755
|
res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
|
|
756
|
+
/* istanbul ignore if */
|
|
640
757
|
if (error) {
|
|
641
758
|
this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
|
|
642
759
|
res.status(500).send('Error downloading the matter storage zip file');
|
|
643
760
|
}
|
|
644
761
|
});
|
|
645
762
|
});
|
|
763
|
+
// Endpoint to download the matterbridge plugin directory
|
|
646
764
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
647
765
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
648
766
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
649
767
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
|
|
768
|
+
/* istanbul ignore if */
|
|
650
769
|
if (error) {
|
|
651
770
|
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
652
771
|
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
653
772
|
}
|
|
654
773
|
});
|
|
655
774
|
});
|
|
775
|
+
// Endpoint to download the matterbridge plugin config files
|
|
656
776
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
657
777
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
658
778
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
659
779
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
780
|
+
/* istanbul ignore if */
|
|
660
781
|
if (error) {
|
|
661
782
|
this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
|
|
662
783
|
res.status(500).send('Error downloading the matterbridge plugin config file');
|
|
663
784
|
}
|
|
664
785
|
});
|
|
665
786
|
});
|
|
787
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
666
788
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
667
789
|
this.log.debug('The frontend sent /api/download-backup');
|
|
668
790
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
791
|
+
/* istanbul ignore if */
|
|
669
792
|
if (error) {
|
|
670
793
|
this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
671
794
|
res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
672
795
|
}
|
|
673
796
|
});
|
|
674
797
|
});
|
|
798
|
+
// Endpoint to upload a package
|
|
675
799
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
676
800
|
const { filename } = req.body;
|
|
677
801
|
const file = req.file;
|
|
802
|
+
/* istanbul ignore if */
|
|
678
803
|
if (!file || !filename) {
|
|
679
804
|
this.log.error(`uploadpackage: invalid request: file and filename are required`);
|
|
680
805
|
res.status(400).send('Invalid request: file and filename are required');
|
|
681
806
|
return;
|
|
682
807
|
}
|
|
683
808
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
809
|
+
// Define the path where the plugin file will be saved
|
|
684
810
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
685
811
|
try {
|
|
812
|
+
// Move the uploaded file to the specified path
|
|
686
813
|
const fs = await import('node:fs');
|
|
687
814
|
await fs.promises.rename(file.path, filePath);
|
|
688
815
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
816
|
+
// Install the plugin package
|
|
689
817
|
if (filename.endsWith('.tgz')) {
|
|
690
818
|
const { spawnCommand } = await import('./utils/spawn.js');
|
|
691
819
|
await spawnCommand(this.matterbridge, 'npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename);
|
|
@@ -705,6 +833,7 @@ export class Frontend extends EventEmitter {
|
|
|
705
833
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
706
834
|
}
|
|
707
835
|
});
|
|
836
|
+
// Fallback for routing (must be the last route)
|
|
708
837
|
this.expressApp.use((req, res) => {
|
|
709
838
|
const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
|
|
710
839
|
this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
|
|
@@ -715,13 +844,16 @@ export class Frontend extends EventEmitter {
|
|
|
715
844
|
async stop() {
|
|
716
845
|
this.log.debug('Stopping the frontend...');
|
|
717
846
|
const ws = await import('ws');
|
|
847
|
+
// Remove listeners from the express app
|
|
718
848
|
if (this.expressApp) {
|
|
719
849
|
this.expressApp.removeAllListeners();
|
|
720
850
|
this.expressApp = undefined;
|
|
721
851
|
this.log.debug('Frontend app closed successfully');
|
|
722
852
|
}
|
|
853
|
+
// Close the WebSocket server
|
|
723
854
|
if (this.webSocketServer) {
|
|
724
855
|
this.log.debug('Closing WebSocket server...');
|
|
856
|
+
// Close all active connections
|
|
725
857
|
this.webSocketServer.clients.forEach((client) => {
|
|
726
858
|
if (client.readyState === ws.WebSocket.OPEN) {
|
|
727
859
|
client.close();
|
|
@@ -730,6 +862,7 @@ export class Frontend extends EventEmitter {
|
|
|
730
862
|
await withTimeout(new Promise((resolve) => {
|
|
731
863
|
this.webSocketServer?.close((error) => {
|
|
732
864
|
if (error) {
|
|
865
|
+
// istanbul ignore next
|
|
733
866
|
this.log.error(`Error closing WebSocket server: ${error}`);
|
|
734
867
|
}
|
|
735
868
|
else {
|
|
@@ -742,8 +875,27 @@ export class Frontend extends EventEmitter {
|
|
|
742
875
|
this.webSocketServer.removeAllListeners();
|
|
743
876
|
this.webSocketServer = undefined;
|
|
744
877
|
}
|
|
878
|
+
// Close the http server
|
|
745
879
|
if (this.httpServer) {
|
|
746
880
|
this.log.debug('Closing http server...');
|
|
881
|
+
/*
|
|
882
|
+
await withTimeout(
|
|
883
|
+
new Promise<void>((resolve) => {
|
|
884
|
+
this.httpServer?.close((error) => {
|
|
885
|
+
if (error) {
|
|
886
|
+
// istanbul ignore next
|
|
887
|
+
this.log.error(`Error closing http server: ${error}`);
|
|
888
|
+
} else {
|
|
889
|
+
this.log.debug('Http server closed successfully');
|
|
890
|
+
this.emit('server_stopped');
|
|
891
|
+
}
|
|
892
|
+
resolve();
|
|
893
|
+
});
|
|
894
|
+
}),
|
|
895
|
+
5000,
|
|
896
|
+
false,
|
|
897
|
+
);
|
|
898
|
+
*/
|
|
747
899
|
this.httpServer.close();
|
|
748
900
|
this.log.debug('Http server closed successfully');
|
|
749
901
|
this.listening = false;
|
|
@@ -752,8 +904,27 @@ export class Frontend extends EventEmitter {
|
|
|
752
904
|
this.httpServer = undefined;
|
|
753
905
|
this.log.debug('Frontend http server closed successfully');
|
|
754
906
|
}
|
|
907
|
+
// Close the https server
|
|
755
908
|
if (this.httpsServer) {
|
|
756
909
|
this.log.debug('Closing https server...');
|
|
910
|
+
/*
|
|
911
|
+
await withTimeout(
|
|
912
|
+
new Promise<void>((resolve) => {
|
|
913
|
+
this.httpsServer?.close((error) => {
|
|
914
|
+
if (error) {
|
|
915
|
+
// istanbul ignore next
|
|
916
|
+
this.log.error(`Error closing https server: ${error}`);
|
|
917
|
+
} else {
|
|
918
|
+
this.log.debug('Https server closed successfully');
|
|
919
|
+
this.emit('server_stopped');
|
|
920
|
+
}
|
|
921
|
+
resolve();
|
|
922
|
+
});
|
|
923
|
+
}),
|
|
924
|
+
5000,
|
|
925
|
+
false,
|
|
926
|
+
);
|
|
927
|
+
*/
|
|
757
928
|
this.httpsServer.close();
|
|
758
929
|
this.log.debug('Https server closed successfully');
|
|
759
930
|
this.listening = false;
|
|
@@ -764,7 +935,13 @@ export class Frontend extends EventEmitter {
|
|
|
764
935
|
}
|
|
765
936
|
this.log.debug('Frontend stopped successfully');
|
|
766
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Retrieves the api settings data.
|
|
940
|
+
*
|
|
941
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
942
|
+
*/
|
|
767
943
|
async getApiSettings() {
|
|
944
|
+
// Update the variable system information properties
|
|
768
945
|
this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
769
946
|
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
770
947
|
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
@@ -774,6 +951,7 @@ export class Frontend extends EventEmitter {
|
|
|
774
951
|
this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
|
|
775
952
|
this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
776
953
|
this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
954
|
+
// Create the matterbridge information
|
|
777
955
|
const info = {
|
|
778
956
|
homeDirectory: this.matterbridge.homeDirectory,
|
|
779
957
|
rootDirectory: this.matterbridge.rootDirectory,
|
|
@@ -809,9 +987,15 @@ export class Frontend extends EventEmitter {
|
|
|
809
987
|
};
|
|
810
988
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
|
|
811
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* Retrieves the reachable attribute.
|
|
992
|
+
*
|
|
993
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
|
|
994
|
+
* @returns {boolean} The reachable attribute.
|
|
995
|
+
*/
|
|
812
996
|
getReachability(device) {
|
|
813
997
|
if (this.matterbridge.hasCleanupStarted)
|
|
814
|
-
return false;
|
|
998
|
+
return false; // Skip if cleanup has started
|
|
815
999
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
816
1000
|
return false;
|
|
817
1001
|
if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
|
|
@@ -822,9 +1006,15 @@ export class Frontend extends EventEmitter {
|
|
|
822
1006
|
return true;
|
|
823
1007
|
return false;
|
|
824
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Retrieves the power source attribute.
|
|
1011
|
+
*
|
|
1012
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
|
|
1013
|
+
* @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
|
|
1014
|
+
*/
|
|
825
1015
|
getPowerSource(endpoint) {
|
|
826
1016
|
if (this.matterbridge.hasCleanupStarted)
|
|
827
|
-
return;
|
|
1017
|
+
return; // Skip if cleanup has started
|
|
828
1018
|
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
829
1019
|
return undefined;
|
|
830
1020
|
const powerSource = (device) => {
|
|
@@ -839,16 +1029,25 @@ export class Frontend extends EventEmitter {
|
|
|
839
1029
|
}
|
|
840
1030
|
return;
|
|
841
1031
|
};
|
|
1032
|
+
// Root endpoint
|
|
842
1033
|
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
843
1034
|
return powerSource(endpoint);
|
|
1035
|
+
// Child endpoints
|
|
844
1036
|
for (const child of endpoint.getChildEndpoints()) {
|
|
845
1037
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
846
1038
|
return powerSource(child);
|
|
847
1039
|
}
|
|
848
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Retrieves the cluster text description from a given device.
|
|
1043
|
+
* The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
|
|
1046
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1047
|
+
*/
|
|
849
1048
|
getClusterTextFromDevice(device) {
|
|
850
1049
|
if (this.matterbridge.hasCleanupStarted)
|
|
851
|
-
return '';
|
|
1050
|
+
return ''; // Skip if cleanup has started
|
|
852
1051
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
853
1052
|
return '';
|
|
854
1053
|
const getUserLabel = (device) => {
|
|
@@ -858,6 +1057,7 @@ export class Frontend extends EventEmitter {
|
|
|
858
1057
|
if (composed)
|
|
859
1058
|
return 'Composed: ' + composed.value;
|
|
860
1059
|
}
|
|
1060
|
+
// istanbul ignore next cause is not reachable
|
|
861
1061
|
return '';
|
|
862
1062
|
};
|
|
863
1063
|
const getFixedLabel = (device) => {
|
|
@@ -867,11 +1067,13 @@ export class Frontend extends EventEmitter {
|
|
|
867
1067
|
if (composed)
|
|
868
1068
|
return 'Composed: ' + composed.value;
|
|
869
1069
|
}
|
|
1070
|
+
// istanbul ignore next cause is not reacheable
|
|
870
1071
|
return '';
|
|
871
1072
|
};
|
|
872
1073
|
let attributes = '';
|
|
873
1074
|
let supportedModes = [];
|
|
874
1075
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1076
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
875
1077
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
876
1078
|
return;
|
|
877
1079
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -961,11 +1163,17 @@ export class Frontend extends EventEmitter {
|
|
|
961
1163
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
962
1164
|
attributes += `${getUserLabel(device)} `;
|
|
963
1165
|
});
|
|
1166
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
964
1167
|
return attributes.trimStart().trimEnd();
|
|
965
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Retrieves the registered plugins sanitized for res.json().
|
|
1171
|
+
*
|
|
1172
|
+
* @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
|
|
1173
|
+
*/
|
|
966
1174
|
getPlugins() {
|
|
967
1175
|
if (this.matterbridge.hasCleanupStarted)
|
|
968
|
-
return [];
|
|
1176
|
+
return []; // Skip if cleanup has started
|
|
969
1177
|
const plugins = [];
|
|
970
1178
|
for (const plugin of this.matterbridge.plugins.array()) {
|
|
971
1179
|
plugins.push({
|
|
@@ -993,18 +1201,27 @@ export class Frontend extends EventEmitter {
|
|
|
993
1201
|
schemaJson: plugin.schemaJson,
|
|
994
1202
|
hasWhiteList: plugin.configJson?.whiteList !== undefined,
|
|
995
1203
|
hasBlackList: plugin.configJson?.blackList !== undefined,
|
|
1204
|
+
// Childbridge mode specific data
|
|
996
1205
|
matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
|
|
997
1206
|
});
|
|
998
1207
|
}
|
|
999
1208
|
return plugins;
|
|
1000
1209
|
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Retrieves the devices from Matterbridge.
|
|
1212
|
+
*
|
|
1213
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
1214
|
+
* @returns {ApiDevice[]} An array of ApiDevices for the frontend.
|
|
1215
|
+
*/
|
|
1001
1216
|
getDevices(pluginName) {
|
|
1002
1217
|
if (this.matterbridge.hasCleanupStarted)
|
|
1003
|
-
return [];
|
|
1218
|
+
return []; // Skip if cleanup has started
|
|
1004
1219
|
const devices = [];
|
|
1005
1220
|
for (const device of this.matterbridge.devices.array()) {
|
|
1221
|
+
// Filter by pluginName if provided
|
|
1006
1222
|
if (pluginName && pluginName !== device.plugin)
|
|
1007
1223
|
continue;
|
|
1224
|
+
// Check if the device has the required properties
|
|
1008
1225
|
if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
1009
1226
|
continue;
|
|
1010
1227
|
devices.push({
|
|
@@ -1024,24 +1241,39 @@ export class Frontend extends EventEmitter {
|
|
|
1024
1241
|
}
|
|
1025
1242
|
return devices;
|
|
1026
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Retrieves the clusters from a given plugin and endpoint number.
|
|
1246
|
+
*
|
|
1247
|
+
* Response for /api/clusters
|
|
1248
|
+
*
|
|
1249
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1250
|
+
* @param {number} endpointNumber - The endpoint number.
|
|
1251
|
+
* @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
|
|
1252
|
+
*/
|
|
1027
1253
|
getClusters(pluginName, endpointNumber) {
|
|
1028
1254
|
if (this.matterbridge.hasCleanupStarted)
|
|
1029
|
-
return;
|
|
1255
|
+
return; // Skip if cleanup has started
|
|
1030
1256
|
const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
|
|
1031
1257
|
if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
|
|
1032
1258
|
this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1033
1259
|
return;
|
|
1034
1260
|
}
|
|
1261
|
+
// this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
|
|
1262
|
+
// Get the device types from the main endpoint
|
|
1035
1263
|
const deviceTypes = [];
|
|
1036
1264
|
const clusters = [];
|
|
1037
1265
|
endpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1038
1266
|
deviceTypes.push(d.deviceType);
|
|
1039
1267
|
});
|
|
1268
|
+
// Get the clusters from the main endpoint
|
|
1040
1269
|
endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1041
1270
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1042
1271
|
return;
|
|
1043
1272
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1044
1273
|
return;
|
|
1274
|
+
// console.log(
|
|
1275
|
+
// `${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}`,
|
|
1276
|
+
// );
|
|
1045
1277
|
clusters.push({
|
|
1046
1278
|
endpoint: endpoint.number.toString(),
|
|
1047
1279
|
number: endpoint.number,
|
|
@@ -1055,12 +1287,19 @@ export class Frontend extends EventEmitter {
|
|
|
1055
1287
|
attributeLocalValue: attributeValue,
|
|
1056
1288
|
});
|
|
1057
1289
|
});
|
|
1290
|
+
// Get the child endpoints
|
|
1058
1291
|
const childEndpoints = endpoint.getChildEndpoints();
|
|
1292
|
+
// if (childEndpoints.length === 0) {
|
|
1293
|
+
// this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1294
|
+
// }
|
|
1059
1295
|
childEndpoints.forEach((childEndpoint) => {
|
|
1296
|
+
// istanbul ignore if cause is not reachable: should never happen but ...
|
|
1060
1297
|
if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
|
|
1061
1298
|
this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1062
1299
|
return;
|
|
1063
1300
|
}
|
|
1301
|
+
// this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
|
|
1302
|
+
// Get the device types of the child endpoint
|
|
1064
1303
|
const deviceTypes = [];
|
|
1065
1304
|
childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1066
1305
|
deviceTypes.push(d.deviceType);
|
|
@@ -1070,6 +1309,9 @@ export class Frontend extends EventEmitter {
|
|
|
1070
1309
|
return;
|
|
1071
1310
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1072
1311
|
return;
|
|
1312
|
+
// console.log(
|
|
1313
|
+
// `${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}`,
|
|
1314
|
+
// );
|
|
1073
1315
|
clusters.push({
|
|
1074
1316
|
endpoint: childEndpoint.number.toString(),
|
|
1075
1317
|
number: childEndpoint.number,
|
|
@@ -1089,6 +1331,7 @@ export class Frontend extends EventEmitter {
|
|
|
1089
1331
|
async generateDiagnostic() {
|
|
1090
1332
|
this.log.debug('Generating diagnostic...');
|
|
1091
1333
|
const serverNodes = [];
|
|
1334
|
+
// istanbul ignore else
|
|
1092
1335
|
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
1093
1336
|
if (this.matterbridge.serverNode)
|
|
1094
1337
|
serverNodes.push(this.matterbridge.serverNode);
|
|
@@ -1099,6 +1342,7 @@ export class Frontend extends EventEmitter {
|
|
|
1099
1342
|
serverNodes.push(plugin.serverNode);
|
|
1100
1343
|
}
|
|
1101
1344
|
}
|
|
1345
|
+
// istanbul ignore next
|
|
1102
1346
|
for (const device of this.matterbridge.devices.array()) {
|
|
1103
1347
|
if (device.serverNode)
|
|
1104
1348
|
serverNodes.push(device.serverNode);
|
|
@@ -1122,8 +1366,15 @@ export class Frontend extends EventEmitter {
|
|
|
1122
1366
|
values: [...serverNodes],
|
|
1123
1367
|
})));
|
|
1124
1368
|
delete Logger.destinations.diagnostic;
|
|
1125
|
-
await wait(500);
|
|
1369
|
+
await wait(500); // Wait for the log to be written
|
|
1126
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Handles incoming websocket api request messages from the Matterbridge frontend.
|
|
1373
|
+
*
|
|
1374
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1375
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1376
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1377
|
+
*/
|
|
1127
1378
|
async wsMessageHandler(client, message) {
|
|
1128
1379
|
let data;
|
|
1129
1380
|
const sendResponse = (data) => {
|
|
@@ -1143,7 +1394,7 @@ export class Frontend extends EventEmitter {
|
|
|
1143
1394
|
};
|
|
1144
1395
|
try {
|
|
1145
1396
|
data = JSON.parse(message.toString());
|
|
1146
|
-
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
|
|
1397
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
|
|
1147
1398
|
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
1148
1399
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
1149
1400
|
return;
|
|
@@ -1200,7 +1451,22 @@ export class Frontend extends EventEmitter {
|
|
|
1200
1451
|
}
|
|
1201
1452
|
this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
|
|
1202
1453
|
this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
|
|
1203
|
-
data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
|
|
1454
|
+
data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
|
|
1455
|
+
/*
|
|
1456
|
+
const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
|
|
1457
|
+
if (plugin) {
|
|
1458
|
+
this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
|
|
1459
|
+
await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
|
|
1460
|
+
this.wssSendRestartRequired();
|
|
1461
|
+
this.wssSendRefreshRequired('plugins');
|
|
1462
|
+
this.wssSendRefreshRequired('devices');
|
|
1463
|
+
this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
|
|
1464
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1465
|
+
} else {
|
|
1466
|
+
this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
|
|
1467
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
|
|
1468
|
+
}
|
|
1469
|
+
*/
|
|
1204
1470
|
const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
|
|
1205
1471
|
if (plugin) {
|
|
1206
1472
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1214,6 +1480,7 @@ export class Frontend extends EventEmitter {
|
|
|
1214
1480
|
return;
|
|
1215
1481
|
})
|
|
1216
1482
|
.catch((_error) => {
|
|
1483
|
+
//
|
|
1217
1484
|
});
|
|
1218
1485
|
}
|
|
1219
1486
|
else {
|
|
@@ -1228,6 +1495,10 @@ export class Frontend extends EventEmitter {
|
|
|
1228
1495
|
}
|
|
1229
1496
|
this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
|
|
1230
1497
|
this.log.debug(`Removing plugin ${data.params.pluginName}...`);
|
|
1498
|
+
/*
|
|
1499
|
+
await this.server.fetch({ type: 'plugins_shutdown', src: this.server.name, dst: 'plugins', params: { plugin: data.params.pluginName, reason: 'The plugin has been removed.', removeAllDevices: true } }, 5000);
|
|
1500
|
+
await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
|
|
1501
|
+
*/
|
|
1231
1502
|
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1232
1503
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
1233
1504
|
await this.matterbridge.plugins.remove(data.params.pluginName);
|
|
@@ -1263,6 +1534,7 @@ export class Frontend extends EventEmitter {
|
|
|
1263
1534
|
return;
|
|
1264
1535
|
})
|
|
1265
1536
|
.catch((_error) => {
|
|
1537
|
+
//
|
|
1266
1538
|
});
|
|
1267
1539
|
}
|
|
1268
1540
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1288,6 +1560,7 @@ export class Frontend extends EventEmitter {
|
|
|
1288
1560
|
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1289
1561
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
|
|
1290
1562
|
if (plugin.serverNode) {
|
|
1563
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1291
1564
|
await this.matterbridge.stopServerNode(plugin.serverNode);
|
|
1292
1565
|
plugin.serverNode = undefined;
|
|
1293
1566
|
}
|
|
@@ -1297,18 +1570,20 @@ export class Frontend extends EventEmitter {
|
|
|
1297
1570
|
this.matterbridge.devices.remove(device);
|
|
1298
1571
|
}
|
|
1299
1572
|
}
|
|
1573
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1300
1574
|
if (plugin.type === 'DynamicPlatform' && !plugin.locked)
|
|
1301
1575
|
await this.matterbridge.createDynamicPlugin(plugin);
|
|
1302
1576
|
await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
|
|
1303
|
-
plugin.restartRequired = false;
|
|
1577
|
+
plugin.restartRequired = false; // Reset plugin restartRequired
|
|
1304
1578
|
let needRestart = 0;
|
|
1305
1579
|
for (const plugin of this.matterbridge.plugins) {
|
|
1306
1580
|
if (plugin.restartRequired)
|
|
1307
1581
|
needRestart++;
|
|
1308
1582
|
}
|
|
1309
1583
|
if (needRestart === 0) {
|
|
1310
|
-
this.wssSendRestartNotRequired(true);
|
|
1584
|
+
this.wssSendRestartNotRequired(true); // Reset global restart required message
|
|
1311
1585
|
}
|
|
1586
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1312
1587
|
if (plugin.serverNode)
|
|
1313
1588
|
await this.matterbridge.startServerNode(plugin.serverNode);
|
|
1314
1589
|
this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
|
|
@@ -1453,6 +1728,9 @@ export class Frontend extends EventEmitter {
|
|
|
1453
1728
|
this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
|
|
1454
1729
|
}
|
|
1455
1730
|
if (data.params.advertise) {
|
|
1731
|
+
// TODO: matter.js 0.16.0
|
|
1732
|
+
// await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
|
|
1733
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1456
1734
|
const advertiser = serverNode.env.get(DeviceAdvertiser);
|
|
1457
1735
|
if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
|
|
1458
1736
|
await advertiser.advertise(true);
|
|
@@ -1578,22 +1856,22 @@ export class Frontend extends EventEmitter {
|
|
|
1578
1856
|
if (isValidString(data.params.value, 4)) {
|
|
1579
1857
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1580
1858
|
if (data.params.value === 'Debug') {
|
|
1581
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1859
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1582
1860
|
}
|
|
1583
1861
|
else if (data.params.value === 'Info') {
|
|
1584
|
-
await this.matterbridge.setLogLevel("info");
|
|
1862
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1585
1863
|
}
|
|
1586
1864
|
else if (data.params.value === 'Notice') {
|
|
1587
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1865
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1588
1866
|
}
|
|
1589
1867
|
else if (data.params.value === 'Warn') {
|
|
1590
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1868
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1591
1869
|
}
|
|
1592
1870
|
else if (data.params.value === 'Error') {
|
|
1593
|
-
await this.matterbridge.setLogLevel("error");
|
|
1871
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1594
1872
|
}
|
|
1595
1873
|
else if (data.params.value === 'Fatal') {
|
|
1596
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1874
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1597
1875
|
}
|
|
1598
1876
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1599
1877
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1604,6 +1882,7 @@ export class Frontend extends EventEmitter {
|
|
|
1604
1882
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1605
1883
|
this.matterbridge.fileLogger = data.params.value;
|
|
1606
1884
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1885
|
+
// Create the file logger for matterbridge
|
|
1607
1886
|
if (data.params.value)
|
|
1608
1887
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
|
|
1609
1888
|
else
|
|
@@ -1632,11 +1911,12 @@ export class Frontend extends EventEmitter {
|
|
|
1632
1911
|
else if (data.params.value === 'Fatal') {
|
|
1633
1912
|
Logger.level = MatterLogLevel.FATAL;
|
|
1634
1913
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1914
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
1915
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
1916
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
1917
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
1918
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
1919
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
1640
1920
|
AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
|
|
1641
1921
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
1642
1922
|
await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
|
|
@@ -1688,6 +1968,7 @@ export class Frontend extends EventEmitter {
|
|
|
1688
1968
|
}
|
|
1689
1969
|
break;
|
|
1690
1970
|
case 'setmatterport':
|
|
1971
|
+
// eslint-disable-next-line no-case-declarations
|
|
1691
1972
|
const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1692
1973
|
if (isValidNumber(port, 5540, 5600)) {
|
|
1693
1974
|
this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
|
|
@@ -1707,6 +1988,7 @@ export class Frontend extends EventEmitter {
|
|
|
1707
1988
|
}
|
|
1708
1989
|
break;
|
|
1709
1990
|
case 'setmatterdiscriminator':
|
|
1991
|
+
// eslint-disable-next-line no-case-declarations
|
|
1710
1992
|
const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1711
1993
|
if (isValidNumber(discriminator, 0, 4095)) {
|
|
1712
1994
|
this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
|
|
@@ -1726,6 +2008,7 @@ export class Frontend extends EventEmitter {
|
|
|
1726
2008
|
}
|
|
1727
2009
|
break;
|
|
1728
2010
|
case 'setmatterpasscode':
|
|
2011
|
+
// eslint-disable-next-line no-case-declarations
|
|
1729
2012
|
const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1730
2013
|
if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
|
|
1731
2014
|
this.matterbridge.passcode = passcode;
|
|
@@ -1771,15 +2054,19 @@ export class Frontend extends EventEmitter {
|
|
|
1771
2054
|
return;
|
|
1772
2055
|
}
|
|
1773
2056
|
const config = plugin.configJson;
|
|
2057
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1774
2058
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2059
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1775
2060
|
if (select === 'serial')
|
|
1776
2061
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1777
2062
|
if (select === 'name')
|
|
1778
2063
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1779
2064
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
2065
|
+
// Remove postfix from the serial if it exists
|
|
1780
2066
|
if (config.postfix) {
|
|
1781
2067
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1782
2068
|
}
|
|
2069
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1783
2070
|
if (isValidArray(config.whiteList, 1)) {
|
|
1784
2071
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1785
2072
|
config.whiteList.push(data.params.serial);
|
|
@@ -1788,6 +2075,7 @@ export class Frontend extends EventEmitter {
|
|
|
1788
2075
|
config.whiteList.push(data.params.name);
|
|
1789
2076
|
}
|
|
1790
2077
|
}
|
|
2078
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1791
2079
|
if (isValidArray(config.blackList, 1)) {
|
|
1792
2080
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1793
2081
|
config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
|
|
@@ -1815,7 +2103,9 @@ export class Frontend extends EventEmitter {
|
|
|
1815
2103
|
return;
|
|
1816
2104
|
}
|
|
1817
2105
|
const config = plugin.configJson;
|
|
2106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1818
2107
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2108
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1819
2109
|
if (select === 'serial')
|
|
1820
2110
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1821
2111
|
if (select === 'name')
|
|
@@ -1824,6 +2114,7 @@ export class Frontend extends EventEmitter {
|
|
|
1824
2114
|
if (config.postfix) {
|
|
1825
2115
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1826
2116
|
}
|
|
2117
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1827
2118
|
if (isValidArray(config.whiteList, 1)) {
|
|
1828
2119
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1829
2120
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
|
|
@@ -1832,6 +2123,7 @@ export class Frontend extends EventEmitter {
|
|
|
1832
2123
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
|
|
1833
2124
|
}
|
|
1834
2125
|
}
|
|
2126
|
+
// Add the serial to the blackList
|
|
1835
2127
|
if (isValidArray(config.blackList)) {
|
|
1836
2128
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1837
2129
|
config.blackList.push(data.params.serial);
|
|
@@ -1854,6 +2146,7 @@ export class Frontend extends EventEmitter {
|
|
|
1854
2146
|
}
|
|
1855
2147
|
}
|
|
1856
2148
|
else {
|
|
2149
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1857
2150
|
const localData = data;
|
|
1858
2151
|
this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
|
|
1859
2152
|
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
|
|
@@ -1863,23 +2156,46 @@ export class Frontend extends EventEmitter {
|
|
|
1863
2156
|
inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
|
|
1864
2157
|
}
|
|
1865
2158
|
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
2161
|
+
*
|
|
2162
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
2163
|
+
* @param {string} time - The time string of the message
|
|
2164
|
+
* @param {string} name - The logger name of the message
|
|
2165
|
+
* @param {string} message - The content of the message.
|
|
2166
|
+
*
|
|
2167
|
+
* @remarks
|
|
2168
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
2169
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
2170
|
+
* The function sends the message to all connected clients.
|
|
2171
|
+
*/
|
|
1866
2172
|
wssSendLogMessage(level, time, name, message) {
|
|
1867
2173
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1868
2174
|
return;
|
|
1869
2175
|
if (!level || !time || !name || !message)
|
|
1870
2176
|
return;
|
|
2177
|
+
// Remove ANSI escape codes from the message
|
|
2178
|
+
// eslint-disable-next-line no-control-regex
|
|
1871
2179
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
2180
|
+
// Remove leading asterisks from the message
|
|
1872
2181
|
message = message.replace(/^\*+/, '');
|
|
2182
|
+
// Replace all occurrences of \t and \n
|
|
1873
2183
|
message = message.replace(/[\t\n]/g, '');
|
|
2184
|
+
// Remove non-printable characters
|
|
2185
|
+
// eslint-disable-next-line no-control-regex
|
|
1874
2186
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
2187
|
+
// Replace all occurrences of \" with "
|
|
1875
2188
|
message = message.replace(/\\"/g, '"');
|
|
2189
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1876
2190
|
const maxContinuousLength = 100;
|
|
1877
2191
|
const keepStartLength = 20;
|
|
1878
2192
|
const keepEndLength = 20;
|
|
2193
|
+
// Split the message into words
|
|
1879
2194
|
if (level !== 'spawn') {
|
|
1880
2195
|
message = message
|
|
1881
2196
|
.split(' ')
|
|
1882
2197
|
.map((word) => {
|
|
2198
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1883
2199
|
if (word.length > maxContinuousLength) {
|
|
1884
2200
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1885
2201
|
}
|
|
@@ -1887,14 +2203,34 @@ export class Frontend extends EventEmitter {
|
|
|
1887
2203
|
})
|
|
1888
2204
|
.join(' ');
|
|
1889
2205
|
}
|
|
2206
|
+
// Send the message to all connected clients
|
|
1890
2207
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
1891
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
2211
|
+
*
|
|
2212
|
+
* @param {string} changed - The changed value.
|
|
2213
|
+
* @param {Record<string, unknown>} params - Additional parameters to send with the message.
|
|
2214
|
+
* possible values for changed:
|
|
2215
|
+
* - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
|
|
2216
|
+
* - 'plugins'
|
|
2217
|
+
* - 'devices'
|
|
2218
|
+
* - 'matter' with param 'matter' (QRDiv component)
|
|
2219
|
+
* @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
|
|
2220
|
+
*/
|
|
1892
2221
|
wssSendRefreshRequired(changed, params) {
|
|
1893
2222
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1894
2223
|
return;
|
|
1895
2224
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
2225
|
+
// Send the message to all connected clients
|
|
1896
2226
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
|
|
1897
2227
|
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
2230
|
+
*
|
|
2231
|
+
* @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
|
|
2232
|
+
* @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
|
|
2233
|
+
*/
|
|
1898
2234
|
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
1899
2235
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1900
2236
|
return;
|
|
@@ -1903,8 +2239,14 @@ export class Frontend extends EventEmitter {
|
|
|
1903
2239
|
this.matterbridge.fixedRestartRequired = fixed;
|
|
1904
2240
|
if (snackbar === true)
|
|
1905
2241
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
2242
|
+
// Send the message to all connected clients
|
|
1906
2243
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
1907
2244
|
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Sends a no need to restart WebSocket message to all connected clients.
|
|
2247
|
+
*
|
|
2248
|
+
* @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
|
|
2249
|
+
*/
|
|
1908
2250
|
wssSendRestartNotRequired(snackbar = true) {
|
|
1909
2251
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1910
2252
|
return;
|
|
@@ -1912,57 +2254,133 @@ export class Frontend extends EventEmitter {
|
|
|
1912
2254
|
this.matterbridge.restartRequired = false;
|
|
1913
2255
|
if (snackbar === true)
|
|
1914
2256
|
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
2257
|
+
// Send the message to all connected clients
|
|
1915
2258
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
1916
2259
|
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
2262
|
+
*
|
|
2263
|
+
* @param {boolean} devVersion - If true, the update is for a development version. Default is false.
|
|
2264
|
+
*/
|
|
1917
2265
|
wssSendUpdateRequired(devVersion = false) {
|
|
1918
2266
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1919
2267
|
return;
|
|
1920
2268
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1921
2269
|
this.matterbridge.updateRequired = true;
|
|
2270
|
+
// Send the message to all connected clients
|
|
1922
2271
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
1923
2272
|
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Sends a cpu update message to all connected clients.
|
|
2275
|
+
*
|
|
2276
|
+
* @param {number} cpuUsage - The CPU usage percentage to send.
|
|
2277
|
+
* @param {number} processCpuUsage - The CPU usage percentage of the process to send.
|
|
2278
|
+
*/
|
|
1924
2279
|
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
1925
2280
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1926
2281
|
return;
|
|
1927
2282
|
if (hasParameter('debug'))
|
|
1928
2283
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
2284
|
+
// Send the message to all connected clients
|
|
1929
2285
|
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 } });
|
|
1930
2286
|
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Sends a memory update message to all connected clients.
|
|
2289
|
+
*
|
|
2290
|
+
* @param {string} totalMemory - The total memory in bytes.
|
|
2291
|
+
* @param {string} freeMemory - The free memory in bytes.
|
|
2292
|
+
* @param {string} rss - The resident set size in bytes.
|
|
2293
|
+
* @param {string} heapTotal - The total heap memory in bytes.
|
|
2294
|
+
* @param {string} heapUsed - The used heap memory in bytes.
|
|
2295
|
+
* @param {string} external - The external memory in bytes.
|
|
2296
|
+
* @param {string} arrayBuffers - The array buffers memory in bytes.
|
|
2297
|
+
*/
|
|
1931
2298
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1932
2299
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1933
2300
|
return;
|
|
1934
2301
|
if (hasParameter('debug'))
|
|
1935
2302
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
2303
|
+
// Send the message to all connected clients
|
|
1936
2304
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
|
|
1937
2305
|
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Sends an uptime update message to all connected clients.
|
|
2308
|
+
*
|
|
2309
|
+
* @param {string} systemUptime - The system uptime in a human-readable format.
|
|
2310
|
+
* @param {string} processUptime - The process uptime in a human-readable format.
|
|
2311
|
+
*/
|
|
1938
2312
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1939
2313
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1940
2314
|
return;
|
|
1941
2315
|
if (hasParameter('debug'))
|
|
1942
2316
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
2317
|
+
// Send the message to all connected clients
|
|
1943
2318
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
1944
2319
|
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Sends an open snackbar message to all connected clients.
|
|
2322
|
+
*
|
|
2323
|
+
* @param {string} message - The message to send.
|
|
2324
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
|
|
2325
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
|
|
2326
|
+
* possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
|
|
2327
|
+
*
|
|
2328
|
+
* @remarks
|
|
2329
|
+
* If timeout is 0, the snackbar message will be displayed until closed by the user.
|
|
2330
|
+
*/
|
|
1945
2331
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1946
2332
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1947
2333
|
return;
|
|
1948
2334
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
2335
|
+
// Send the message to all connected clients
|
|
1949
2336
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
1950
2337
|
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Sends a close snackbar message to all connected clients.
|
|
2340
|
+
* It will close the snackbar message with the same message and timeout = 0.
|
|
2341
|
+
*
|
|
2342
|
+
* @param {string} message - The message to send.
|
|
2343
|
+
*/
|
|
1951
2344
|
wssSendCloseSnackbarMessage(message) {
|
|
1952
2345
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1953
2346
|
return;
|
|
1954
2347
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
2348
|
+
// Send the message to all connected clients
|
|
1955
2349
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
1956
2350
|
}
|
|
2351
|
+
/**
|
|
2352
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
2353
|
+
*
|
|
2354
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
2355
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
2356
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
2357
|
+
* @param {EndpointNumber} number - The endpoint number where the attribute belongs.
|
|
2358
|
+
* @param {string} id - The endpoint id where the attribute belongs.
|
|
2359
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
2360
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
2361
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
2362
|
+
*
|
|
2363
|
+
* @remarks
|
|
2364
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
2365
|
+
* with the updated attribute information.
|
|
2366
|
+
*/
|
|
1957
2367
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
1958
2368
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1959
2369
|
return;
|
|
1960
2370
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
2371
|
+
// Send the message to all connected clients
|
|
1961
2372
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
|
|
1962
2373
|
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Sends a message to all connected clients.
|
|
2376
|
+
* This is an helper function to send a broadcast message to all connected clients.
|
|
2377
|
+
*
|
|
2378
|
+
* @param {WsMessageBroadcast} msg - The message to send.
|
|
2379
|
+
*/
|
|
1963
2380
|
wssBroadcastMessage(msg) {
|
|
1964
2381
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1965
2382
|
return;
|
|
2383
|
+
// Send the message to all connected clients
|
|
1966
2384
|
const stringifiedMsg = JSON.stringify(msg);
|
|
1967
2385
|
if (msg.method !== 'log')
|
|
1968
2386
|
this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
|
|
@@ -1973,3 +2391,4 @@ export class Frontend extends EventEmitter {
|
|
|
1973
2391
|
});
|
|
1974
2392
|
}
|
|
1975
2393
|
}
|
|
2394
|
+
//# sourceMappingURL=frontend.js.map
|