matterbridge 3.4.0-dev-20251126-5087664 → 3.4.0
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 +838 -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 +135 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +113 -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 +61 -0
- package/dist/devices/cooktop.d.ts.map +1 -0
- package/dist/devices/cooktop.js +56 -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 +85 -0
- package/dist/devices/oven.js.map +1 -0
- package/dist/devices/refrigerator.d.ts +118 -0
- package/dist/devices/refrigerator.d.ts.map +1 -0
- package/dist/devices/refrigerator.js +102 -0
- package/dist/devices/refrigerator.js.map +1 -0
- package/dist/devices/roboticVacuumCleaner.d.ts +112 -0
- package/dist/devices/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/devices/roboticVacuumCleaner.js +100 -9
- package/dist/devices/roboticVacuumCleaner.js.map +1 -0
- package/dist/devices/solarPower.d.ts +40 -0
- package/dist/devices/solarPower.d.ts.map +1 -0
- package/dist/devices/solarPower.js +38 -0
- package/dist/devices/solarPower.js.map +1 -0
- package/dist/devices/speaker.d.ts +87 -0
- package/dist/devices/speaker.d.ts.map +1 -0
- package/dist/devices/speaker.js +84 -0
- package/dist/devices/speaker.js.map +1 -0
- package/dist/devices/temperatureControl.d.ts +166 -0
- package/dist/devices/temperatureControl.d.ts.map +1 -0
- package/dist/devices/temperatureControl.js +24 -3
- package/dist/devices/temperatureControl.js.map +1 -0
- package/dist/devices/waterHeater.d.ts +111 -0
- package/dist/devices/waterHeater.d.ts.map +1 -0
- package/dist/devices/waterHeater.js +82 -2
- package/dist/devices/waterHeater.js.map +1 -0
- package/dist/dgram/coap.d.ts +205 -0
- package/dist/dgram/coap.d.ts.map +1 -0
- package/dist/dgram/coap.js +126 -13
- package/dist/dgram/coap.js.map +1 -0
- package/dist/dgram/dgram.d.ts +141 -0
- package/dist/dgram/dgram.d.ts.map +1 -0
- package/dist/dgram/dgram.js +114 -2
- package/dist/dgram/dgram.js.map +1 -0
- package/dist/dgram/mb_coap.d.ts +24 -0
- package/dist/dgram/mb_coap.d.ts.map +1 -0
- package/dist/dgram/mb_coap.js +41 -3
- package/dist/dgram/mb_coap.js.map +1 -0
- package/dist/dgram/mb_mdns.d.ts +24 -0
- package/dist/dgram/mb_mdns.d.ts.map +1 -0
- package/dist/dgram/mb_mdns.js +80 -15
- package/dist/dgram/mb_mdns.js.map +1 -0
- package/dist/dgram/mdns.d.ts +290 -0
- package/dist/dgram/mdns.d.ts.map +1 -0
- package/dist/dgram/mdns.js +299 -137
- package/dist/dgram/mdns.js.map +1 -0
- package/dist/dgram/multicast.d.ts +67 -0
- package/dist/dgram/multicast.d.ts.map +1 -0
- package/dist/dgram/multicast.js +62 -1
- package/dist/dgram/multicast.js.map +1 -0
- package/dist/dgram/unicast.d.ts +56 -0
- package/dist/dgram/unicast.d.ts.map +1 -0
- package/dist/dgram/unicast.js +54 -0
- package/dist/dgram/unicast.js.map +1 -0
- package/dist/frontend.d.ts +238 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +455 -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 +303 -0
- package/dist/jestutils/jestHelpers.d.ts.map +1 -0
- package/dist/jestutils/jestHelpers.js +352 -13
- 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/matterNode.d.ts +342 -0
- package/dist/matterNode.d.ts.map +1 -0
- package/dist/matterNode.js +369 -8
- package/dist/matterNode.js.map +1 -0
- package/dist/matterbridge.d.ts +473 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +787 -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 +68 -5
- 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 +1507 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +1444 -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 +483 -20
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgeEndpointTypes.d.ts +166 -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 +524 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +439 -1
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +251 -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 +340 -5
- 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 +35 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +37 -0
- 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 +111 -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 +33 -0
- package/dist/utils/spawn.d.ts.map +1 -0
- package/dist/utils/spawn.js +71 -1
- 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));
|
|
@@ -140,23 +166,53 @@ export class Frontend extends EventEmitter {
|
|
|
140
166
|
this.port = port;
|
|
141
167
|
this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
|
|
142
168
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
169
|
+
// Initialize multer with the upload directory
|
|
143
170
|
const multer = await import('multer');
|
|
144
|
-
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads');
|
|
171
|
+
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
|
|
145
172
|
const upload = multer.default({ dest: uploadDir });
|
|
173
|
+
// Create the express app that serves the frontend
|
|
146
174
|
const express = await import('express');
|
|
147
175
|
this.expressApp = express.default();
|
|
176
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
177
|
+
/*
|
|
178
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
179
|
+
for (const method of methods) {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
183
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
184
|
+
try {
|
|
185
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
186
|
+
return original(path, ...rest);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
*/
|
|
194
|
+
// Log all requests to the server for debugging
|
|
195
|
+
/*
|
|
196
|
+
this.expressApp.use((req, res, next) => {
|
|
197
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
198
|
+
next();
|
|
199
|
+
});
|
|
200
|
+
*/
|
|
201
|
+
// Serve static files from 'frontend/build' directory
|
|
148
202
|
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'frontend', 'build')));
|
|
203
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
149
204
|
this.log.debug(`Creating WebSocketServer...`);
|
|
150
205
|
const ws = await import('ws');
|
|
151
206
|
this.webSocketServer = new ws.WebSocketServer({ noServer: true });
|
|
152
207
|
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
153
208
|
this.webSocketServer.on('connection', (ws, request) => {
|
|
154
209
|
const clientIp = request.socket.remoteAddress;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
210
|
+
// Set the global logger callback for the WebSocketServer
|
|
211
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
212
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
213
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
214
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
215
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
160
216
|
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
161
217
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
162
218
|
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
@@ -178,16 +234,25 @@ export class Frontend extends EventEmitter {
|
|
|
178
234
|
}
|
|
179
235
|
});
|
|
180
236
|
ws.on('error', (error) => {
|
|
237
|
+
// istanbul ignore next
|
|
181
238
|
this.log.error(`WebSocket client error: ${error}`);
|
|
182
239
|
});
|
|
183
240
|
});
|
|
184
241
|
this.webSocketServer.on('close', () => {
|
|
185
242
|
this.log.debug(`WebSocketServer closed`);
|
|
186
243
|
});
|
|
244
|
+
/* With { noServer: true } it never fires
|
|
245
|
+
this.webSocketServer.on('listening', () => {
|
|
246
|
+
this.log.info(`The WebSocketServer is listening`);
|
|
247
|
+
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
248
|
+
});
|
|
249
|
+
*/
|
|
250
|
+
// istanbul ignore next
|
|
187
251
|
this.webSocketServer.on('error', (ws, error) => {
|
|
188
252
|
this.log.error(`WebSocketServer error: ${error}`);
|
|
189
253
|
});
|
|
190
254
|
if (!hasParameter('ssl')) {
|
|
255
|
+
// Create an HTTP server and attach the express app
|
|
191
256
|
const http = await import('node:http');
|
|
192
257
|
try {
|
|
193
258
|
this.log.debug(`Creating HTTP server...`);
|
|
@@ -198,6 +263,7 @@ export class Frontend extends EventEmitter {
|
|
|
198
263
|
this.emit('server_error', error);
|
|
199
264
|
return;
|
|
200
265
|
}
|
|
266
|
+
// Listen on the specified port
|
|
201
267
|
if (hasParameter('ingress')) {
|
|
202
268
|
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
203
269
|
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -217,24 +283,30 @@ export class Frontend extends EventEmitter {
|
|
|
217
283
|
}
|
|
218
284
|
this.httpServer.on('upgrade', async (req, socket, head) => {
|
|
219
285
|
try {
|
|
286
|
+
// Only proceed for real WebSocket upgrades
|
|
287
|
+
// istanbul ignore next cause is only a safety check
|
|
220
288
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
221
289
|
this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
|
|
222
290
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
223
291
|
return socket.destroy();
|
|
224
292
|
}
|
|
293
|
+
// Build a URL so we can read ?password=...
|
|
225
294
|
const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
|
|
295
|
+
// Validate WebSocket password
|
|
226
296
|
const password = url.searchParams.get('password') ?? '';
|
|
227
297
|
if (password !== this.storedPassword) {
|
|
228
298
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
229
299
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
230
300
|
return socket.destroy();
|
|
231
301
|
}
|
|
302
|
+
// Complete the WebSocket handshake
|
|
232
303
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
233
304
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
234
305
|
this.webSocketServer?.emit('connection', ws, req);
|
|
235
306
|
});
|
|
236
307
|
}
|
|
237
308
|
catch (err) {
|
|
309
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
238
310
|
{
|
|
239
311
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
240
312
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -257,6 +329,7 @@ export class Frontend extends EventEmitter {
|
|
|
257
329
|
});
|
|
258
330
|
}
|
|
259
331
|
else {
|
|
332
|
+
// SSL is enabled, load the certificate and the private key
|
|
260
333
|
let cert;
|
|
261
334
|
let key;
|
|
262
335
|
let ca;
|
|
@@ -266,6 +339,7 @@ export class Frontend extends EventEmitter {
|
|
|
266
339
|
let httpsServerOptions = {};
|
|
267
340
|
const fs = await import('node:fs');
|
|
268
341
|
if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
|
|
342
|
+
// Load the p12 certificate and the passphrase
|
|
269
343
|
try {
|
|
270
344
|
pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
|
|
271
345
|
this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
|
|
@@ -277,7 +351,7 @@ export class Frontend extends EventEmitter {
|
|
|
277
351
|
}
|
|
278
352
|
try {
|
|
279
353
|
passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
|
|
280
|
-
passphrase = passphrase.trim();
|
|
354
|
+
passphrase = passphrase.trim(); // Ensure no extra characters
|
|
281
355
|
this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
|
|
282
356
|
}
|
|
283
357
|
catch (error) {
|
|
@@ -288,6 +362,7 @@ export class Frontend extends EventEmitter {
|
|
|
288
362
|
httpsServerOptions = { pfx, passphrase };
|
|
289
363
|
}
|
|
290
364
|
else {
|
|
365
|
+
// 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.
|
|
291
366
|
try {
|
|
292
367
|
cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
|
|
293
368
|
this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
|
|
@@ -317,9 +392,10 @@ export class Frontend extends EventEmitter {
|
|
|
317
392
|
httpsServerOptions = { cert: fullChain ?? cert, key, ca };
|
|
318
393
|
}
|
|
319
394
|
if (hasParameter('mtls')) {
|
|
320
|
-
httpsServerOptions.requestCert = true;
|
|
321
|
-
httpsServerOptions.rejectUnauthorized = true;
|
|
395
|
+
httpsServerOptions.requestCert = true; // Request client certificate
|
|
396
|
+
httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
|
|
322
397
|
}
|
|
398
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
323
399
|
const https = await import('node:https');
|
|
324
400
|
try {
|
|
325
401
|
this.log.debug(`Creating HTTPS server...`);
|
|
@@ -330,6 +406,7 @@ export class Frontend extends EventEmitter {
|
|
|
330
406
|
this.emit('server_error', error);
|
|
331
407
|
return;
|
|
332
408
|
}
|
|
409
|
+
// Listen on the specified port
|
|
333
410
|
if (hasParameter('ingress')) {
|
|
334
411
|
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
335
412
|
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
@@ -349,23 +426,29 @@ export class Frontend extends EventEmitter {
|
|
|
349
426
|
}
|
|
350
427
|
this.httpsServer.on('upgrade', async (req, socket, head) => {
|
|
351
428
|
try {
|
|
429
|
+
// Only proceed for real WebSocket upgrades
|
|
430
|
+
// istanbul ignore next cause is only a safety check
|
|
352
431
|
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
353
432
|
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
354
433
|
return socket.destroy();
|
|
355
434
|
}
|
|
435
|
+
// Build a URL so we can read ?password=...
|
|
356
436
|
const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
|
|
437
|
+
// Validate WebSocket password
|
|
357
438
|
const password = url.searchParams.get('password') ?? '';
|
|
358
439
|
if (password !== this.storedPassword) {
|
|
359
440
|
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
360
441
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
361
442
|
return socket.destroy();
|
|
362
443
|
}
|
|
444
|
+
// Complete the WebSocket handshake
|
|
363
445
|
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
364
446
|
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
365
447
|
this.webSocketServer?.emit('connection', ws, req);
|
|
366
448
|
});
|
|
367
449
|
}
|
|
368
450
|
catch (err) {
|
|
451
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
369
452
|
{
|
|
370
453
|
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
371
454
|
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
@@ -387,6 +470,7 @@ export class Frontend extends EventEmitter {
|
|
|
387
470
|
return;
|
|
388
471
|
});
|
|
389
472
|
}
|
|
473
|
+
// Subscribe to cli events
|
|
390
474
|
cliEmitter.removeAllListeners();
|
|
391
475
|
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
392
476
|
this.wssSendUptimeUpdate(systemUptime, processUptime);
|
|
@@ -397,6 +481,8 @@ export class Frontend extends EventEmitter {
|
|
|
397
481
|
cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
|
|
398
482
|
this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
|
|
399
483
|
});
|
|
484
|
+
// Endpoint to validate login code
|
|
485
|
+
// curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
|
|
400
486
|
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
401
487
|
const { password } = req.body;
|
|
402
488
|
this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
|
|
@@ -409,17 +495,20 @@ export class Frontend extends EventEmitter {
|
|
|
409
495
|
res.json({ valid: false });
|
|
410
496
|
}
|
|
411
497
|
});
|
|
498
|
+
// Endpoint to provide health check for docker
|
|
412
499
|
this.expressApp.get('/health', (req, res) => {
|
|
413
500
|
this.log.debug('Express received /health');
|
|
414
501
|
const healthStatus = {
|
|
415
|
-
status: 'ok',
|
|
416
|
-
uptime: process.uptime(),
|
|
417
|
-
timestamp: new Date().toISOString(),
|
|
502
|
+
status: 'ok', // Indicate service is healthy
|
|
503
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
504
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
418
505
|
};
|
|
419
506
|
res.status(200).json(healthStatus);
|
|
420
507
|
});
|
|
508
|
+
// Endpoint to provide memory usage details
|
|
421
509
|
this.expressApp.get('/memory', async (req, res) => {
|
|
422
510
|
this.log.debug('Express received /memory');
|
|
511
|
+
// Memory usage from process
|
|
423
512
|
const memoryUsageRaw = process.memoryUsage();
|
|
424
513
|
const memoryUsage = {
|
|
425
514
|
rss: formatBytes(memoryUsageRaw.rss),
|
|
@@ -428,10 +517,13 @@ export class Frontend extends EventEmitter {
|
|
|
428
517
|
external: formatBytes(memoryUsageRaw.external),
|
|
429
518
|
arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
|
|
430
519
|
};
|
|
520
|
+
// V8 heap statistics
|
|
431
521
|
const { default: v8 } = await import('node:v8');
|
|
432
522
|
const heapStatsRaw = v8.getHeapStatistics();
|
|
433
523
|
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
524
|
+
// Format heapStats
|
|
434
525
|
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
|
|
526
|
+
// Format heapSpaces
|
|
435
527
|
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
436
528
|
...space,
|
|
437
529
|
space_size: formatBytes(space.space_size),
|
|
@@ -450,18 +542,22 @@ export class Frontend extends EventEmitter {
|
|
|
450
542
|
};
|
|
451
543
|
res.status(200).json(memoryReport);
|
|
452
544
|
});
|
|
545
|
+
// Endpoint to provide settings
|
|
453
546
|
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
454
547
|
this.log.debug('The frontend sent /api/settings');
|
|
455
548
|
res.json(await this.getApiSettings());
|
|
456
549
|
});
|
|
550
|
+
// Endpoint to provide plugins
|
|
457
551
|
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
458
552
|
this.log.debug('The frontend sent /api/plugins');
|
|
459
553
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
|
|
460
554
|
});
|
|
555
|
+
// Endpoint to provide devices
|
|
461
556
|
this.expressApp.get('/api/devices', async (req, res) => {
|
|
462
557
|
this.log.debug('The frontend sent /api/devices');
|
|
463
558
|
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
|
|
464
559
|
});
|
|
560
|
+
// Endpoint to view the matterbridge log
|
|
465
561
|
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
466
562
|
this.log.debug('The frontend sent /api/view-mblog');
|
|
467
563
|
try {
|
|
@@ -475,6 +571,7 @@ export class Frontend extends EventEmitter {
|
|
|
475
571
|
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
476
572
|
}
|
|
477
573
|
});
|
|
574
|
+
// Endpoint to view the matter.js log
|
|
478
575
|
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
479
576
|
this.log.debug('The frontend sent /api/view-mjlog');
|
|
480
577
|
try {
|
|
@@ -488,6 +585,7 @@ export class Frontend extends EventEmitter {
|
|
|
488
585
|
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
489
586
|
}
|
|
490
587
|
});
|
|
588
|
+
// Endpoint to view the diagnostic.log
|
|
491
589
|
this.expressApp.get('/api/view-diagnostic', async (req, res) => {
|
|
492
590
|
this.log.debug('The frontend sent /api/view-diagnostic');
|
|
493
591
|
await this.generateDiagnostic();
|
|
@@ -498,10 +596,13 @@ export class Frontend extends EventEmitter {
|
|
|
498
596
|
res.send(data.slice(29));
|
|
499
597
|
}
|
|
500
598
|
catch (error) {
|
|
599
|
+
// istanbul ignore next
|
|
501
600
|
this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
601
|
+
// istanbul ignore next
|
|
502
602
|
res.status(500).send('Error reading diagnostic log file.');
|
|
503
603
|
}
|
|
504
604
|
});
|
|
605
|
+
// Endpoint to download the diagnostic.log
|
|
505
606
|
this.expressApp.get('/api/download-diagnostic', async (req, res) => {
|
|
506
607
|
this.log.debug(`The frontend sent /api/download-diagnostic`);
|
|
507
608
|
await this.generateDiagnostic();
|
|
@@ -512,16 +613,19 @@ export class Frontend extends EventEmitter {
|
|
|
512
613
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
|
|
513
614
|
}
|
|
514
615
|
catch (error) {
|
|
616
|
+
// istanbul ignore next
|
|
515
617
|
this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
|
|
516
618
|
}
|
|
517
619
|
res.type('text/plain');
|
|
518
620
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
|
|
621
|
+
/* istanbul ignore if */
|
|
519
622
|
if (error) {
|
|
520
623
|
this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
521
624
|
res.status(500).send('Error downloading the diagnostic log file');
|
|
522
625
|
}
|
|
523
626
|
});
|
|
524
627
|
});
|
|
628
|
+
// Endpoint to view the history.html
|
|
525
629
|
this.expressApp.get('/api/viewhistory', async (req, res) => {
|
|
526
630
|
this.log.debug('The frontend sent /api/viewhistory');
|
|
527
631
|
try {
|
|
@@ -535,6 +639,7 @@ export class Frontend extends EventEmitter {
|
|
|
535
639
|
res.status(500).send('Error reading history file.');
|
|
536
640
|
}
|
|
537
641
|
});
|
|
642
|
+
// Endpoint to download the history.html
|
|
538
643
|
this.expressApp.get('/api/downloadhistory', async (req, res) => {
|
|
539
644
|
this.log.debug(`The frontend sent /api/downloadhistory`);
|
|
540
645
|
try {
|
|
@@ -544,6 +649,7 @@ export class Frontend extends EventEmitter {
|
|
|
544
649
|
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
|
|
545
650
|
res.type('text/plain');
|
|
546
651
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
|
|
652
|
+
/* istanbul ignore if */
|
|
547
653
|
if (error) {
|
|
548
654
|
this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
549
655
|
res.status(500).send('Error downloading history file');
|
|
@@ -555,6 +661,7 @@ export class Frontend extends EventEmitter {
|
|
|
555
661
|
res.status(500).send('Error reading history file.');
|
|
556
662
|
}
|
|
557
663
|
});
|
|
664
|
+
// Endpoint to view the shelly log
|
|
558
665
|
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
559
666
|
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
560
667
|
try {
|
|
@@ -568,6 +675,7 @@ export class Frontend extends EventEmitter {
|
|
|
568
675
|
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
569
676
|
}
|
|
570
677
|
});
|
|
678
|
+
// Endpoint to download the matterbridge log
|
|
571
679
|
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
572
680
|
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
573
681
|
const fs = await import('node:fs');
|
|
@@ -582,12 +690,14 @@ export class Frontend extends EventEmitter {
|
|
|
582
690
|
}
|
|
583
691
|
res.type('text/plain');
|
|
584
692
|
res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
|
|
693
|
+
/* istanbul ignore if */
|
|
585
694
|
if (error) {
|
|
586
695
|
this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
587
696
|
res.status(500).send('Error downloading the matterbridge log file');
|
|
588
697
|
}
|
|
589
698
|
});
|
|
590
699
|
});
|
|
700
|
+
// Endpoint to download the matter log
|
|
591
701
|
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
592
702
|
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
593
703
|
const fs = await import('node:fs');
|
|
@@ -602,12 +712,14 @@ export class Frontend extends EventEmitter {
|
|
|
602
712
|
}
|
|
603
713
|
res.type('text/plain');
|
|
604
714
|
res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
|
|
715
|
+
/* istanbul ignore if */
|
|
605
716
|
if (error) {
|
|
606
717
|
this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
607
718
|
res.status(500).send('Error downloading the matter log file');
|
|
608
719
|
}
|
|
609
720
|
});
|
|
610
721
|
});
|
|
722
|
+
// Endpoint to download the shelly log
|
|
611
723
|
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
612
724
|
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
613
725
|
const fs = await import('node:fs');
|
|
@@ -622,75 +734,91 @@ export class Frontend extends EventEmitter {
|
|
|
622
734
|
}
|
|
623
735
|
res.type('text/plain');
|
|
624
736
|
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
737
|
+
/* istanbul ignore if */
|
|
625
738
|
if (error) {
|
|
626
739
|
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
627
740
|
res.status(500).send('Error downloading Shelly system log file');
|
|
628
741
|
}
|
|
629
742
|
});
|
|
630
743
|
});
|
|
744
|
+
// Endpoint to download the matterbridge storage directory
|
|
631
745
|
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
632
746
|
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
633
747
|
await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
|
|
634
748
|
res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
|
|
749
|
+
/* istanbul ignore if */
|
|
635
750
|
if (error) {
|
|
636
751
|
this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
|
|
637
752
|
res.status(500).send('Error downloading the matterbridge storage file');
|
|
638
753
|
}
|
|
639
754
|
});
|
|
640
755
|
});
|
|
756
|
+
// Endpoint to download the matter storage file
|
|
641
757
|
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
642
758
|
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
643
759
|
await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
644
760
|
res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
|
|
761
|
+
/* istanbul ignore if */
|
|
645
762
|
if (error) {
|
|
646
763
|
this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
|
|
647
764
|
res.status(500).send('Error downloading the matter storage zip file');
|
|
648
765
|
}
|
|
649
766
|
});
|
|
650
767
|
});
|
|
768
|
+
// Endpoint to download the matterbridge plugin directory
|
|
651
769
|
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
652
770
|
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
653
771
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
654
772
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
|
|
773
|
+
/* istanbul ignore if */
|
|
655
774
|
if (error) {
|
|
656
775
|
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
657
776
|
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
658
777
|
}
|
|
659
778
|
});
|
|
660
779
|
});
|
|
780
|
+
// Endpoint to download the matterbridge plugin config files
|
|
661
781
|
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
662
782
|
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
663
783
|
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
664
784
|
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
785
|
+
/* istanbul ignore if */
|
|
665
786
|
if (error) {
|
|
666
787
|
this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
|
|
667
788
|
res.status(500).send('Error downloading the matterbridge plugin config file');
|
|
668
789
|
}
|
|
669
790
|
});
|
|
670
791
|
});
|
|
792
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
671
793
|
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
672
794
|
this.log.debug('The frontend sent /api/download-backup');
|
|
673
795
|
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
796
|
+
/* istanbul ignore if */
|
|
674
797
|
if (error) {
|
|
675
798
|
this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
676
799
|
res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
677
800
|
}
|
|
678
801
|
});
|
|
679
802
|
});
|
|
803
|
+
// Endpoint to upload a package
|
|
680
804
|
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
681
805
|
const { filename } = req.body;
|
|
682
806
|
const file = req.file;
|
|
807
|
+
/* istanbul ignore if */
|
|
683
808
|
if (!file || !filename) {
|
|
684
809
|
this.log.error(`uploadpackage: invalid request: file and filename are required`);
|
|
685
810
|
res.status(400).send('Invalid request: file and filename are required');
|
|
686
811
|
return;
|
|
687
812
|
}
|
|
688
813
|
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
814
|
+
// Define the path where the plugin file will be saved
|
|
689
815
|
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
690
816
|
try {
|
|
817
|
+
// Move the uploaded file to the specified path
|
|
691
818
|
const fs = await import('node:fs');
|
|
692
819
|
await fs.promises.rename(file.path, filePath);
|
|
693
820
|
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
821
|
+
// Install the plugin package
|
|
694
822
|
if (filename.endsWith('.tgz')) {
|
|
695
823
|
const { spawnCommand } = await import('./utils/spawn.js');
|
|
696
824
|
if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
|
|
@@ -718,6 +846,7 @@ export class Frontend extends EventEmitter {
|
|
|
718
846
|
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
719
847
|
}
|
|
720
848
|
});
|
|
849
|
+
// Fallback for routing (must be the last route)
|
|
721
850
|
this.expressApp.use((req, res) => {
|
|
722
851
|
const filePath = path.resolve(this.matterbridge.rootDirectory, 'frontend', 'build');
|
|
723
852
|
this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
|
|
@@ -728,13 +857,16 @@ export class Frontend extends EventEmitter {
|
|
|
728
857
|
async stop() {
|
|
729
858
|
this.log.debug('Stopping the frontend...');
|
|
730
859
|
const ws = await import('ws');
|
|
860
|
+
// Remove listeners from the express app
|
|
731
861
|
if (this.expressApp) {
|
|
732
862
|
this.expressApp.removeAllListeners();
|
|
733
863
|
this.expressApp = undefined;
|
|
734
864
|
this.log.debug('Frontend app closed successfully');
|
|
735
865
|
}
|
|
866
|
+
// Close the WebSocket server
|
|
736
867
|
if (this.webSocketServer) {
|
|
737
868
|
this.log.debug('Closing WebSocket server...');
|
|
869
|
+
// Close all active connections
|
|
738
870
|
this.webSocketServer.clients.forEach((client) => {
|
|
739
871
|
if (client.readyState === ws.WebSocket.OPEN) {
|
|
740
872
|
client.close();
|
|
@@ -743,6 +875,7 @@ export class Frontend extends EventEmitter {
|
|
|
743
875
|
await withTimeout(new Promise((resolve) => {
|
|
744
876
|
this.webSocketServer?.close((error) => {
|
|
745
877
|
if (error) {
|
|
878
|
+
// istanbul ignore next
|
|
746
879
|
this.log.error(`Error closing WebSocket server: ${error}`);
|
|
747
880
|
}
|
|
748
881
|
else {
|
|
@@ -755,8 +888,27 @@ export class Frontend extends EventEmitter {
|
|
|
755
888
|
this.webSocketServer.removeAllListeners();
|
|
756
889
|
this.webSocketServer = undefined;
|
|
757
890
|
}
|
|
891
|
+
// Close the http server
|
|
758
892
|
if (this.httpServer) {
|
|
759
893
|
this.log.debug('Closing http server...');
|
|
894
|
+
/*
|
|
895
|
+
await withTimeout(
|
|
896
|
+
new Promise<void>((resolve) => {
|
|
897
|
+
this.httpServer?.close((error) => {
|
|
898
|
+
if (error) {
|
|
899
|
+
// istanbul ignore next
|
|
900
|
+
this.log.error(`Error closing http server: ${error}`);
|
|
901
|
+
} else {
|
|
902
|
+
this.log.debug('Http server closed successfully');
|
|
903
|
+
this.emit('server_stopped');
|
|
904
|
+
}
|
|
905
|
+
resolve();
|
|
906
|
+
});
|
|
907
|
+
}),
|
|
908
|
+
5000,
|
|
909
|
+
false,
|
|
910
|
+
);
|
|
911
|
+
*/
|
|
760
912
|
this.httpServer.close();
|
|
761
913
|
this.log.debug('Http server closed successfully');
|
|
762
914
|
this.listening = false;
|
|
@@ -765,8 +917,27 @@ export class Frontend extends EventEmitter {
|
|
|
765
917
|
this.httpServer = undefined;
|
|
766
918
|
this.log.debug('Frontend http server closed successfully');
|
|
767
919
|
}
|
|
920
|
+
// Close the https server
|
|
768
921
|
if (this.httpsServer) {
|
|
769
922
|
this.log.debug('Closing https server...');
|
|
923
|
+
/*
|
|
924
|
+
await withTimeout(
|
|
925
|
+
new Promise<void>((resolve) => {
|
|
926
|
+
this.httpsServer?.close((error) => {
|
|
927
|
+
if (error) {
|
|
928
|
+
// istanbul ignore next
|
|
929
|
+
this.log.error(`Error closing https server: ${error}`);
|
|
930
|
+
} else {
|
|
931
|
+
this.log.debug('Https server closed successfully');
|
|
932
|
+
this.emit('server_stopped');
|
|
933
|
+
}
|
|
934
|
+
resolve();
|
|
935
|
+
});
|
|
936
|
+
}),
|
|
937
|
+
5000,
|
|
938
|
+
false,
|
|
939
|
+
);
|
|
940
|
+
*/
|
|
770
941
|
this.httpsServer.close();
|
|
771
942
|
this.log.debug('Https server closed successfully');
|
|
772
943
|
this.listening = false;
|
|
@@ -777,7 +948,13 @@ export class Frontend extends EventEmitter {
|
|
|
777
948
|
}
|
|
778
949
|
this.log.debug('Frontend stopped successfully');
|
|
779
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* Retrieves the api settings data.
|
|
953
|
+
*
|
|
954
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
955
|
+
*/
|
|
780
956
|
async getApiSettings() {
|
|
957
|
+
// Update the variable system information properties
|
|
781
958
|
this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
782
959
|
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
783
960
|
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
@@ -787,6 +964,7 @@ export class Frontend extends EventEmitter {
|
|
|
787
964
|
this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
|
|
788
965
|
this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
789
966
|
this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
967
|
+
// Create the matterbridge information
|
|
790
968
|
const info = {
|
|
791
969
|
homeDirectory: this.matterbridge.homeDirectory,
|
|
792
970
|
rootDirectory: this.matterbridge.rootDirectory,
|
|
@@ -822,9 +1000,15 @@ export class Frontend extends EventEmitter {
|
|
|
822
1000
|
};
|
|
823
1001
|
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
|
|
824
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Retrieves the reachable attribute.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
|
|
1007
|
+
* @returns {boolean} The reachable attribute.
|
|
1008
|
+
*/
|
|
825
1009
|
getReachability(device) {
|
|
826
1010
|
if (this.matterbridge.hasCleanupStarted)
|
|
827
|
-
return false;
|
|
1011
|
+
return false; // Skip if cleanup has started
|
|
828
1012
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
829
1013
|
return false;
|
|
830
1014
|
if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
|
|
@@ -835,9 +1019,15 @@ export class Frontend extends EventEmitter {
|
|
|
835
1019
|
return true;
|
|
836
1020
|
return false;
|
|
837
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Retrieves the power source attribute.
|
|
1024
|
+
*
|
|
1025
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
|
|
1026
|
+
* @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
|
|
1027
|
+
*/
|
|
838
1028
|
getPowerSource(endpoint) {
|
|
839
1029
|
if (this.matterbridge.hasCleanupStarted)
|
|
840
|
-
return;
|
|
1030
|
+
return; // Skip if cleanup has started
|
|
841
1031
|
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
842
1032
|
return undefined;
|
|
843
1033
|
const powerSource = (device) => {
|
|
@@ -852,16 +1042,25 @@ export class Frontend extends EventEmitter {
|
|
|
852
1042
|
}
|
|
853
1043
|
return;
|
|
854
1044
|
};
|
|
1045
|
+
// Root endpoint
|
|
855
1046
|
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
856
1047
|
return powerSource(endpoint);
|
|
1048
|
+
// Child endpoints
|
|
857
1049
|
for (const child of endpoint.getChildEndpoints()) {
|
|
858
1050
|
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
859
1051
|
return powerSource(child);
|
|
860
1052
|
}
|
|
861
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Retrieves the cluster text description from a given device.
|
|
1056
|
+
* The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
|
|
1059
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1060
|
+
*/
|
|
862
1061
|
getClusterTextFromDevice(device) {
|
|
863
1062
|
if (this.matterbridge.hasCleanupStarted)
|
|
864
|
-
return '';
|
|
1063
|
+
return ''; // Skip if cleanup has started
|
|
865
1064
|
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
866
1065
|
return '';
|
|
867
1066
|
const getUserLabel = (device) => {
|
|
@@ -871,6 +1070,7 @@ export class Frontend extends EventEmitter {
|
|
|
871
1070
|
if (composed)
|
|
872
1071
|
return 'Composed: ' + composed.value;
|
|
873
1072
|
}
|
|
1073
|
+
// istanbul ignore next cause is not reachable
|
|
874
1074
|
return '';
|
|
875
1075
|
};
|
|
876
1076
|
const getFixedLabel = (device) => {
|
|
@@ -880,11 +1080,13 @@ export class Frontend extends EventEmitter {
|
|
|
880
1080
|
if (composed)
|
|
881
1081
|
return 'Composed: ' + composed.value;
|
|
882
1082
|
}
|
|
1083
|
+
// istanbul ignore next cause is not reacheable
|
|
883
1084
|
return '';
|
|
884
1085
|
};
|
|
885
1086
|
let attributes = '';
|
|
886
1087
|
let supportedModes = [];
|
|
887
1088
|
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1089
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
888
1090
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
889
1091
|
return;
|
|
890
1092
|
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
@@ -974,11 +1176,17 @@ export class Frontend extends EventEmitter {
|
|
|
974
1176
|
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
975
1177
|
attributes += `${getUserLabel(device)} `;
|
|
976
1178
|
});
|
|
1179
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
977
1180
|
return attributes.trimStart().trimEnd();
|
|
978
1181
|
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Retrieves the registered plugins sanitized for res.json().
|
|
1184
|
+
*
|
|
1185
|
+
* @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
|
|
1186
|
+
*/
|
|
979
1187
|
getPlugins() {
|
|
980
1188
|
if (this.matterbridge.hasCleanupStarted)
|
|
981
|
-
return [];
|
|
1189
|
+
return []; // Skip if cleanup has started
|
|
982
1190
|
const plugins = [];
|
|
983
1191
|
for (const plugin of this.matterbridge.plugins.array()) {
|
|
984
1192
|
plugins.push({
|
|
@@ -1006,18 +1214,27 @@ export class Frontend extends EventEmitter {
|
|
|
1006
1214
|
schemaJson: plugin.schemaJson,
|
|
1007
1215
|
hasWhiteList: plugin.configJson?.whiteList !== undefined,
|
|
1008
1216
|
hasBlackList: plugin.configJson?.blackList !== undefined,
|
|
1217
|
+
// Childbridge mode specific data
|
|
1009
1218
|
matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
|
|
1010
1219
|
});
|
|
1011
1220
|
}
|
|
1012
1221
|
return plugins;
|
|
1013
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Retrieves the devices from Matterbridge.
|
|
1225
|
+
*
|
|
1226
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
1227
|
+
* @returns {ApiDevice[]} An array of ApiDevices for the frontend.
|
|
1228
|
+
*/
|
|
1014
1229
|
getDevices(pluginName) {
|
|
1015
1230
|
if (this.matterbridge.hasCleanupStarted)
|
|
1016
|
-
return [];
|
|
1231
|
+
return []; // Skip if cleanup has started
|
|
1017
1232
|
const devices = [];
|
|
1018
1233
|
for (const device of this.matterbridge.devices.array()) {
|
|
1234
|
+
// Filter by pluginName if provided
|
|
1019
1235
|
if (pluginName && pluginName !== device.plugin)
|
|
1020
1236
|
continue;
|
|
1237
|
+
// Check if the device has the required properties
|
|
1021
1238
|
if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
1022
1239
|
continue;
|
|
1023
1240
|
devices.push({
|
|
@@ -1037,24 +1254,39 @@ export class Frontend extends EventEmitter {
|
|
|
1037
1254
|
}
|
|
1038
1255
|
return devices;
|
|
1039
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Retrieves the clusters from a given plugin and endpoint number.
|
|
1259
|
+
*
|
|
1260
|
+
* Response for /api/clusters
|
|
1261
|
+
*
|
|
1262
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1263
|
+
* @param {number} endpointNumber - The endpoint number.
|
|
1264
|
+
* @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
|
|
1265
|
+
*/
|
|
1040
1266
|
getClusters(pluginName, endpointNumber) {
|
|
1041
1267
|
if (this.matterbridge.hasCleanupStarted)
|
|
1042
|
-
return;
|
|
1268
|
+
return; // Skip if cleanup has started
|
|
1043
1269
|
const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
|
|
1044
1270
|
if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
|
|
1045
1271
|
this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1046
1272
|
return;
|
|
1047
1273
|
}
|
|
1274
|
+
// this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
|
|
1275
|
+
// Get the device types from the main endpoint
|
|
1048
1276
|
const deviceTypes = [];
|
|
1049
1277
|
const clusters = [];
|
|
1050
1278
|
endpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1051
1279
|
deviceTypes.push(d.deviceType);
|
|
1052
1280
|
});
|
|
1281
|
+
// Get the clusters from the main endpoint
|
|
1053
1282
|
endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1054
1283
|
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1055
1284
|
return;
|
|
1056
1285
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1057
1286
|
return;
|
|
1287
|
+
// console.log(
|
|
1288
|
+
// `${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}`,
|
|
1289
|
+
// );
|
|
1058
1290
|
clusters.push({
|
|
1059
1291
|
endpoint: endpoint.number.toString(),
|
|
1060
1292
|
number: endpoint.number,
|
|
@@ -1068,12 +1300,19 @@ export class Frontend extends EventEmitter {
|
|
|
1068
1300
|
attributeLocalValue: attributeValue,
|
|
1069
1301
|
});
|
|
1070
1302
|
});
|
|
1303
|
+
// Get the child endpoints
|
|
1071
1304
|
const childEndpoints = endpoint.getChildEndpoints();
|
|
1305
|
+
// if (childEndpoints.length === 0) {
|
|
1306
|
+
// this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1307
|
+
// }
|
|
1072
1308
|
childEndpoints.forEach((childEndpoint) => {
|
|
1309
|
+
// istanbul ignore if cause is not reachable: should never happen but ...
|
|
1073
1310
|
if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
|
|
1074
1311
|
this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1075
1312
|
return;
|
|
1076
1313
|
}
|
|
1314
|
+
// this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
|
|
1315
|
+
// Get the device types of the child endpoint
|
|
1077
1316
|
const deviceTypes = [];
|
|
1078
1317
|
childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1079
1318
|
deviceTypes.push(d.deviceType);
|
|
@@ -1083,6 +1322,9 @@ export class Frontend extends EventEmitter {
|
|
|
1083
1322
|
return;
|
|
1084
1323
|
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1085
1324
|
return;
|
|
1325
|
+
// console.log(
|
|
1326
|
+
// `${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}`,
|
|
1327
|
+
// );
|
|
1086
1328
|
clusters.push({
|
|
1087
1329
|
endpoint: childEndpoint.number.toString(),
|
|
1088
1330
|
number: childEndpoint.number,
|
|
@@ -1102,6 +1344,7 @@ export class Frontend extends EventEmitter {
|
|
|
1102
1344
|
async generateDiagnostic() {
|
|
1103
1345
|
this.log.debug('Generating diagnostic...');
|
|
1104
1346
|
const serverNodes = [];
|
|
1347
|
+
// istanbul ignore else
|
|
1105
1348
|
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
1106
1349
|
if (this.matterbridge.serverNode)
|
|
1107
1350
|
serverNodes.push(this.matterbridge.serverNode);
|
|
@@ -1112,6 +1355,7 @@ export class Frontend extends EventEmitter {
|
|
|
1112
1355
|
serverNodes.push(plugin.serverNode);
|
|
1113
1356
|
}
|
|
1114
1357
|
}
|
|
1358
|
+
// istanbul ignore next
|
|
1115
1359
|
for (const device of this.matterbridge.devices.array()) {
|
|
1116
1360
|
if (device.serverNode)
|
|
1117
1361
|
serverNodes.push(device.serverNode);
|
|
@@ -1135,8 +1379,15 @@ export class Frontend extends EventEmitter {
|
|
|
1135
1379
|
values: [...serverNodes],
|
|
1136
1380
|
})));
|
|
1137
1381
|
delete Logger.destinations.diagnostic;
|
|
1138
|
-
await wait(500);
|
|
1382
|
+
await wait(500); // Wait for the log to be written
|
|
1139
1383
|
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Handles incoming websocket api request messages from the Matterbridge frontend.
|
|
1386
|
+
*
|
|
1387
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1388
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1389
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1390
|
+
*/
|
|
1140
1391
|
async wsMessageHandler(client, message) {
|
|
1141
1392
|
let data;
|
|
1142
1393
|
const sendResponse = (data) => {
|
|
@@ -1154,12 +1405,13 @@ export class Frontend extends EventEmitter {
|
|
|
1154
1405
|
client.send(JSON.stringify(data));
|
|
1155
1406
|
}
|
|
1156
1407
|
else {
|
|
1408
|
+
// istanbul ignore next cause is only a safety check
|
|
1157
1409
|
this.log.error('Cannot send api response, client not connected');
|
|
1158
1410
|
}
|
|
1159
1411
|
};
|
|
1160
1412
|
try {
|
|
1161
1413
|
data = JSON.parse(message.toString());
|
|
1162
|
-
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) || data.dst !== 'Matterbridge') {
|
|
1414
|
+
if (!isValidNumber(data.id) || !isValidString(data.dst) || !isValidString(data.src) || !isValidString(data.method) /* || !isValidObject(data.params)*/ || data.dst !== 'Matterbridge') {
|
|
1163
1415
|
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
1164
1416
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
1165
1417
|
return;
|
|
@@ -1216,7 +1468,22 @@ export class Frontend extends EventEmitter {
|
|
|
1216
1468
|
}
|
|
1217
1469
|
this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
|
|
1218
1470
|
this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
|
|
1219
|
-
data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, '');
|
|
1471
|
+
data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
|
|
1472
|
+
/*
|
|
1473
|
+
const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
|
|
1474
|
+
if (plugin) {
|
|
1475
|
+
this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
|
|
1476
|
+
await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
|
|
1477
|
+
this.wssSendRestartRequired();
|
|
1478
|
+
this.wssSendRefreshRequired('plugins');
|
|
1479
|
+
this.wssSendRefreshRequired('devices');
|
|
1480
|
+
this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
|
|
1481
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1482
|
+
} else {
|
|
1483
|
+
this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
|
|
1484
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
|
|
1485
|
+
}
|
|
1486
|
+
*/
|
|
1220
1487
|
const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
|
|
1221
1488
|
if (plugin) {
|
|
1222
1489
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1230,6 +1497,7 @@ export class Frontend extends EventEmitter {
|
|
|
1230
1497
|
return;
|
|
1231
1498
|
})
|
|
1232
1499
|
.catch((_error) => {
|
|
1500
|
+
//
|
|
1233
1501
|
});
|
|
1234
1502
|
}
|
|
1235
1503
|
else {
|
|
@@ -1244,6 +1512,10 @@ export class Frontend extends EventEmitter {
|
|
|
1244
1512
|
}
|
|
1245
1513
|
this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
|
|
1246
1514
|
this.log.debug(`Removing plugin ${data.params.pluginName}...`);
|
|
1515
|
+
/*
|
|
1516
|
+
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);
|
|
1517
|
+
await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
|
|
1518
|
+
*/
|
|
1247
1519
|
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1248
1520
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
1249
1521
|
await this.matterbridge.plugins.remove(data.params.pluginName);
|
|
@@ -1279,6 +1551,7 @@ export class Frontend extends EventEmitter {
|
|
|
1279
1551
|
return;
|
|
1280
1552
|
})
|
|
1281
1553
|
.catch((_error) => {
|
|
1554
|
+
//
|
|
1282
1555
|
});
|
|
1283
1556
|
}
|
|
1284
1557
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1304,6 +1577,7 @@ export class Frontend extends EventEmitter {
|
|
|
1304
1577
|
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1305
1578
|
await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
|
|
1306
1579
|
if (plugin.serverNode) {
|
|
1580
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1307
1581
|
await this.matterbridge.stopServerNode(plugin.serverNode);
|
|
1308
1582
|
plugin.serverNode = undefined;
|
|
1309
1583
|
}
|
|
@@ -1313,18 +1587,20 @@ export class Frontend extends EventEmitter {
|
|
|
1313
1587
|
this.matterbridge.devices.remove(device);
|
|
1314
1588
|
}
|
|
1315
1589
|
}
|
|
1590
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1316
1591
|
if (plugin.type === 'DynamicPlatform' && !plugin.locked)
|
|
1317
1592
|
await this.matterbridge.createDynamicPlugin(plugin);
|
|
1318
1593
|
await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
|
|
1319
|
-
plugin.restartRequired = false;
|
|
1594
|
+
plugin.restartRequired = false; // Reset plugin restartRequired
|
|
1320
1595
|
let needRestart = 0;
|
|
1321
1596
|
for (const plugin of this.matterbridge.plugins) {
|
|
1322
1597
|
if (plugin.restartRequired)
|
|
1323
1598
|
needRestart++;
|
|
1324
1599
|
}
|
|
1325
1600
|
if (needRestart === 0) {
|
|
1326
|
-
this.wssSendRestartNotRequired(true);
|
|
1601
|
+
this.wssSendRestartNotRequired(true); // Reset global restart required message
|
|
1327
1602
|
}
|
|
1603
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1328
1604
|
if (plugin.serverNode)
|
|
1329
1605
|
await this.matterbridge.startServerNode(plugin.serverNode);
|
|
1330
1606
|
this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
|
|
@@ -1469,6 +1745,9 @@ export class Frontend extends EventEmitter {
|
|
|
1469
1745
|
this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
|
|
1470
1746
|
}
|
|
1471
1747
|
if (data.params.advertise) {
|
|
1748
|
+
// TODO: matter.js 0.16.0
|
|
1749
|
+
// await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
|
|
1750
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1472
1751
|
const advertiser = serverNode.env.get(DeviceAdvertiser);
|
|
1473
1752
|
if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
|
|
1474
1753
|
await advertiser.advertise(true);
|
|
@@ -1594,22 +1873,22 @@ export class Frontend extends EventEmitter {
|
|
|
1594
1873
|
if (isValidString(data.params.value, 4)) {
|
|
1595
1874
|
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
1596
1875
|
if (data.params.value === 'Debug') {
|
|
1597
|
-
await this.matterbridge.setLogLevel("debug");
|
|
1876
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
1598
1877
|
}
|
|
1599
1878
|
else if (data.params.value === 'Info') {
|
|
1600
|
-
await this.matterbridge.setLogLevel("info");
|
|
1879
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
1601
1880
|
}
|
|
1602
1881
|
else if (data.params.value === 'Notice') {
|
|
1603
|
-
await this.matterbridge.setLogLevel("notice");
|
|
1882
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
1604
1883
|
}
|
|
1605
1884
|
else if (data.params.value === 'Warn') {
|
|
1606
|
-
await this.matterbridge.setLogLevel("warn");
|
|
1885
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
1607
1886
|
}
|
|
1608
1887
|
else if (data.params.value === 'Error') {
|
|
1609
|
-
await this.matterbridge.setLogLevel("error");
|
|
1888
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
1610
1889
|
}
|
|
1611
1890
|
else if (data.params.value === 'Fatal') {
|
|
1612
|
-
await this.matterbridge.setLogLevel("fatal");
|
|
1891
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
1613
1892
|
}
|
|
1614
1893
|
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
1615
1894
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
@@ -1620,6 +1899,7 @@ export class Frontend extends EventEmitter {
|
|
|
1620
1899
|
this.log.debug('Matterbridge file log:', data.params.value);
|
|
1621
1900
|
this.matterbridge.fileLogger = data.params.value;
|
|
1622
1901
|
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
1902
|
+
// Create the file logger for matterbridge
|
|
1623
1903
|
if (data.params.value)
|
|
1624
1904
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
|
|
1625
1905
|
else
|
|
@@ -1649,11 +1929,12 @@ export class Frontend extends EventEmitter {
|
|
|
1649
1929
|
Logger.level = MatterLogLevel.FATAL;
|
|
1650
1930
|
}
|
|
1651
1931
|
this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1932
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
1933
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
1934
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
1935
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
1936
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
1937
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
1657
1938
|
AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
|
|
1658
1939
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
1659
1940
|
await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
|
|
@@ -1705,6 +1986,7 @@ export class Frontend extends EventEmitter {
|
|
|
1705
1986
|
}
|
|
1706
1987
|
break;
|
|
1707
1988
|
case 'setmatterport':
|
|
1989
|
+
// eslint-disable-next-line no-case-declarations
|
|
1708
1990
|
const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1709
1991
|
if (isValidNumber(port, 5540, 5600)) {
|
|
1710
1992
|
this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
|
|
@@ -1724,6 +2006,7 @@ export class Frontend extends EventEmitter {
|
|
|
1724
2006
|
}
|
|
1725
2007
|
break;
|
|
1726
2008
|
case 'setmatterdiscriminator':
|
|
2009
|
+
// eslint-disable-next-line no-case-declarations
|
|
1727
2010
|
const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1728
2011
|
if (isValidNumber(discriminator, 0, 4095)) {
|
|
1729
2012
|
this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
|
|
@@ -1743,6 +2026,7 @@ export class Frontend extends EventEmitter {
|
|
|
1743
2026
|
}
|
|
1744
2027
|
break;
|
|
1745
2028
|
case 'setmatterpasscode':
|
|
2029
|
+
// eslint-disable-next-line no-case-declarations
|
|
1746
2030
|
const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
1747
2031
|
if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
|
|
1748
2032
|
this.matterbridge.passcode = passcode;
|
|
@@ -1788,15 +2072,19 @@ export class Frontend extends EventEmitter {
|
|
|
1788
2072
|
return;
|
|
1789
2073
|
}
|
|
1790
2074
|
const config = plugin.configJson;
|
|
2075
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1791
2076
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2077
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1792
2078
|
if (select === 'serial')
|
|
1793
2079
|
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
1794
2080
|
if (select === 'name')
|
|
1795
2081
|
this.log.info(`Selected device name ${data.params.name}`);
|
|
1796
2082
|
if (config && select && (select === 'serial' || select === 'name')) {
|
|
2083
|
+
// Remove postfix from the serial if it exists
|
|
1797
2084
|
if (config.postfix) {
|
|
1798
2085
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1799
2086
|
}
|
|
2087
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
1800
2088
|
if (isValidArray(config.whiteList, 1)) {
|
|
1801
2089
|
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
1802
2090
|
config.whiteList.push(data.params.serial);
|
|
@@ -1805,6 +2093,7 @@ export class Frontend extends EventEmitter {
|
|
|
1805
2093
|
config.whiteList.push(data.params.name);
|
|
1806
2094
|
}
|
|
1807
2095
|
}
|
|
2096
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
1808
2097
|
if (isValidArray(config.blackList, 1)) {
|
|
1809
2098
|
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
1810
2099
|
config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
|
|
@@ -1832,7 +2121,9 @@ export class Frontend extends EventEmitter {
|
|
|
1832
2121
|
return;
|
|
1833
2122
|
}
|
|
1834
2123
|
const config = plugin.configJson;
|
|
2124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1835
2125
|
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2126
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
1836
2127
|
if (select === 'serial')
|
|
1837
2128
|
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
1838
2129
|
if (select === 'name')
|
|
@@ -1841,6 +2132,7 @@ export class Frontend extends EventEmitter {
|
|
|
1841
2132
|
if (config.postfix) {
|
|
1842
2133
|
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
1843
2134
|
}
|
|
2135
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
1844
2136
|
if (isValidArray(config.whiteList, 1)) {
|
|
1845
2137
|
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
1846
2138
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
|
|
@@ -1849,6 +2141,7 @@ export class Frontend extends EventEmitter {
|
|
|
1849
2141
|
config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
|
|
1850
2142
|
}
|
|
1851
2143
|
}
|
|
2144
|
+
// Add the serial to the blackList
|
|
1852
2145
|
if (isValidArray(config.blackList)) {
|
|
1853
2146
|
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
1854
2147
|
config.blackList.push(data.params.serial);
|
|
@@ -1871,6 +2164,7 @@ export class Frontend extends EventEmitter {
|
|
|
1871
2164
|
}
|
|
1872
2165
|
}
|
|
1873
2166
|
else {
|
|
2167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1874
2168
|
const localData = data;
|
|
1875
2169
|
this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
|
|
1876
2170
|
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
|
|
@@ -1880,23 +2174,46 @@ export class Frontend extends EventEmitter {
|
|
|
1880
2174
|
inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
|
|
1881
2175
|
}
|
|
1882
2176
|
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
2179
|
+
*
|
|
2180
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
2181
|
+
* @param {string} time - The time string of the message
|
|
2182
|
+
* @param {string} name - The logger name of the message
|
|
2183
|
+
* @param {string} message - The content of the message.
|
|
2184
|
+
*
|
|
2185
|
+
* @remarks
|
|
2186
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
2187
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
2188
|
+
* The function sends the message to all connected clients.
|
|
2189
|
+
*/
|
|
1883
2190
|
wssSendLogMessage(level, time, name, message) {
|
|
1884
2191
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1885
2192
|
return;
|
|
1886
2193
|
if (!level || !time || !name || !message)
|
|
1887
2194
|
return;
|
|
2195
|
+
// Remove ANSI escape codes from the message
|
|
2196
|
+
// eslint-disable-next-line no-control-regex
|
|
1888
2197
|
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
2198
|
+
// Remove leading asterisks from the message
|
|
1889
2199
|
message = message.replace(/^\*+/, '');
|
|
2200
|
+
// Replace all occurrences of \t and \n
|
|
1890
2201
|
message = message.replace(/[\t\n]/g, '');
|
|
2202
|
+
// Remove non-printable characters
|
|
2203
|
+
// eslint-disable-next-line no-control-regex
|
|
1891
2204
|
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
2205
|
+
// Replace all occurrences of \" with "
|
|
1892
2206
|
message = message.replace(/\\"/g, '"');
|
|
2207
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
1893
2208
|
const maxContinuousLength = 100;
|
|
1894
2209
|
const keepStartLength = 20;
|
|
1895
2210
|
const keepEndLength = 20;
|
|
2211
|
+
// Split the message into words
|
|
1896
2212
|
if (level !== 'spawn') {
|
|
1897
2213
|
message = message
|
|
1898
2214
|
.split(' ')
|
|
1899
2215
|
.map((word) => {
|
|
2216
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
1900
2217
|
if (word.length > maxContinuousLength) {
|
|
1901
2218
|
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
1902
2219
|
}
|
|
@@ -1904,14 +2221,34 @@ export class Frontend extends EventEmitter {
|
|
|
1904
2221
|
})
|
|
1905
2222
|
.join(' ');
|
|
1906
2223
|
}
|
|
2224
|
+
// Send the message to all connected clients
|
|
1907
2225
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
1908
2226
|
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
2229
|
+
*
|
|
2230
|
+
* @param {string} changed - The changed value.
|
|
2231
|
+
* @param {Record<string, unknown>} params - Additional parameters to send with the message.
|
|
2232
|
+
* possible values for changed:
|
|
2233
|
+
* - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
|
|
2234
|
+
* - 'plugins'
|
|
2235
|
+
* - 'devices'
|
|
2236
|
+
* - 'matter' with param 'matter' (QRDiv component)
|
|
2237
|
+
* @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
|
|
2238
|
+
*/
|
|
1909
2239
|
wssSendRefreshRequired(changed, params) {
|
|
1910
2240
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1911
2241
|
return;
|
|
1912
2242
|
this.log.debug('Sending a refresh required message to all connected clients');
|
|
2243
|
+
// Send the message to all connected clients
|
|
1913
2244
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
|
|
1914
2245
|
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
2248
|
+
*
|
|
2249
|
+
* @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
|
|
2250
|
+
* @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
|
|
2251
|
+
*/
|
|
1915
2252
|
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
1916
2253
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1917
2254
|
return;
|
|
@@ -1920,8 +2257,14 @@ export class Frontend extends EventEmitter {
|
|
|
1920
2257
|
this.matterbridge.fixedRestartRequired = fixed;
|
|
1921
2258
|
if (snackbar === true)
|
|
1922
2259
|
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
2260
|
+
// Send the message to all connected clients
|
|
1923
2261
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
1924
2262
|
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Sends a no need to restart WebSocket message to all connected clients.
|
|
2265
|
+
*
|
|
2266
|
+
* @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
|
|
2267
|
+
*/
|
|
1925
2268
|
wssSendRestartNotRequired(snackbar = true) {
|
|
1926
2269
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1927
2270
|
return;
|
|
@@ -1929,57 +2272,133 @@ export class Frontend extends EventEmitter {
|
|
|
1929
2272
|
this.matterbridge.restartRequired = false;
|
|
1930
2273
|
if (snackbar === true)
|
|
1931
2274
|
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
2275
|
+
// Send the message to all connected clients
|
|
1932
2276
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
1933
2277
|
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
2280
|
+
*
|
|
2281
|
+
* @param {boolean} devVersion - If true, the update is for a development version. Default is false.
|
|
2282
|
+
*/
|
|
1934
2283
|
wssSendUpdateRequired(devVersion = false) {
|
|
1935
2284
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1936
2285
|
return;
|
|
1937
2286
|
this.log.debug('Sending an update required message to all connected clients');
|
|
1938
2287
|
this.matterbridge.updateRequired = true;
|
|
2288
|
+
// Send the message to all connected clients
|
|
1939
2289
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
1940
2290
|
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Sends a cpu update message to all connected clients.
|
|
2293
|
+
*
|
|
2294
|
+
* @param {number} cpuUsage - The CPU usage percentage to send.
|
|
2295
|
+
* @param {number} processCpuUsage - The CPU usage percentage of the process to send.
|
|
2296
|
+
*/
|
|
1941
2297
|
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
1942
2298
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1943
2299
|
return;
|
|
1944
2300
|
if (hasParameter('debug'))
|
|
1945
2301
|
this.log.debug('Sending a cpu update message to all connected clients');
|
|
2302
|
+
// Send the message to all connected clients
|
|
1946
2303
|
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 } });
|
|
1947
2304
|
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Sends a memory update message to all connected clients.
|
|
2307
|
+
*
|
|
2308
|
+
* @param {string} totalMemory - The total memory in bytes.
|
|
2309
|
+
* @param {string} freeMemory - The free memory in bytes.
|
|
2310
|
+
* @param {string} rss - The resident set size in bytes.
|
|
2311
|
+
* @param {string} heapTotal - The total heap memory in bytes.
|
|
2312
|
+
* @param {string} heapUsed - The used heap memory in bytes.
|
|
2313
|
+
* @param {string} external - The external memory in bytes.
|
|
2314
|
+
* @param {string} arrayBuffers - The array buffers memory in bytes.
|
|
2315
|
+
*/
|
|
1948
2316
|
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
1949
2317
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1950
2318
|
return;
|
|
1951
2319
|
if (hasParameter('debug'))
|
|
1952
2320
|
this.log.debug('Sending a memory update message to all connected clients');
|
|
2321
|
+
// Send the message to all connected clients
|
|
1953
2322
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'memory_update', success: true, response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers } });
|
|
1954
2323
|
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Sends an uptime update message to all connected clients.
|
|
2326
|
+
*
|
|
2327
|
+
* @param {string} systemUptime - The system uptime in a human-readable format.
|
|
2328
|
+
* @param {string} processUptime - The process uptime in a human-readable format.
|
|
2329
|
+
*/
|
|
1955
2330
|
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
1956
2331
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1957
2332
|
return;
|
|
1958
2333
|
if (hasParameter('debug'))
|
|
1959
2334
|
this.log.debug('Sending a uptime update message to all connected clients');
|
|
2335
|
+
// Send the message to all connected clients
|
|
1960
2336
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
1961
2337
|
}
|
|
2338
|
+
/**
|
|
2339
|
+
* Sends an open snackbar message to all connected clients.
|
|
2340
|
+
*
|
|
2341
|
+
* @param {string} message - The message to send.
|
|
2342
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
|
|
2343
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
|
|
2344
|
+
* possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
|
|
2345
|
+
*
|
|
2346
|
+
* @remarks
|
|
2347
|
+
* If timeout is 0, the snackbar message will be displayed until closed by the user.
|
|
2348
|
+
*/
|
|
1962
2349
|
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
1963
2350
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1964
2351
|
return;
|
|
1965
2352
|
this.log.debug('Sending a snackbar message to all connected clients');
|
|
2353
|
+
// Send the message to all connected clients
|
|
1966
2354
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
1967
2355
|
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Sends a close snackbar message to all connected clients.
|
|
2358
|
+
* It will close the snackbar message with the same message and timeout = 0.
|
|
2359
|
+
*
|
|
2360
|
+
* @param {string} message - The message to send.
|
|
2361
|
+
*/
|
|
1968
2362
|
wssSendCloseSnackbarMessage(message) {
|
|
1969
2363
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1970
2364
|
return;
|
|
1971
2365
|
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
2366
|
+
// Send the message to all connected clients
|
|
1972
2367
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
1973
2368
|
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
2371
|
+
*
|
|
2372
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
2373
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
2374
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
2375
|
+
* @param {EndpointNumber} number - The endpoint number where the attribute belongs.
|
|
2376
|
+
* @param {string} id - The endpoint id where the attribute belongs.
|
|
2377
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
2378
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
2379
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
2380
|
+
*
|
|
2381
|
+
* @remarks
|
|
2382
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
2383
|
+
* with the updated attribute information.
|
|
2384
|
+
*/
|
|
1974
2385
|
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
1975
2386
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1976
2387
|
return;
|
|
1977
2388
|
this.log.debug('Sending an attribute update message to all connected clients');
|
|
2389
|
+
// Send the message to all connected clients
|
|
1978
2390
|
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'state_update', success: true, response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value } });
|
|
1979
2391
|
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Sends a message to all connected clients.
|
|
2394
|
+
* This is an helper function to send a broadcast message to all connected clients.
|
|
2395
|
+
*
|
|
2396
|
+
* @param {WsMessageBroadcast} msg - The message to send.
|
|
2397
|
+
*/
|
|
1980
2398
|
wssBroadcastMessage(msg) {
|
|
1981
2399
|
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
1982
2400
|
return;
|
|
2401
|
+
// Send the message to all connected clients
|
|
1983
2402
|
const stringifiedMsg = JSON.stringify(msg);
|
|
1984
2403
|
if (msg.method !== 'log')
|
|
1985
2404
|
this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
|
|
@@ -1990,3 +2409,4 @@ export class Frontend extends EventEmitter {
|
|
|
1990
2409
|
});
|
|
1991
2410
|
}
|
|
1992
2411
|
}
|
|
2412
|
+
//# sourceMappingURL=frontend.js.map
|