@matterbridge/core 3.5.3
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/LICENSE +202 -0
- package/README.md +22 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +268 -0
- 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 +49 -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 +826 -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 +3 -0
- package/dist/clusters/export.js.map +1 -0
- package/dist/crypto/attestationDecoder.d.ts +180 -0
- package/dist/crypto/attestationDecoder.d.ts.map +1 -0
- package/dist/crypto/attestationDecoder.js +176 -0
- package/dist/crypto/attestationDecoder.js.map +1 -0
- package/dist/crypto/declarationDecoder.d.ts +72 -0
- package/dist/crypto/declarationDecoder.d.ts.map +1 -0
- package/dist/crypto/declarationDecoder.js +241 -0
- package/dist/crypto/declarationDecoder.js.map +1 -0
- package/dist/crypto/extract/342/200/220cert/342/200/220extensions.d.ts +9 -0
- package/dist/crypto/extract/342/200/220cert/342/200/220extensions.d.ts.map +1 -0
- package/dist/crypto/extract/342/200/220cert/342/200/220extensions.js +120 -0
- package/dist/crypto/extract/342/200/220cert/342/200/220extensions.js.map +1 -0
- package/dist/crypto/read-extensions.d.ts +2 -0
- package/dist/crypto/read-extensions.d.ts.map +1 -0
- package/dist/crypto/read-extensions.js +81 -0
- package/dist/crypto/read-extensions.js.map +1 -0
- package/dist/crypto/testData.d.ts +31 -0
- package/dist/crypto/testData.d.ts.map +1 -0
- package/dist/crypto/testData.js +131 -0
- package/dist/crypto/testData.js.map +1 -0
- package/dist/crypto/walk-der.d.ts +2 -0
- package/dist/crypto/walk-der.d.ts.map +1 -0
- package/dist/crypto/walk-der.js +165 -0
- package/dist/crypto/walk-der.js.map +1 -0
- package/dist/deviceManager.d.ts +135 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +270 -0
- 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 +74 -0
- package/dist/devices/airConditioner.js.map +1 -0
- package/dist/devices/basicVideoPlayer.d.ts +88 -0
- package/dist/devices/basicVideoPlayer.d.ts.map +1 -0
- package/dist/devices/basicVideoPlayer.js +155 -0
- package/dist/devices/basicVideoPlayer.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 +75 -0
- package/dist/devices/batteryStorage.js.map +1 -0
- package/dist/devices/castingVideoPlayer.d.ts +79 -0
- package/dist/devices/castingVideoPlayer.d.ts.map +1 -0
- package/dist/devices/castingVideoPlayer.js +101 -0
- package/dist/devices/castingVideoPlayer.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 +77 -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 +130 -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 +156 -0
- package/dist/devices/evse.js.map +1 -0
- package/dist/devices/export.d.ts +19 -0
- package/dist/devices/export.d.ts.map +1 -0
- package/dist/devices/export.js +23 -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 +78 -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 +84 -0
- 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 +106 -0
- 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 +147 -0
- 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 +179 -0
- 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 +190 -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 +186 -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 +268 -0
- 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 +59 -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 +120 -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 +78 -0
- 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 +166 -0
- package/dist/devices/waterHeater.js.map +1 -0
- package/dist/dgram/export.d.ts +2 -0
- package/dist/dgram/export.d.ts.map +1 -0
- package/dist/dgram/export.js +2 -0
- package/dist/dgram/export.js.map +1 -0
- package/dist/export.d.ts +32 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +39 -0
- package/dist/export.js.map +1 -0
- package/dist/frontend.d.ts +248 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +2605 -0
- package/dist/frontend.js.map +1 -0
- package/dist/helpers.d.ts +48 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +161 -0
- package/dist/helpers.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 +2 -0
- package/dist/jestutils/export.js.map +1 -0
- package/dist/jestutils/jestHelpers.d.ts +349 -0
- package/dist/jestutils/jestHelpers.d.ts.map +1 -0
- package/dist/jestutils/jestHelpers.js +980 -0
- package/dist/jestutils/jestHelpers.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 +3 -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 +3 -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 +3 -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 +3 -0
- package/dist/matter/endpoints.js.map +1 -0
- package/dist/matter/export.d.ts +4 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +5 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/types.d.ts +2 -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 +341 -0
- package/dist/matterNode.d.ts.map +1 -0
- package/dist/matterNode.js +1329 -0
- package/dist/matterNode.js.map +1 -0
- package/dist/matterbridge.d.ts +544 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +2880 -0
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +49 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +80 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +2428 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +620 -0
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +744 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +1312 -0
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +49 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +80 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +1548 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +2883 -0
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +1855 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +1270 -0
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgeEndpointTypes.d.ts +172 -0
- package/dist/matterbridgeEndpointTypes.d.ts.map +1 -0
- package/dist/matterbridgeEndpointTypes.js +28 -0
- package/dist/matterbridgeEndpointTypes.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +520 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +921 -0
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/mb_coap.d.ts +24 -0
- package/dist/mb_coap.d.ts.map +1 -0
- package/dist/mb_coap.js +89 -0
- package/dist/mb_coap.js.map +1 -0
- package/dist/mb_health.d.ts +77 -0
- package/dist/mb_health.d.ts.map +1 -0
- package/dist/mb_health.js +147 -0
- package/dist/mb_health.js.map +1 -0
- package/dist/mb_mdns.d.ts +24 -0
- package/dist/mb_mdns.d.ts.map +1 -0
- package/dist/mb_mdns.js +285 -0
- package/dist/mb_mdns.js.map +1 -0
- package/dist/pluginManager.d.ts +388 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +1574 -0
- package/dist/pluginManager.js.map +1 -0
- package/dist/spawn.d.ts +33 -0
- package/dist/spawn.d.ts.map +1 -0
- package/dist/spawn.js +165 -0
- package/dist/spawn.js.map +1 -0
- package/dist/utils/export.d.ts +2 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +2 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/workers/brand.d.ts +25 -0
- package/dist/workers/brand.d.ts.map +1 -0
- package/dist/workers/brand.extend.d.ts +10 -0
- package/dist/workers/brand.extend.d.ts.map +1 -0
- package/dist/workers/brand.extend.js +15 -0
- package/dist/workers/brand.extend.js.map +1 -0
- package/dist/workers/brand.invalid.d.ts +9 -0
- package/dist/workers/brand.invalid.d.ts.map +1 -0
- package/dist/workers/brand.invalid.js +19 -0
- package/dist/workers/brand.invalid.js.map +1 -0
- package/dist/workers/brand.js +67 -0
- package/dist/workers/brand.js.map +1 -0
- package/dist/workers/clusterTypes.d.ts +47 -0
- package/dist/workers/clusterTypes.d.ts.map +1 -0
- package/dist/workers/clusterTypes.js +57 -0
- package/dist/workers/clusterTypes.js.map +1 -0
- package/dist/workers/frontendWorker.d.ts +2 -0
- package/dist/workers/frontendWorker.d.ts.map +1 -0
- package/dist/workers/frontendWorker.js +90 -0
- package/dist/workers/frontendWorker.js.map +1 -0
- package/dist/workers/helloWorld.d.ts +2 -0
- package/dist/workers/helloWorld.d.ts.map +1 -0
- package/dist/workers/helloWorld.js +135 -0
- package/dist/workers/helloWorld.js.map +1 -0
- package/dist/workers/matterWorker.d.ts +2 -0
- package/dist/workers/matterWorker.d.ts.map +1 -0
- package/dist/workers/matterWorker.js +104 -0
- package/dist/workers/matterWorker.js.map +1 -0
- package/dist/workers/matterbridgeWorker.d.ts +2 -0
- package/dist/workers/matterbridgeWorker.d.ts.map +1 -0
- package/dist/workers/matterbridgeWorker.js +75 -0
- package/dist/workers/matterbridgeWorker.js.map +1 -0
- package/dist/workers/messageLab.d.ts +134 -0
- package/dist/workers/messageLab.d.ts.map +1 -0
- package/dist/workers/messageLab.js +129 -0
- package/dist/workers/messageLab.js.map +1 -0
- package/dist/workers/testWorker.d.ts +2 -0
- package/dist/workers/testWorker.d.ts.map +1 -0
- package/dist/workers/testWorker.js +45 -0
- package/dist/workers/testWorker.js.map +1 -0
- package/dist/workers/usage.d.ts +19 -0
- package/dist/workers/usage.d.ts.map +1 -0
- package/dist/workers/usage.js +140 -0
- package/dist/workers/usage.js.map +1 -0
- package/dist/workers/workerManager.d.ts +115 -0
- package/dist/workers/workerManager.d.ts.map +1 -0
- package/dist/workers/workerManager.js +464 -0
- package/dist/workers/workerManager.js.map +1 -0
- package/dist/workers/workerServer.d.ts +126 -0
- package/dist/workers/workerServer.d.ts.map +1 -0
- package/dist/workers/workerServer.js +340 -0
- package/dist/workers/workerServer.js.map +1 -0
- package/dist/workers/workerTypes.d.ts +23 -0
- package/dist/workers/workerTypes.d.ts.map +1 -0
- package/dist/workers/workerTypes.js +3 -0
- package/dist/workers/workerTypes.js.map +1 -0
- package/package.json +120 -0
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,2605 @@
|
|
|
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.3
|
|
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 */ /* istanbul ignore next */
|
|
25
|
+
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
26
|
+
console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
|
|
27
|
+
// Node modules
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import EventEmitter from 'node:events';
|
|
31
|
+
// AnsiLogger module
|
|
32
|
+
import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
|
|
33
|
+
import { Logger, Diagnostic, LogDestination, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/general';
|
|
34
|
+
import { DeviceAdvertiser, DeviceCommissioner, FabricManager } from '@matter/protocol';
|
|
35
|
+
import { FabricIndex } from '@matter/types/datatype';
|
|
36
|
+
import { CommissioningOptions } from '@matter/types/commissioning';
|
|
37
|
+
import { BridgedDeviceBasicInformation } from '@matter/types/clusters/bridged-device-basic-information';
|
|
38
|
+
import { PowerSource } from '@matter/types/clusters/power-source';
|
|
39
|
+
// @matterbridge
|
|
40
|
+
import { createZip, formatBytes, formatPercent, formatUptime, getParameter, hasParameter, inspectError, isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString, wait, withTimeout, } from '@matterbridge/utils';
|
|
41
|
+
import { MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
|
|
42
|
+
import { BroadcastServer } from '@matterbridge/thread';
|
|
43
|
+
import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
|
|
44
|
+
import { cliEmitter, lastOsCpuUsage, lastProcessCpuUsage } from './cliEmitter.js';
|
|
45
|
+
import { generateHistoryPage } from './cliHistory.js';
|
|
46
|
+
export class Frontend extends EventEmitter {
|
|
47
|
+
matterbridge;
|
|
48
|
+
log;
|
|
49
|
+
port = 8283;
|
|
50
|
+
listening = false;
|
|
51
|
+
storedPassword = undefined;
|
|
52
|
+
authClients = [];
|
|
53
|
+
expressApp;
|
|
54
|
+
httpServer;
|
|
55
|
+
httpsServer;
|
|
56
|
+
webSocketServer;
|
|
57
|
+
server;
|
|
58
|
+
debug = hasParameter('debug') || hasParameter('verbose');
|
|
59
|
+
verbose = hasParameter('verbose');
|
|
60
|
+
constructor(matterbridge) {
|
|
61
|
+
super();
|
|
62
|
+
this.matterbridge = matterbridge;
|
|
63
|
+
this.log = new AnsiLogger({
|
|
64
|
+
logName: 'Frontend',
|
|
65
|
+
logNameColor: '\x1b[38;5;97m',
|
|
66
|
+
logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */,
|
|
67
|
+
logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */,
|
|
68
|
+
});
|
|
69
|
+
this.server = new BroadcastServer('frontend', this.log);
|
|
70
|
+
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
71
|
+
}
|
|
72
|
+
destroy() {
|
|
73
|
+
this.server.off('broadcast_message', this.msgHandler.bind(this));
|
|
74
|
+
this.server.close();
|
|
75
|
+
}
|
|
76
|
+
async msgHandler(msg) {
|
|
77
|
+
if (this.server.isWorkerRequest(msg)) {
|
|
78
|
+
// istanbul ignore else
|
|
79
|
+
if (this.verbose)
|
|
80
|
+
this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
81
|
+
switch (msg.type) {
|
|
82
|
+
case 'get_log_level':
|
|
83
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
84
|
+
break;
|
|
85
|
+
case 'set_log_level':
|
|
86
|
+
this.log.logLevel = msg.params.logLevel;
|
|
87
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
88
|
+
break;
|
|
89
|
+
case 'frontend_start':
|
|
90
|
+
await this.start(msg.params.port);
|
|
91
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
92
|
+
break;
|
|
93
|
+
case 'frontend_stop':
|
|
94
|
+
await this.stop();
|
|
95
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
96
|
+
break;
|
|
97
|
+
case 'frontend_refreshrequired':
|
|
98
|
+
this.wssSendRefreshRequired(msg.params.changed, msg.params.matter ? { matter: msg.params.matter } : undefined);
|
|
99
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
100
|
+
break;
|
|
101
|
+
case 'frontend_restartrequired':
|
|
102
|
+
this.wssSendRestartRequired(msg.params.snackbar, msg.params.fixed);
|
|
103
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
104
|
+
break;
|
|
105
|
+
case 'frontend_restartnotrequired':
|
|
106
|
+
this.wssSendRestartNotRequired(msg.params.snackbar);
|
|
107
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
108
|
+
break;
|
|
109
|
+
case 'frontend_updaterequired':
|
|
110
|
+
this.wssSendUpdateRequired(msg.params.devVersion);
|
|
111
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
112
|
+
break;
|
|
113
|
+
case 'frontend_snackbarmessage':
|
|
114
|
+
this.wssSendSnackbarMessage(msg.params.message, msg.params.timeout, msg.params.severity);
|
|
115
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
116
|
+
break;
|
|
117
|
+
case 'frontend_attributechanged':
|
|
118
|
+
this.wssSendAttributeChangedMessage(msg.params.plugin, msg.params.serialNumber, msg.params.uniqueId, msg.params.number, msg.params.id, msg.params.cluster, msg.params.attribute, msg.params.value);
|
|
119
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
120
|
+
break;
|
|
121
|
+
case 'frontend_logmessage':
|
|
122
|
+
this.wssSendLogMessage(msg.params.level, msg.params.time, msg.params.name, msg.params.message);
|
|
123
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
124
|
+
break;
|
|
125
|
+
case 'frontend_broadcast_message':
|
|
126
|
+
this.wssBroadcastMessage(msg.params.msg);
|
|
127
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
// istanbul ignore next
|
|
131
|
+
if (this.verbose)
|
|
132
|
+
this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (this.server.isWorkerResponse(msg) && msg.result) {
|
|
136
|
+
// istanbul ignore next
|
|
137
|
+
if (this.verbose)
|
|
138
|
+
this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
139
|
+
switch (msg.type) {
|
|
140
|
+
case 'plugins_install':
|
|
141
|
+
this.wssSendCloseSnackbarMessage(`Installing package ${msg.result.packageName}...`);
|
|
142
|
+
if (msg.result.success) {
|
|
143
|
+
this.wssSendRestartRequired(true, true);
|
|
144
|
+
this.wssSendRefreshRequired('plugins');
|
|
145
|
+
this.wssSendSnackbarMessage(`Installed package ${msg.result.packageName}`, 5, 'success');
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.wssSendSnackbarMessage(`Package ${msg.result.packageName} not installed`, 10, 'error');
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
case 'plugins_uninstall':
|
|
152
|
+
this.wssSendCloseSnackbarMessage(`Uninstalling package ${msg.result.packageName}...`);
|
|
153
|
+
if (msg.result.success) {
|
|
154
|
+
this.wssSendRestartRequired(true, true);
|
|
155
|
+
this.wssSendRefreshRequired('plugins');
|
|
156
|
+
this.wssSendSnackbarMessage(`Uninstalled package ${msg.result.packageName}`, 5, 'success');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
this.wssSendSnackbarMessage(`Package ${msg.result.packageName} not uninstalled`, 10, 'error');
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
set logLevel(logLevel) {
|
|
166
|
+
this.log.logLevel = logLevel;
|
|
167
|
+
}
|
|
168
|
+
validateReq(req, res) {
|
|
169
|
+
if (req.ip && !this.authClients.includes(req.ip)) {
|
|
170
|
+
this.log.warn(`Warning blocked unauthorized access request ${req.originalUrl ?? req.url} from ${req.ip}`);
|
|
171
|
+
res.status(401).json({ error: 'Unauthorized' });
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
async start(port = 8283) {
|
|
177
|
+
this.port = port;
|
|
178
|
+
this.storedPassword = await this.matterbridge.nodeContext?.get('password', '');
|
|
179
|
+
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${this.port}${db}`);
|
|
180
|
+
// Initialize multer with the upload directory
|
|
181
|
+
const multer = await import('multer');
|
|
182
|
+
const uploadDir = path.join(this.matterbridge.matterbridgeDirectory, 'uploads'); // Is created by matterbridge initialize
|
|
183
|
+
const upload = multer.default({ dest: uploadDir });
|
|
184
|
+
// Create the express app that serves the frontend
|
|
185
|
+
const express = await import('express');
|
|
186
|
+
this.expressApp = express.default();
|
|
187
|
+
// Inject logging/debug wrapper for route/middleware registration
|
|
188
|
+
/*
|
|
189
|
+
const methods = ['get', 'post', 'put', 'delete', 'use'];
|
|
190
|
+
for (const method of methods) {
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
192
|
+
const original = (this.expressApp as any)[method].bind(this.expressApp);
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
|
+
(this.expressApp as any)[method] = (path: any, ...rest: any) => {
|
|
195
|
+
try {
|
|
196
|
+
console.log(`[DEBUG] Registering ${method.toUpperCase()} route:`, path);
|
|
197
|
+
return original(path, ...rest);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(`[ERROR] Failed to register route: ${path}`);
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
*/
|
|
205
|
+
// Log all requests to the server for debugging
|
|
206
|
+
/*
|
|
207
|
+
this.expressApp.use((req, res, next) => {
|
|
208
|
+
this.log.debug(`***Received request on expressApp: ${req.method} ${req.url}`);
|
|
209
|
+
next();
|
|
210
|
+
});
|
|
211
|
+
*/
|
|
212
|
+
// Serve static files from 'frontend/build' directory
|
|
213
|
+
this.expressApp.use(express.static(path.join(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build')));
|
|
214
|
+
// Create a WebSocket server and attach it to the http or https server
|
|
215
|
+
this.log.debug(`Creating WebSocketServer...`);
|
|
216
|
+
const ws = await import('ws');
|
|
217
|
+
this.webSocketServer = new ws.WebSocketServer({ noServer: true });
|
|
218
|
+
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
219
|
+
this.webSocketServer.on('connection', (ws, request) => {
|
|
220
|
+
const clientIp = request.socket.remoteAddress;
|
|
221
|
+
// Set the global logger callback for the WebSocketServer
|
|
222
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
223
|
+
// istanbul ignore else
|
|
224
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
225
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
226
|
+
// istanbul ignore else
|
|
227
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
228
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
229
|
+
AnsiLogger.setGlobalCallback(this.wssSendLogMessage.bind(this), callbackLogLevel);
|
|
230
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
231
|
+
this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
|
|
232
|
+
ws.on('message', (message) => {
|
|
233
|
+
this.wsMessageHandler(ws, message);
|
|
234
|
+
});
|
|
235
|
+
ws.on('ping', () => {
|
|
236
|
+
this.log.debug('WebSocket client ping received');
|
|
237
|
+
ws.pong();
|
|
238
|
+
});
|
|
239
|
+
ws.on('pong', () => {
|
|
240
|
+
this.log.debug('WebSocket client pong received');
|
|
241
|
+
});
|
|
242
|
+
ws.on('close', () => {
|
|
243
|
+
this.log.info('WebSocket client disconnected');
|
|
244
|
+
// istanbul ignore else
|
|
245
|
+
if (this.webSocketServer?.clients.size === 0) {
|
|
246
|
+
AnsiLogger.setGlobalCallback(undefined);
|
|
247
|
+
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
|
|
248
|
+
this.authClients = [];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// istanbul ignore next
|
|
252
|
+
ws.on('error', (error) => {
|
|
253
|
+
// istanbul ignore next
|
|
254
|
+
this.log.error(`WebSocket client error: ${error}`);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
this.webSocketServer.on('close', () => {
|
|
258
|
+
this.log.debug(`WebSocketServer closed`);
|
|
259
|
+
});
|
|
260
|
+
/* With { noServer: true } it never fires
|
|
261
|
+
this.webSocketServer.on('listening', () => {
|
|
262
|
+
this.log.info(`The WebSocketServer is listening`);
|
|
263
|
+
this.emit('websocket_server_listening', hasParameter('ssl') ? 'wss' : 'ws');
|
|
264
|
+
});
|
|
265
|
+
*/
|
|
266
|
+
// istanbul ignore next
|
|
267
|
+
this.webSocketServer.on('error', (ws, error) => {
|
|
268
|
+
this.log.error(`WebSocketServer error: ${error}`);
|
|
269
|
+
});
|
|
270
|
+
if (!hasParameter('ssl')) {
|
|
271
|
+
// Create an HTTP server and attach the express app
|
|
272
|
+
const http = await import('node:http');
|
|
273
|
+
try {
|
|
274
|
+
this.log.debug(`Creating HTTP server...`);
|
|
275
|
+
this.httpServer = http.createServer(this.expressApp);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
this.log.error(`Failed to create HTTP server: ${error}`);
|
|
279
|
+
this.emit('server_error', error);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Listen on the specified port
|
|
283
|
+
if (hasParameter('ingress')) {
|
|
284
|
+
// We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
|
|
285
|
+
this.httpServer.listen(this.port, '0.0.0.0', () => {
|
|
286
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
287
|
+
this.listening = true;
|
|
288
|
+
this.emit('server_listening', 'http', this.port, '0.0.0.0');
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
// We listen to all available addresses
|
|
293
|
+
this.httpServer.listen(this.port, getParameter('bind'), () => {
|
|
294
|
+
const addr = this.httpServer?.address();
|
|
295
|
+
// istanbul ignore else
|
|
296
|
+
if (addr && typeof addr !== 'string') {
|
|
297
|
+
this.log.info(`The frontend http server is bound to ${addr.family} ${addr.address}:${addr.port}`);
|
|
298
|
+
}
|
|
299
|
+
// istanbul ignore else
|
|
300
|
+
if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
|
|
301
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
302
|
+
// istanbul ignore else
|
|
303
|
+
if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
|
|
304
|
+
this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
305
|
+
this.listening = true;
|
|
306
|
+
this.emit('server_listening', 'http', this.port);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
this.httpServer.on('upgrade', async (req, socket, head) => {
|
|
310
|
+
try {
|
|
311
|
+
// Only proceed for real WebSocket upgrades
|
|
312
|
+
// istanbul ignore next cause is only a safety check
|
|
313
|
+
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
314
|
+
this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
|
|
315
|
+
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
316
|
+
return socket.destroy();
|
|
317
|
+
}
|
|
318
|
+
// Build a URL so we can read ?password=...
|
|
319
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host || 'localhost'}`);
|
|
320
|
+
// Validate WebSocket password
|
|
321
|
+
const password = url.searchParams.get('password') ?? '';
|
|
322
|
+
if (password !== this.storedPassword) {
|
|
323
|
+
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
324
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
325
|
+
return socket.destroy();
|
|
326
|
+
}
|
|
327
|
+
// Complete the WebSocket handshake
|
|
328
|
+
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
329
|
+
// istanbul ignore else
|
|
330
|
+
if (req.socket.remoteAddress)
|
|
331
|
+
this.authClients.push(req.socket.remoteAddress);
|
|
332
|
+
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
333
|
+
this.webSocketServer?.emit('connection', ws, req);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
338
|
+
{
|
|
339
|
+
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
340
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
341
|
+
socket.destroy();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
this.httpServer.on('error', (error) => {
|
|
346
|
+
this.log.error(`Frontend http server error listening on ${this.port}`);
|
|
347
|
+
switch (error.code) {
|
|
348
|
+
case 'EACCES':
|
|
349
|
+
this.log.error(`Port ${this.port} requires elevated privileges`);
|
|
350
|
+
break;
|
|
351
|
+
case 'EADDRINUSE':
|
|
352
|
+
this.log.error(`Port ${this.port} is already in use`);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
this.emit('server_error', error);
|
|
356
|
+
return;
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// SSL is enabled, load the certificate and the private key
|
|
361
|
+
let cert;
|
|
362
|
+
let key;
|
|
363
|
+
let ca;
|
|
364
|
+
let fullChain;
|
|
365
|
+
let pfx;
|
|
366
|
+
let passphrase;
|
|
367
|
+
let httpsServerOptions = {};
|
|
368
|
+
const fs = await import('node:fs');
|
|
369
|
+
if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'))) {
|
|
370
|
+
// Load the p12 certificate and the passphrase
|
|
371
|
+
try {
|
|
372
|
+
pfx = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12'));
|
|
373
|
+
this.log.info(`Loaded p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}`);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
this.log.error(`Error reading p12 certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.p12')}: ${error}`);
|
|
377
|
+
this.emit('server_error', error);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
passphrase = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass'), 'utf8');
|
|
382
|
+
passphrase = passphrase.trim(); // Ensure no extra characters
|
|
383
|
+
this.log.info(`Loaded p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}`);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
this.log.error(`Error reading p12 passphrase file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pass')}: ${error}`);
|
|
387
|
+
this.emit('server_error', error);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
httpsServerOptions = { pfx, passphrase };
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
// 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.
|
|
394
|
+
try {
|
|
395
|
+
cert = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem'), 'utf8');
|
|
396
|
+
this.log.info(`Loaded certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}`);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
this.log.error(`Error reading certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'cert.pem')}: ${error}`);
|
|
400
|
+
this.emit('server_error', error);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
key = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem'), 'utf8');
|
|
405
|
+
this.log.info(`Loaded key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem')}`);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
this.log.error(`Error reading key file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'key.pem')}: ${error}`);
|
|
409
|
+
this.emit('server_error', error);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
ca = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem'), 'utf8');
|
|
414
|
+
fullChain = `${cert}\n${ca}`;
|
|
415
|
+
this.log.info(`Loaded CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem')}`);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
this.log.info(`CA certificate file ${path.join(this.matterbridge.matterbridgeDirectory, 'certs', 'ca.pem')} not loaded: ${error}`);
|
|
419
|
+
}
|
|
420
|
+
httpsServerOptions = { cert: fullChain ?? cert, key, ca };
|
|
421
|
+
}
|
|
422
|
+
if (hasParameter('mtls')) {
|
|
423
|
+
httpsServerOptions.requestCert = true; // Request client certificate
|
|
424
|
+
httpsServerOptions.rejectUnauthorized = true; // Require client certificate validation
|
|
425
|
+
}
|
|
426
|
+
// Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
|
|
427
|
+
const https = await import('node:https');
|
|
428
|
+
try {
|
|
429
|
+
this.log.debug(`Creating HTTPS server...`);
|
|
430
|
+
this.httpsServer = https.createServer(httpsServerOptions, this.expressApp);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
this.log.error(`Failed to create HTTPS server: ${error}`);
|
|
434
|
+
this.emit('server_error', error);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Listen on the specified port
|
|
438
|
+
if (hasParameter('ingress')) {
|
|
439
|
+
// We limit to all ipv4 addresses when running in ingress mode (Home Assistant add-on)
|
|
440
|
+
this.httpsServer.listen(this.port, '0.0.0.0', () => {
|
|
441
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${this.port}${UNDERLINEOFF}${rs}`);
|
|
442
|
+
this.listening = true;
|
|
443
|
+
this.emit('server_listening', 'https', this.port, '0.0.0.0');
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// We listen to all available addresses
|
|
448
|
+
this.httpsServer.listen(this.port, getParameter('bind'), () => {
|
|
449
|
+
const addr = this.httpsServer?.address();
|
|
450
|
+
// istanbul ignore else
|
|
451
|
+
if (addr && typeof addr !== 'string') {
|
|
452
|
+
this.log.info(`The frontend https server is bound to ${addr.family} ${addr.address}:${addr.port}`);
|
|
453
|
+
}
|
|
454
|
+
// istanbul ignore else
|
|
455
|
+
if (this.matterbridge.systemInformation.ipv4Address !== '' && !getParameter('bind'))
|
|
456
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.matterbridge.systemInformation.ipv4Address}:${this.port}${UNDERLINEOFF}${rs}`);
|
|
457
|
+
// istanbul ignore else
|
|
458
|
+
if (this.matterbridge.systemInformation.ipv6Address !== '' && !getParameter('bind'))
|
|
459
|
+
this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.matterbridge.systemInformation.ipv6Address}]:${this.port}${UNDERLINEOFF}${rs}`);
|
|
460
|
+
this.listening = true;
|
|
461
|
+
this.emit('server_listening', 'https', this.port);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
this.httpsServer.on('upgrade', async (req, socket, head) => {
|
|
465
|
+
try {
|
|
466
|
+
// Only proceed for real WebSocket upgrades
|
|
467
|
+
// istanbul ignore next cause is only a safety check
|
|
468
|
+
if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
|
|
469
|
+
socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
470
|
+
return socket.destroy();
|
|
471
|
+
}
|
|
472
|
+
// Build a URL so we can read ?password=...
|
|
473
|
+
const url = new URL(req.url ?? '/', `https://${req.headers.host || 'localhost'}`);
|
|
474
|
+
// Validate WebSocket password
|
|
475
|
+
const password = url.searchParams.get('password') ?? '';
|
|
476
|
+
if (password !== this.storedPassword) {
|
|
477
|
+
this.log.error(`WebSocket upgrade error: Invalid password ${password ? '[redacted]' : '(empty)'}`);
|
|
478
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
479
|
+
return socket.destroy();
|
|
480
|
+
}
|
|
481
|
+
// Complete the WebSocket handshake
|
|
482
|
+
this.log.debug(`WebSocket upgrade success host ${url.host} password ${password ? '[redacted]' : '(empty)'}`);
|
|
483
|
+
// istanbul ignore else
|
|
484
|
+
if (req.socket.remoteAddress)
|
|
485
|
+
this.authClients.push(req.socket.remoteAddress);
|
|
486
|
+
this.webSocketServer?.handleUpgrade(req, socket, head, (ws) => {
|
|
487
|
+
this.webSocketServer?.emit('connection', ws, req);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
/* istanbul ignore next: only triggered on unexpected internal error */
|
|
492
|
+
{
|
|
493
|
+
inspectError(this.log, 'WebSocket upgrade error:', err);
|
|
494
|
+
socket.write('HTTP/1.1 500 Internal Server Error\r\nConnection: close\r\n\r\n');
|
|
495
|
+
socket.destroy();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.httpsServer.on('error', (error) => {
|
|
500
|
+
this.log.error(`Frontend https server error listening on ${this.port}`);
|
|
501
|
+
switch (error.code) {
|
|
502
|
+
case 'EACCES':
|
|
503
|
+
this.log.error(`Port ${this.port} requires elevated privileges`);
|
|
504
|
+
break;
|
|
505
|
+
case 'EADDRINUSE':
|
|
506
|
+
this.log.error(`Port ${this.port} is already in use`);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
this.emit('server_error', error);
|
|
510
|
+
return;
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
// Subscribe to cli events
|
|
514
|
+
cliEmitter.removeAllListeners();
|
|
515
|
+
cliEmitter.on('uptime', (systemUptime, processUptime) => {
|
|
516
|
+
this.wssSendUptimeUpdate(systemUptime, processUptime);
|
|
517
|
+
});
|
|
518
|
+
cliEmitter.on('memory', (totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) => {
|
|
519
|
+
this.wssSendMemoryUpdate(totalMememory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers);
|
|
520
|
+
});
|
|
521
|
+
cliEmitter.on('cpu', (cpuUsage, processCpuUsage) => {
|
|
522
|
+
this.wssSendCpuUpdate(cpuUsage, processCpuUsage);
|
|
523
|
+
});
|
|
524
|
+
// Endpoint to validate login code
|
|
525
|
+
// curl -X POST "http://localhost:8283/api/login" -H "Content-Type: application/json" -d "{\"password\":\"Here\"}"
|
|
526
|
+
this.expressApp.post('/api/login', express.json(), async (req, res) => {
|
|
527
|
+
const { password } = req.body;
|
|
528
|
+
this.log.debug(`The frontend sent /api/login with password ${password ? '[redacted]' : '(empty)'}`);
|
|
529
|
+
if (this.storedPassword === '' || password === this.storedPassword) {
|
|
530
|
+
this.log.debug('/api/login password valid');
|
|
531
|
+
res.json({ valid: true });
|
|
532
|
+
if (req.ip)
|
|
533
|
+
this.authClients.push(req.ip);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
this.log.warn('/api/login error wrong password');
|
|
537
|
+
res.json({ valid: false });
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
// Endpoint to provide health check for docker
|
|
541
|
+
this.expressApp.get('/health', (req, res) => {
|
|
542
|
+
this.log.debug('Express received /health');
|
|
543
|
+
const healthStatus = {
|
|
544
|
+
status: 'ok', // Indicate service is healthy
|
|
545
|
+
uptime: process.uptime(), // Server uptime in seconds
|
|
546
|
+
timestamp: new Date().toISOString(), // Current timestamp
|
|
547
|
+
};
|
|
548
|
+
res.status(200).json(healthStatus);
|
|
549
|
+
});
|
|
550
|
+
// Endpoint to provide memory usage details
|
|
551
|
+
this.expressApp.get('/memory', async (req, res) => {
|
|
552
|
+
this.log.debug('Express received /memory');
|
|
553
|
+
// Memory usage from process
|
|
554
|
+
const memoryUsageRaw = process.memoryUsage();
|
|
555
|
+
const memoryUsage = {
|
|
556
|
+
rss: formatBytes(memoryUsageRaw.rss),
|
|
557
|
+
heapTotal: formatBytes(memoryUsageRaw.heapTotal),
|
|
558
|
+
heapUsed: formatBytes(memoryUsageRaw.heapUsed),
|
|
559
|
+
external: formatBytes(memoryUsageRaw.external),
|
|
560
|
+
arrayBuffers: formatBytes(memoryUsageRaw.arrayBuffers),
|
|
561
|
+
};
|
|
562
|
+
// V8 heap statistics
|
|
563
|
+
const { default: v8 } = await import('node:v8');
|
|
564
|
+
const heapStatsRaw = v8.getHeapStatistics();
|
|
565
|
+
const heapSpacesRaw = v8.getHeapSpaceStatistics();
|
|
566
|
+
// Format heapStats
|
|
567
|
+
const heapStats = Object.fromEntries(Object.entries(heapStatsRaw).map(([key, value]) => [key, formatBytes(value)]));
|
|
568
|
+
// Format heapSpaces
|
|
569
|
+
const heapSpaces = heapSpacesRaw.map((space) => ({
|
|
570
|
+
...space,
|
|
571
|
+
space_size: formatBytes(space.space_size),
|
|
572
|
+
space_used_size: formatBytes(space.space_used_size),
|
|
573
|
+
space_available_size: formatBytes(space.space_available_size),
|
|
574
|
+
physical_space_size: formatBytes(space.physical_space_size),
|
|
575
|
+
}));
|
|
576
|
+
const { createRequire } = await import('node:module');
|
|
577
|
+
const require = createRequire(import.meta.url);
|
|
578
|
+
const cjsModules = Object.keys(require.cache).sort();
|
|
579
|
+
const memoryReport = {
|
|
580
|
+
memoryUsage,
|
|
581
|
+
heapStats,
|
|
582
|
+
heapSpaces,
|
|
583
|
+
cjsModules,
|
|
584
|
+
};
|
|
585
|
+
res.status(200).json(memoryReport);
|
|
586
|
+
});
|
|
587
|
+
// Endpoint to provide settings
|
|
588
|
+
this.expressApp.get('/api/settings', express.json(), async (req, res) => {
|
|
589
|
+
this.log.debug('The frontend sent /api/settings');
|
|
590
|
+
if (!this.validateReq(req, res))
|
|
591
|
+
return;
|
|
592
|
+
res.json(await this.getApiSettings());
|
|
593
|
+
});
|
|
594
|
+
// Endpoint to provide plugins
|
|
595
|
+
this.expressApp.get('/api/plugins', async (req, res) => {
|
|
596
|
+
this.log.debug('The frontend sent /api/plugins');
|
|
597
|
+
if (!this.validateReq(req, res))
|
|
598
|
+
return;
|
|
599
|
+
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getPlugins());
|
|
600
|
+
});
|
|
601
|
+
// Endpoint to provide devices
|
|
602
|
+
this.expressApp.get('/api/devices', async (req, res) => {
|
|
603
|
+
this.log.debug('The frontend sent /api/devices');
|
|
604
|
+
if (!this.validateReq(req, res))
|
|
605
|
+
return;
|
|
606
|
+
res.json(this.matterbridge.hasCleanupStarted ? [] : this.getDevices());
|
|
607
|
+
});
|
|
608
|
+
// Endpoint to view the matterbridge log
|
|
609
|
+
this.expressApp.get('/api/view-mblog', async (req, res) => {
|
|
610
|
+
this.log.debug('The frontend sent /api/view-mblog');
|
|
611
|
+
if (!this.validateReq(req, res))
|
|
612
|
+
return;
|
|
613
|
+
try {
|
|
614
|
+
const fs = await import('node:fs');
|
|
615
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), 'utf8');
|
|
616
|
+
res.type('text/plain');
|
|
617
|
+
res.send(data);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
this.log.error(`Error reading matterbridge log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
621
|
+
res.status(500).send('Error reading matterbridge log file. Please enable the matterbridge log on file in the settings.');
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// Endpoint to view the matter.js log
|
|
625
|
+
this.expressApp.get('/api/view-mjlog', async (req, res) => {
|
|
626
|
+
this.log.debug('The frontend sent /api/view-mjlog');
|
|
627
|
+
if (!this.validateReq(req, res))
|
|
628
|
+
return;
|
|
629
|
+
try {
|
|
630
|
+
const fs = await import('node:fs');
|
|
631
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), 'utf8');
|
|
632
|
+
res.type('text/plain');
|
|
633
|
+
res.send(data);
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
this.log.error(`Error reading matter log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
637
|
+
res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
// Endpoint to view the diagnostic.log
|
|
641
|
+
this.expressApp.get('/api/view-diagnostic', async (req, res) => {
|
|
642
|
+
this.log.debug('The frontend sent /api/view-diagnostic');
|
|
643
|
+
if (!this.validateReq(req, res))
|
|
644
|
+
return;
|
|
645
|
+
await this.generateDiagnostic();
|
|
646
|
+
try {
|
|
647
|
+
const fs = await import('node:fs');
|
|
648
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), 'utf8');
|
|
649
|
+
res.type('text/plain');
|
|
650
|
+
res.send(data.slice(29));
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
// istanbul ignore next
|
|
654
|
+
this.log.error(`Error reading diagnostic log file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
655
|
+
// istanbul ignore next
|
|
656
|
+
res.status(500).send('Error reading diagnostic log file.');
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
// Endpoint to download the diagnostic.log
|
|
660
|
+
this.expressApp.get('/api/download-diagnostic', async (req, res) => {
|
|
661
|
+
this.log.debug(`The frontend sent /api/download-diagnostic`);
|
|
662
|
+
if (!this.validateReq(req, res))
|
|
663
|
+
return;
|
|
664
|
+
await this.generateDiagnostic();
|
|
665
|
+
try {
|
|
666
|
+
const fs = await import('node:fs');
|
|
667
|
+
await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), fs.constants.F_OK);
|
|
668
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), 'utf8');
|
|
669
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), data, 'utf-8');
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
// istanbul ignore next
|
|
673
|
+
this.log.debug(`Error in /api/download-diagnostic: ${error instanceof Error ? error.message : error}`);
|
|
674
|
+
}
|
|
675
|
+
res.type('text/plain');
|
|
676
|
+
res.download(path.join(os.tmpdir(), MATTERBRIDGE_DIAGNOSTIC_FILE), MATTERBRIDGE_DIAGNOSTIC_FILE, (error) => {
|
|
677
|
+
/* istanbul ignore if */
|
|
678
|
+
if (error) {
|
|
679
|
+
this.log.error(`Error downloading file ${MATTERBRIDGE_DIAGNOSTIC_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
680
|
+
res.status(500).send('Error downloading the diagnostic log file');
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
// Endpoint to view the history.html
|
|
685
|
+
this.expressApp.get('/api/viewhistory', async (req, res) => {
|
|
686
|
+
this.log.debug('The frontend sent /api/viewhistory');
|
|
687
|
+
if (!this.validateReq(req, res))
|
|
688
|
+
return;
|
|
689
|
+
try {
|
|
690
|
+
const fs = await import('node:fs');
|
|
691
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), 'utf8');
|
|
692
|
+
res.type('text/html');
|
|
693
|
+
res.send(data);
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
this.log.error(`Error in /api/viewhistory reading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
697
|
+
res.status(500).send('Error reading history file.');
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
// Endpoint to download the history.html
|
|
701
|
+
this.expressApp.get('/api/downloadhistory', async (req, res) => {
|
|
702
|
+
this.log.debug(`The frontend sent /api/downloadhistory`);
|
|
703
|
+
if (!this.validateReq(req, res))
|
|
704
|
+
return;
|
|
705
|
+
try {
|
|
706
|
+
const fs = await import('node:fs');
|
|
707
|
+
await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), fs.constants.F_OK);
|
|
708
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), 'utf8');
|
|
709
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), data, 'utf-8');
|
|
710
|
+
res.type('text/plain');
|
|
711
|
+
res.download(path.join(os.tmpdir(), MATTERBRIDGE_HISTORY_FILE), MATTERBRIDGE_HISTORY_FILE, (error) => {
|
|
712
|
+
/* istanbul ignore if */
|
|
713
|
+
if (error) {
|
|
714
|
+
this.log.error(`Error in /api/downloadhistory downloading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
715
|
+
res.status(500).send('Error downloading history file');
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
this.log.error(`Error in /api/downloadhistory reading history file ${MATTERBRIDGE_HISTORY_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
721
|
+
res.status(500).send('Error reading history file.');
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// Endpoint to view the shelly log
|
|
725
|
+
this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
|
|
726
|
+
this.log.debug('The frontend sent /api/shellyviewsystemlog');
|
|
727
|
+
if (!this.validateReq(req, res))
|
|
728
|
+
return;
|
|
729
|
+
try {
|
|
730
|
+
const fs = await import('node:fs');
|
|
731
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
|
|
732
|
+
res.type('text/plain');
|
|
733
|
+
res.send(data);
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
this.log.error(`Error reading shelly log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
737
|
+
res.status(500).send('Error reading shelly log file. Please create the shelly system log before loading it.');
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
// Endpoint to download the matterbridge log
|
|
741
|
+
this.expressApp.get('/api/download-mblog', async (req, res) => {
|
|
742
|
+
this.log.debug(`The frontend sent /api/download-mblog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
743
|
+
if (!this.validateReq(req, res))
|
|
744
|
+
return;
|
|
745
|
+
const fs = await import('node:fs');
|
|
746
|
+
try {
|
|
747
|
+
await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), fs.constants.F_OK);
|
|
748
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), 'utf8');
|
|
749
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), data, 'utf-8');
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'Enable the matterbridge log on file in the settings to download the matterbridge log.', 'utf-8');
|
|
753
|
+
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
754
|
+
}
|
|
755
|
+
res.type('text/plain');
|
|
756
|
+
res.download(path.join(os.tmpdir(), MATTERBRIDGE_LOGGER_FILE), 'matterbridge.log', (error) => {
|
|
757
|
+
/* istanbul ignore if */
|
|
758
|
+
if (error) {
|
|
759
|
+
this.log.error(`Error downloading log file ${MATTERBRIDGE_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
760
|
+
res.status(500).send('Error downloading the matterbridge log file');
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
// Endpoint to download the matter log
|
|
765
|
+
this.expressApp.get('/api/download-mjlog', async (req, res) => {
|
|
766
|
+
this.log.debug(`The frontend sent /api/download-mjlog ${path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE)}`);
|
|
767
|
+
if (!this.validateReq(req, res))
|
|
768
|
+
return;
|
|
769
|
+
const fs = await import('node:fs');
|
|
770
|
+
try {
|
|
771
|
+
await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), fs.constants.F_OK);
|
|
772
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE), 'utf8');
|
|
773
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTER_LOGGER_FILE), data, 'utf-8');
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'Enable the matter log on file in the settings to download the matter log.', 'utf-8');
|
|
777
|
+
this.log.debug(`Error in /api/download-mblog: ${error instanceof Error ? error.message : error}`);
|
|
778
|
+
}
|
|
779
|
+
res.type('text/plain');
|
|
780
|
+
res.download(path.join(os.tmpdir(), MATTER_LOGGER_FILE), 'matter.log', (error) => {
|
|
781
|
+
/* istanbul ignore if */
|
|
782
|
+
if (error) {
|
|
783
|
+
this.log.error(`Error downloading log file ${MATTER_LOGGER_FILE}: ${error instanceof Error ? error.message : error}`);
|
|
784
|
+
res.status(500).send('Error downloading the matter log file');
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
// Endpoint to download the shelly log
|
|
789
|
+
this.expressApp.get('/api/shellydownloadsystemlog', async (req, res) => {
|
|
790
|
+
this.log.debug('The frontend sent /api/shellydownloadsystemlog');
|
|
791
|
+
if (!this.validateReq(req, res))
|
|
792
|
+
return;
|
|
793
|
+
const fs = await import('node:fs');
|
|
794
|
+
try {
|
|
795
|
+
await fs.promises.access(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), fs.constants.F_OK);
|
|
796
|
+
const data = await fs.promises.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'shelly.log'), 'utf8');
|
|
797
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), 'shelly.log'), data, 'utf-8');
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
await fs.promises.writeFile(path.join(os.tmpdir(), 'shelly.log'), 'Create the Shelly system log before downloading it.', 'utf-8');
|
|
801
|
+
this.log.debug(`Error in /api/shellydownloadsystemlog: ${error instanceof Error ? error.message : error}`);
|
|
802
|
+
}
|
|
803
|
+
res.type('text/plain');
|
|
804
|
+
res.download(path.join(os.tmpdir(), 'shelly.log'), 'shelly.log', (error) => {
|
|
805
|
+
/* istanbul ignore if */
|
|
806
|
+
if (error) {
|
|
807
|
+
this.log.error(`Error downloading Shelly system log file: ${error instanceof Error ? error.message : error}`);
|
|
808
|
+
res.status(500).send('Error downloading Shelly system log file');
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
// Endpoint to download the matterbridge storage directory
|
|
813
|
+
this.expressApp.get('/api/download-mbstorage', async (req, res) => {
|
|
814
|
+
this.log.debug('The frontend sent /api/download-mbstorage');
|
|
815
|
+
if (!this.validateReq(req, res))
|
|
816
|
+
return;
|
|
817
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR));
|
|
818
|
+
res.download(path.join(os.tmpdir(), `matterbridge.${NODE_STORAGE_DIR}.zip`), `matterbridge.${NODE_STORAGE_DIR}.zip`, (error) => {
|
|
819
|
+
/* istanbul ignore if */
|
|
820
|
+
if (error) {
|
|
821
|
+
this.log.error(`Error downloading file ${`matterbridge.${NODE_STORAGE_DIR}.zip`}: ${error instanceof Error ? error.message : error}`);
|
|
822
|
+
res.status(500).send('Error downloading the matterbridge storage file');
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
// Endpoint to download the matter storage file
|
|
827
|
+
this.expressApp.get('/api/download-mjstorage', async (req, res) => {
|
|
828
|
+
this.log.debug('The frontend sent /api/download-mjstorage');
|
|
829
|
+
if (!this.validateReq(req, res))
|
|
830
|
+
return;
|
|
831
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
832
|
+
res.download(path.join(os.tmpdir(), `matterbridge.${MATTER_STORAGE_NAME}.zip`), `matterbridge.${MATTER_STORAGE_NAME}.zip`, (error) => {
|
|
833
|
+
/* istanbul ignore if */
|
|
834
|
+
if (error) {
|
|
835
|
+
this.log.error(`Error downloading the matter storage matterbridge.${MATTER_STORAGE_NAME}.zip: ${error instanceof Error ? error.message : error}`);
|
|
836
|
+
res.status(500).send('Error downloading the matter storage zip file');
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
// Endpoint to download the matterbridge plugin directory
|
|
841
|
+
this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
|
|
842
|
+
this.log.debug('The frontend sent /api/download-pluginstorage');
|
|
843
|
+
if (!this.validateReq(req, res))
|
|
844
|
+
return;
|
|
845
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridge.matterbridgePluginDirectory);
|
|
846
|
+
res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
|
|
847
|
+
/* istanbul ignore if */
|
|
848
|
+
if (error) {
|
|
849
|
+
this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
|
|
850
|
+
res.status(500).send('Error downloading the matterbridge plugin storage file');
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
// Endpoint to download the matterbridge plugin config files
|
|
855
|
+
this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
|
|
856
|
+
this.log.debug('The frontend sent /api/download-pluginconfig');
|
|
857
|
+
if (!this.validateReq(req, res))
|
|
858
|
+
return;
|
|
859
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridge.matterbridgeDirectory, '*.config.json')));
|
|
860
|
+
res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
|
|
861
|
+
/* istanbul ignore if */
|
|
862
|
+
if (error) {
|
|
863
|
+
this.log.error(`Error downloading file matterbridge.pluginconfig.zip: ${error instanceof Error ? error.message : error}`);
|
|
864
|
+
res.status(500).send('Error downloading the matterbridge plugin config file');
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
// Endpoint to download the matterbridge backup (created with the backup command)
|
|
869
|
+
this.expressApp.get('/api/download-backup', async (req, res) => {
|
|
870
|
+
this.log.debug('The frontend sent /api/download-backup');
|
|
871
|
+
if (!this.validateReq(req, res))
|
|
872
|
+
return;
|
|
873
|
+
res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
|
|
874
|
+
/* istanbul ignore if */
|
|
875
|
+
if (error) {
|
|
876
|
+
this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
877
|
+
res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
// Endpoint to upload a package
|
|
882
|
+
this.expressApp.post('/api/uploadpackage', upload.single('file'), async (req, res) => {
|
|
883
|
+
if (!this.validateReq(req, res))
|
|
884
|
+
return;
|
|
885
|
+
const { filename } = req.body;
|
|
886
|
+
const file = req.file;
|
|
887
|
+
/* istanbul ignore if */
|
|
888
|
+
if (!file || !filename) {
|
|
889
|
+
this.log.error(`uploadpackage: invalid request: file and filename are required`);
|
|
890
|
+
res.status(400).send('Invalid request: file and filename are required');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
this.wssSendSnackbarMessage(`Installing package ${filename}. Please wait...`, 0);
|
|
894
|
+
// Define the path where the plugin file will be saved
|
|
895
|
+
const filePath = path.join(this.matterbridge.matterbridgeDirectory, 'uploads', filename);
|
|
896
|
+
try {
|
|
897
|
+
// Move the uploaded file to the specified path
|
|
898
|
+
const fs = await import('node:fs');
|
|
899
|
+
await fs.promises.rename(file.path, filePath);
|
|
900
|
+
this.log.info(`File ${plg}${filename}${nf} uploaded successfully`);
|
|
901
|
+
// Install the plugin package
|
|
902
|
+
if (filename.endsWith('.tgz')) {
|
|
903
|
+
const { spawnCommand } = await import('./spawn.js');
|
|
904
|
+
if (await spawnCommand('npm', ['install', '-g', filePath, '--omit=dev', '--verbose'], 'install', filename)) {
|
|
905
|
+
this.log.info(`Plugin package ${plg}${filename}${nf} installed successfully. Full restart required.`);
|
|
906
|
+
this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
|
|
907
|
+
this.wssSendSnackbarMessage(`Installed package ${filename}`, 10, 'success');
|
|
908
|
+
this.wssSendRestartRequired();
|
|
909
|
+
res.send(`Plugin package ${filename} uploaded and installed successfully`);
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
this.log.error(`Error uploading or installing plugin package file ${plg}${filename}${er}`);
|
|
913
|
+
this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
|
|
914
|
+
this.wssSendSnackbarMessage(`Error uploading or installing plugin package ${filename}`, 10, 'error');
|
|
915
|
+
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
res.send(`File ${filename} uploaded successfully`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
catch (err) {
|
|
923
|
+
this.log.error(`Error uploading or installing plugin package file ${plg}${filename}${er}:`, err);
|
|
924
|
+
this.wssSendCloseSnackbarMessage(`Installing package ${filename}. Please wait...`);
|
|
925
|
+
this.wssSendSnackbarMessage(`Error uploading or installing plugin package ${filename}`, 10, 'error');
|
|
926
|
+
res.status(500).send(`Error uploading or installing plugin package ${filename}`);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
// Fallback for routing (must be the last route)
|
|
930
|
+
this.expressApp.use((req, res) => {
|
|
931
|
+
const filePath = path.resolve(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build');
|
|
932
|
+
this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
|
|
933
|
+
res.sendFile('index.html', { root: filePath });
|
|
934
|
+
});
|
|
935
|
+
this.log.debug(`Frontend initialized on port ${YELLOW}${this.port}${db} static ${UNDERLINE}${path.join(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build')}${UNDERLINEOFF}${rs}`);
|
|
936
|
+
}
|
|
937
|
+
async stop() {
|
|
938
|
+
this.log.debug('Stopping the frontend...');
|
|
939
|
+
const ws = await import('ws');
|
|
940
|
+
// Remove listeners from the express app
|
|
941
|
+
if (this.expressApp) {
|
|
942
|
+
this.expressApp.removeAllListeners();
|
|
943
|
+
this.expressApp = undefined;
|
|
944
|
+
this.log.debug('Frontend app closed successfully');
|
|
945
|
+
}
|
|
946
|
+
// Close the WebSocket server
|
|
947
|
+
if (this.webSocketServer) {
|
|
948
|
+
this.log.debug('Closing WebSocket server...');
|
|
949
|
+
// Close all active connections
|
|
950
|
+
this.webSocketServer.clients.forEach((client) => {
|
|
951
|
+
if (client.readyState === ws.WebSocket.OPEN) {
|
|
952
|
+
client.close();
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
await withTimeout(new Promise((resolve) => {
|
|
956
|
+
this.webSocketServer?.close((error) => {
|
|
957
|
+
// istanbul ignore if
|
|
958
|
+
if (error) {
|
|
959
|
+
// istanbul ignore next
|
|
960
|
+
this.log.error(`Error closing WebSocket server: ${error}`);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
this.log.debug('WebSocket server closed successfully');
|
|
964
|
+
this.emit('websocket_server_stopped');
|
|
965
|
+
}
|
|
966
|
+
resolve();
|
|
967
|
+
});
|
|
968
|
+
}), 10000, false);
|
|
969
|
+
this.webSocketServer.removeAllListeners();
|
|
970
|
+
this.webSocketServer = undefined;
|
|
971
|
+
}
|
|
972
|
+
// Close the http server
|
|
973
|
+
if (this.httpServer) {
|
|
974
|
+
this.log.debug('Closing http server...');
|
|
975
|
+
/*
|
|
976
|
+
await withTimeout(
|
|
977
|
+
new Promise<void>((resolve) => {
|
|
978
|
+
this.httpServer?.close((error) => {
|
|
979
|
+
if (error) {
|
|
980
|
+
// istanbul ignore next
|
|
981
|
+
this.log.error(`Error closing http server: ${error}`);
|
|
982
|
+
} else {
|
|
983
|
+
this.log.debug('Http server closed successfully');
|
|
984
|
+
this.emit('server_stopped');
|
|
985
|
+
}
|
|
986
|
+
resolve();
|
|
987
|
+
});
|
|
988
|
+
}),
|
|
989
|
+
5000,
|
|
990
|
+
false,
|
|
991
|
+
);
|
|
992
|
+
*/
|
|
993
|
+
this.httpServer.close();
|
|
994
|
+
this.log.debug('Http server closed successfully');
|
|
995
|
+
this.listening = false;
|
|
996
|
+
this.emit('server_stopped');
|
|
997
|
+
this.httpServer.removeAllListeners();
|
|
998
|
+
this.httpServer = undefined;
|
|
999
|
+
this.log.debug('Frontend http server closed successfully');
|
|
1000
|
+
}
|
|
1001
|
+
// Close the https server
|
|
1002
|
+
if (this.httpsServer) {
|
|
1003
|
+
this.log.debug('Closing https server...');
|
|
1004
|
+
/*
|
|
1005
|
+
await withTimeout(
|
|
1006
|
+
new Promise<void>((resolve) => {
|
|
1007
|
+
this.httpsServer?.close((error) => {
|
|
1008
|
+
if (error) {
|
|
1009
|
+
// istanbul ignore next
|
|
1010
|
+
this.log.error(`Error closing https server: ${error}`);
|
|
1011
|
+
} else {
|
|
1012
|
+
this.log.debug('Https server closed successfully');
|
|
1013
|
+
this.emit('server_stopped');
|
|
1014
|
+
}
|
|
1015
|
+
resolve();
|
|
1016
|
+
});
|
|
1017
|
+
}),
|
|
1018
|
+
5000,
|
|
1019
|
+
false,
|
|
1020
|
+
);
|
|
1021
|
+
*/
|
|
1022
|
+
this.httpsServer.close();
|
|
1023
|
+
this.log.debug('Https server closed successfully');
|
|
1024
|
+
this.listening = false;
|
|
1025
|
+
this.emit('server_stopped');
|
|
1026
|
+
this.httpsServer.removeAllListeners();
|
|
1027
|
+
this.httpsServer = undefined;
|
|
1028
|
+
this.log.debug('Frontend https server closed successfully');
|
|
1029
|
+
}
|
|
1030
|
+
this.log.debug('Frontend stopped successfully');
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Retrieves the api settings data.
|
|
1034
|
+
*
|
|
1035
|
+
* @returns {Promise<{ matterbridgeInformation: MatterbridgeInformation, systemInformation: SystemInformation }>} A promise that resolve in the api settings object.
|
|
1036
|
+
*/
|
|
1037
|
+
async getApiSettings() {
|
|
1038
|
+
// Update the variable system information properties
|
|
1039
|
+
this.matterbridge.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
1040
|
+
this.matterbridge.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
1041
|
+
this.matterbridge.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
1042
|
+
this.matterbridge.systemInformation.processUptime = formatUptime(Math.floor(process.uptime()));
|
|
1043
|
+
this.matterbridge.systemInformation.cpuUsage = formatPercent(lastOsCpuUsage);
|
|
1044
|
+
this.matterbridge.systemInformation.processCpuUsage = formatPercent(lastProcessCpuUsage);
|
|
1045
|
+
this.matterbridge.systemInformation.rss = formatBytes(process.memoryUsage().rss);
|
|
1046
|
+
this.matterbridge.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
1047
|
+
this.matterbridge.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
1048
|
+
// Create the matterbridge information
|
|
1049
|
+
const info = {
|
|
1050
|
+
homeDirectory: this.matterbridge.homeDirectory,
|
|
1051
|
+
rootDirectory: this.matterbridge.rootDirectory,
|
|
1052
|
+
matterbridgeDirectory: this.matterbridge.matterbridgeDirectory,
|
|
1053
|
+
matterbridgePluginDirectory: this.matterbridge.matterbridgePluginDirectory,
|
|
1054
|
+
matterbridgeCertDirectory: this.matterbridge.matterbridgeCertDirectory,
|
|
1055
|
+
globalModulesDirectory: this.matterbridge.globalModulesDirectory,
|
|
1056
|
+
matterbridgeVersion: this.matterbridge.matterbridgeVersion,
|
|
1057
|
+
matterbridgeLatestVersion: this.matterbridge.matterbridgeLatestVersion,
|
|
1058
|
+
matterbridgeDevVersion: this.matterbridge.matterbridgeDevVersion,
|
|
1059
|
+
frontendVersion: this.matterbridge.frontendVersion,
|
|
1060
|
+
bridgeMode: this.matterbridge.bridgeMode,
|
|
1061
|
+
restartMode: this.matterbridge.restartMode,
|
|
1062
|
+
virtualMode: this.matterbridge.virtualMode,
|
|
1063
|
+
profile: this.matterbridge.profile,
|
|
1064
|
+
readOnly: this.matterbridge.readOnly,
|
|
1065
|
+
shellyBoard: this.matterbridge.shellyBoard,
|
|
1066
|
+
shellySysUpdate: this.matterbridge.shellySysUpdate,
|
|
1067
|
+
shellyMainUpdate: this.matterbridge.shellyMainUpdate,
|
|
1068
|
+
loggerLevel: await this.matterbridge.getLogLevel(),
|
|
1069
|
+
fileLogger: this.matterbridge.fileLogger,
|
|
1070
|
+
matterLoggerLevel: Logger.level,
|
|
1071
|
+
matterFileLogger: this.matterbridge.matterFileLogger,
|
|
1072
|
+
matterMdnsInterface: this.matterbridge.mdnsInterface,
|
|
1073
|
+
matterIpv4Address: this.matterbridge.ipv4Address,
|
|
1074
|
+
matterIpv6Address: this.matterbridge.ipv6Address,
|
|
1075
|
+
matterPort: (await this.matterbridge.nodeContext?.get('matterport', 5540)) ?? 5540,
|
|
1076
|
+
matterDiscriminator: await this.matterbridge.nodeContext?.get('matterdiscriminator'),
|
|
1077
|
+
matterPasscode: await this.matterbridge.nodeContext?.get('matterpasscode'),
|
|
1078
|
+
restartRequired: this.matterbridge.restartRequired,
|
|
1079
|
+
fixedRestartRequired: this.matterbridge.fixedRestartRequired,
|
|
1080
|
+
updateRequired: this.matterbridge.updateRequired,
|
|
1081
|
+
};
|
|
1082
|
+
return { systemInformation: this.matterbridge.systemInformation, matterbridgeInformation: info };
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Retrieves the reachable attribute.
|
|
1086
|
+
*
|
|
1087
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint object.
|
|
1088
|
+
* @returns {boolean} The reachable attribute.
|
|
1089
|
+
*/
|
|
1090
|
+
getReachability(device) {
|
|
1091
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1092
|
+
return false; // Skip if cleanup has started
|
|
1093
|
+
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
1094
|
+
return false;
|
|
1095
|
+
if (device.hasClusterServer(BridgedDeviceBasicInformation.Cluster.id))
|
|
1096
|
+
return device.getAttribute(BridgedDeviceBasicInformation.Cluster.id, 'reachable');
|
|
1097
|
+
if (device.mode === 'server' && device.serverNode && device.serverNode.state.basicInformation.reachable !== undefined)
|
|
1098
|
+
return device.serverNode.state.basicInformation.reachable;
|
|
1099
|
+
if (this.matterbridge.bridgeMode === 'childbridge')
|
|
1100
|
+
return true;
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Retrieves the power source attribute.
|
|
1105
|
+
*
|
|
1106
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
|
|
1107
|
+
* @returns {'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined} The power source attribute.
|
|
1108
|
+
*/
|
|
1109
|
+
getPowerSource(endpoint) {
|
|
1110
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1111
|
+
return undefined; // Skip if cleanup has started
|
|
1112
|
+
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
const powerSource = (device) => {
|
|
1115
|
+
const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
|
|
1116
|
+
if (featureMap.wired) {
|
|
1117
|
+
const wiredCurrentType = device.getAttribute(PowerSource.Cluster.id, 'wiredCurrentType');
|
|
1118
|
+
return ['ac', 'dc'][wiredCurrentType];
|
|
1119
|
+
}
|
|
1120
|
+
if (featureMap.battery) {
|
|
1121
|
+
const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batChargeLevel');
|
|
1122
|
+
return ['ok', 'warning', 'critical'][batChargeLevel];
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
};
|
|
1126
|
+
// Root endpoint
|
|
1127
|
+
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
1128
|
+
return powerSource(endpoint);
|
|
1129
|
+
// Child endpoints
|
|
1130
|
+
for (const child of endpoint.getChildEndpoints()) {
|
|
1131
|
+
// istanbul ignore else
|
|
1132
|
+
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
1133
|
+
return powerSource(child);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Retrieves the battery level attribute.
|
|
1138
|
+
*
|
|
1139
|
+
* @param {MatterbridgeEndpoint} endpoint - The MatterbridgeDevice to retrieve the power source from.
|
|
1140
|
+
* @returns {number | undefined} The battery level attribute.
|
|
1141
|
+
*/
|
|
1142
|
+
getBatteryLevel(endpoint) {
|
|
1143
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1144
|
+
return undefined; // Skip if cleanup has started
|
|
1145
|
+
if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
|
|
1146
|
+
return undefined;
|
|
1147
|
+
const batteryLevel = (device) => {
|
|
1148
|
+
const featureMap = device.getAttribute(PowerSource.Cluster.id, 'featureMap');
|
|
1149
|
+
if (featureMap.battery) {
|
|
1150
|
+
const batChargeLevel = device.getAttribute(PowerSource.Cluster.id, 'batPercentRemaining');
|
|
1151
|
+
return isValidNumber(batChargeLevel) ? batChargeLevel / 2 : undefined;
|
|
1152
|
+
}
|
|
1153
|
+
return undefined;
|
|
1154
|
+
};
|
|
1155
|
+
// Root endpoint
|
|
1156
|
+
if (endpoint.hasClusterServer(PowerSource.Cluster.id))
|
|
1157
|
+
return batteryLevel(endpoint);
|
|
1158
|
+
// Child endpoints
|
|
1159
|
+
for (const child of endpoint.getChildEndpoints()) {
|
|
1160
|
+
// istanbul ignore else
|
|
1161
|
+
if (child.hasClusterServer(PowerSource.Cluster.id))
|
|
1162
|
+
return batteryLevel(child);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Retrieves the cluster text description from a given device.
|
|
1167
|
+
* The output is a string with the attributes description of the cluster servers in the device to show in the frontend.
|
|
1168
|
+
*
|
|
1169
|
+
* @param {MatterbridgeEndpoint} device - The MatterbridgeEndpoint to retrieve the cluster text from.
|
|
1170
|
+
* @returns {string} The attributes description of the cluster servers in the device.
|
|
1171
|
+
*/
|
|
1172
|
+
getClusterTextFromDevice(device) {
|
|
1173
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1174
|
+
return ''; // Skip if cleanup has started
|
|
1175
|
+
// istanbul ignore else
|
|
1176
|
+
if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
|
|
1177
|
+
return '';
|
|
1178
|
+
const getUserLabel = (device) => {
|
|
1179
|
+
const labelList = getAttribute(device, 'userLabel', 'labelList');
|
|
1180
|
+
if (labelList) {
|
|
1181
|
+
const composed = labelList.find((entry) => entry.label === 'composed');
|
|
1182
|
+
if (composed)
|
|
1183
|
+
return 'Composed: ' + composed.value;
|
|
1184
|
+
}
|
|
1185
|
+
// istanbul ignore next cause is not reachable
|
|
1186
|
+
return '';
|
|
1187
|
+
};
|
|
1188
|
+
const getFixedLabel = (device) => {
|
|
1189
|
+
const labelList = getAttribute(device, 'fixedLabel', 'labelList');
|
|
1190
|
+
if (labelList) {
|
|
1191
|
+
const composed = labelList.find((entry) => entry.label === 'composed');
|
|
1192
|
+
if (composed)
|
|
1193
|
+
return 'Composed: ' + composed.value;
|
|
1194
|
+
}
|
|
1195
|
+
// istanbul ignore next cause is not reacheable
|
|
1196
|
+
return '';
|
|
1197
|
+
};
|
|
1198
|
+
let attributes = '';
|
|
1199
|
+
let supportedModes = [];
|
|
1200
|
+
device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1201
|
+
// console.log(`${device.deviceName} => Cluster: ${clusterName}-${clusterId} Attribute: ${attributeName}-${attributeId} Value(${typeof attributeValue}): ${attributeValue}`);
|
|
1202
|
+
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1203
|
+
return;
|
|
1204
|
+
if (clusterName === 'onOff' && attributeName === 'onOff')
|
|
1205
|
+
attributes += `OnOff: ${attributeValue} `;
|
|
1206
|
+
if (clusterName === 'switch' && attributeName === 'currentPosition')
|
|
1207
|
+
attributes += `Position: ${attributeValue} `;
|
|
1208
|
+
if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths' && isValidNumber(attributeValue, 0, 10000))
|
|
1209
|
+
attributes += `Cover position: ${attributeValue / 100}% `;
|
|
1210
|
+
if (clusterName === 'doorLock' && attributeName === 'lockState')
|
|
1211
|
+
attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
|
|
1212
|
+
if (clusterName === 'thermostat' && attributeName === 'localTemperature' && isValidNumber(attributeValue))
|
|
1213
|
+
attributes += `Temperature: ${attributeValue / 100}°C `;
|
|
1214
|
+
if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint' && isValidNumber(attributeValue))
|
|
1215
|
+
attributes += `Heat to: ${attributeValue / 100}°C `;
|
|
1216
|
+
if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint' && isValidNumber(attributeValue))
|
|
1217
|
+
attributes += `Cool to: ${attributeValue / 100}°C `;
|
|
1218
|
+
const modeClusters = ['modeSelect', 'rvcRunMode', 'rvcCleanMode', 'laundryWasherMode', 'ovenMode', 'microwaveOvenMode'];
|
|
1219
|
+
if (modeClusters.includes(clusterName) && attributeName === 'supportedModes') {
|
|
1220
|
+
supportedModes = attributeValue;
|
|
1221
|
+
}
|
|
1222
|
+
if (modeClusters.includes(clusterName) && attributeName === 'currentMode') {
|
|
1223
|
+
const supportedMode = supportedModes.find((mode) => mode.mode === attributeValue);
|
|
1224
|
+
if (supportedMode)
|
|
1225
|
+
attributes += `Mode: ${supportedMode.label} `;
|
|
1226
|
+
}
|
|
1227
|
+
const operationalStateClusters = ['operationalState', 'rvcOperationalState'];
|
|
1228
|
+
if (operationalStateClusters.includes(clusterName) && attributeName === 'operationalState')
|
|
1229
|
+
attributes += `OpState: ${attributeValue} `;
|
|
1230
|
+
if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
|
|
1231
|
+
attributes += `Mode: ${attributeValue} `;
|
|
1232
|
+
if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
|
|
1233
|
+
attributes += `State: ${attributeValue} `;
|
|
1234
|
+
if (clusterName === 'levelControl' && attributeName === 'currentLevel')
|
|
1235
|
+
attributes += `Level: ${attributeValue} `;
|
|
1236
|
+
if (clusterName === 'colorControl' && attributeName === 'colorMode' && isValidNumber(attributeValue, 0, 2))
|
|
1237
|
+
attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
|
|
1238
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue' && isValidNumber(attributeValue))
|
|
1239
|
+
attributes += `Hue: ${Math.round(attributeValue)} `;
|
|
1240
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation' && isValidNumber(attributeValue))
|
|
1241
|
+
attributes += `Saturation: ${Math.round(attributeValue)} `;
|
|
1242
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX' && isValidNumber(attributeValue))
|
|
1243
|
+
attributes += `X: ${Math.round(attributeValue / 655.36) / 100} `;
|
|
1244
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY' && isValidNumber(attributeValue))
|
|
1245
|
+
attributes += `Y: ${Math.round(attributeValue / 655.36) / 100} `;
|
|
1246
|
+
if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds' && isValidNumber(attributeValue))
|
|
1247
|
+
attributes += `ColorTemp: ${Math.round(attributeValue)} `;
|
|
1248
|
+
if (clusterName === 'booleanState' && attributeName === 'stateValue')
|
|
1249
|
+
attributes += `Contact: ${attributeValue} `;
|
|
1250
|
+
if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive' && isValidObject(attributeValue))
|
|
1251
|
+
attributes += `Active alarms: ${stringify(attributeValue)} `;
|
|
1252
|
+
if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
|
|
1253
|
+
attributes += `Smoke: ${attributeValue} `;
|
|
1254
|
+
if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
|
|
1255
|
+
attributes += `Co: ${attributeValue} `;
|
|
1256
|
+
if (clusterName === 'fanControl' && attributeName === 'fanMode')
|
|
1257
|
+
attributes += `Mode: ${attributeValue} `;
|
|
1258
|
+
if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
|
|
1259
|
+
attributes += `Percent: ${attributeValue} `;
|
|
1260
|
+
if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
|
|
1261
|
+
attributes += `Speed: ${attributeValue} `;
|
|
1262
|
+
if (clusterName === 'occupancySensing' && attributeName === 'occupancy' && isValidObject(attributeValue, 1))
|
|
1263
|
+
attributes += `Occupancy: ${attributeValue.occupied} `;
|
|
1264
|
+
if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
|
|
1265
|
+
attributes += `Illuminance: ${Math.round(Math.max(Math.pow(10, attributeValue / 10000), 0))} `;
|
|
1266
|
+
if (clusterName === 'airQuality' && attributeName === 'airQuality')
|
|
1267
|
+
attributes += `Air quality: ${attributeValue} `;
|
|
1268
|
+
if (clusterName === 'totalVolatileOrganicCompoundsConcentrationMeasurement' && attributeName === 'measuredValue')
|
|
1269
|
+
attributes += `Voc: ${attributeValue} `;
|
|
1270
|
+
if (clusterName === 'pm1ConcentrationMeasurement' && attributeName === 'measuredValue')
|
|
1271
|
+
attributes += `Pm1: ${attributeValue} `;
|
|
1272
|
+
if (clusterName === 'pm25ConcentrationMeasurement' && attributeName === 'measuredValue')
|
|
1273
|
+
attributes += `Pm2.5: ${attributeValue} `;
|
|
1274
|
+
if (clusterName === 'pm10ConcentrationMeasurement' && attributeName === 'measuredValue')
|
|
1275
|
+
attributes += `Pm10: ${attributeValue} `;
|
|
1276
|
+
if (clusterName === 'formaldehydeConcentrationMeasurement' && attributeName === 'measuredValue')
|
|
1277
|
+
attributes += `CH₂O: ${attributeValue} `;
|
|
1278
|
+
if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
|
|
1279
|
+
attributes += `Temperature: ${attributeValue / 100}°C `;
|
|
1280
|
+
if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue' && isValidNumber(attributeValue))
|
|
1281
|
+
attributes += `Humidity: ${attributeValue / 100}% `;
|
|
1282
|
+
if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
|
|
1283
|
+
attributes += `Pressure: ${attributeValue} `;
|
|
1284
|
+
if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
|
|
1285
|
+
attributes += `Flow: ${attributeValue} `;
|
|
1286
|
+
if (clusterName === 'fixedLabel' && attributeName === 'labelList')
|
|
1287
|
+
attributes += `${getFixedLabel(device)} `;
|
|
1288
|
+
if (clusterName === 'userLabel' && attributeName === 'labelList')
|
|
1289
|
+
attributes += `${getUserLabel(device)} `;
|
|
1290
|
+
});
|
|
1291
|
+
// console.log(`${device.deviceName}.forEachAttribute: ${attributes}`);
|
|
1292
|
+
return attributes.trimStart().trimEnd();
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Retrieves the registered plugins sanitized for res.json().
|
|
1296
|
+
*
|
|
1297
|
+
* @returns {ApiPlugin[]} An array of BaseRegisteredPlugin.
|
|
1298
|
+
*/
|
|
1299
|
+
getPlugins() {
|
|
1300
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1301
|
+
return []; // Skip if cleanup has started
|
|
1302
|
+
const plugins = [];
|
|
1303
|
+
for (const plugin of this.matterbridge.plugins.array()) {
|
|
1304
|
+
plugins.push({
|
|
1305
|
+
path: plugin.path,
|
|
1306
|
+
type: plugin.type,
|
|
1307
|
+
name: plugin.name,
|
|
1308
|
+
version: plugin.version,
|
|
1309
|
+
description: plugin.description,
|
|
1310
|
+
author: plugin.author,
|
|
1311
|
+
homepage: plugin.homepage,
|
|
1312
|
+
help: plugin.help,
|
|
1313
|
+
changelog: plugin.changelog,
|
|
1314
|
+
funding: plugin.funding,
|
|
1315
|
+
latestVersion: plugin.latestVersion,
|
|
1316
|
+
devVersion: plugin.devVersion,
|
|
1317
|
+
locked: plugin.locked,
|
|
1318
|
+
error: plugin.error,
|
|
1319
|
+
enabled: plugin.enabled,
|
|
1320
|
+
loaded: plugin.loaded,
|
|
1321
|
+
started: plugin.started,
|
|
1322
|
+
configured: plugin.configured,
|
|
1323
|
+
restartRequired: plugin.restartRequired,
|
|
1324
|
+
registeredDevices: plugin.registeredDevices,
|
|
1325
|
+
configJson: plugin.configJson,
|
|
1326
|
+
schemaJson: plugin.schemaJson,
|
|
1327
|
+
hasWhiteList: plugin.configJson?.whiteList !== undefined,
|
|
1328
|
+
hasBlackList: plugin.configJson?.blackList !== undefined,
|
|
1329
|
+
// Childbridge mode specific data
|
|
1330
|
+
matter: plugin.serverNode ? this.matterbridge.getServerNodeData(plugin.serverNode) : undefined,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
return plugins;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Retrieves the devices from Matterbridge.
|
|
1337
|
+
*
|
|
1338
|
+
* @param {string} [pluginName] - The name of the plugin to filter devices by.
|
|
1339
|
+
* @returns {ApiDevice[]} An array of ApiDevices for the frontend.
|
|
1340
|
+
*/
|
|
1341
|
+
getDevices(pluginName) {
|
|
1342
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1343
|
+
return []; // Skip if cleanup has started
|
|
1344
|
+
const devices = [];
|
|
1345
|
+
for (const device of this.matterbridge.devices.array()) {
|
|
1346
|
+
// Filter by pluginName if provided
|
|
1347
|
+
if (pluginName && pluginName !== device.plugin)
|
|
1348
|
+
continue;
|
|
1349
|
+
// Check if the device has the required properties
|
|
1350
|
+
if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
|
|
1351
|
+
continue;
|
|
1352
|
+
devices.push({
|
|
1353
|
+
pluginName: device.plugin,
|
|
1354
|
+
type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
|
|
1355
|
+
endpoint: device.number,
|
|
1356
|
+
name: device.deviceName,
|
|
1357
|
+
serial: device.serialNumber,
|
|
1358
|
+
productUrl: device.productUrl,
|
|
1359
|
+
configUrl: device.configUrl,
|
|
1360
|
+
uniqueId: device.uniqueId,
|
|
1361
|
+
reachable: this.getReachability(device),
|
|
1362
|
+
powerSource: this.getPowerSource(device),
|
|
1363
|
+
batteryLevel: this.getBatteryLevel(device),
|
|
1364
|
+
matter: device.mode === 'server' && device.serverNode ? this.matterbridge.getServerNodeData(device.serverNode) : undefined,
|
|
1365
|
+
cluster: this.getClusterTextFromDevice(device),
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return devices;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Retrieves the clusters from a given plugin and endpoint number.
|
|
1372
|
+
*
|
|
1373
|
+
* Response for /api/clusters
|
|
1374
|
+
*
|
|
1375
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1376
|
+
* @param {number} endpointNumber - The endpoint number.
|
|
1377
|
+
* @returns {ApiClusters | undefined} A promise that resolves to the clusters or undefined if not found.
|
|
1378
|
+
*/
|
|
1379
|
+
getClusters(pluginName, endpointNumber) {
|
|
1380
|
+
if (this.matterbridge.hasCleanupStarted)
|
|
1381
|
+
return; // Skip if cleanup has started
|
|
1382
|
+
const endpoint = this.matterbridge.devices.array().find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber);
|
|
1383
|
+
if (!endpoint || !endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
|
|
1384
|
+
this.log.error(`getClusters: no device found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
// this.log.debug(`***getClusters: getting clusters for device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${endpointNumber}`);
|
|
1388
|
+
// Get the device types from the main endpoint
|
|
1389
|
+
const deviceTypes = [];
|
|
1390
|
+
const clusters = [];
|
|
1391
|
+
endpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1392
|
+
deviceTypes.push(d.deviceType);
|
|
1393
|
+
});
|
|
1394
|
+
// Get the clusters from the main endpoint
|
|
1395
|
+
endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1396
|
+
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1397
|
+
return;
|
|
1398
|
+
// istanbul ignore if cause is not reachable without the EveHistory cluster
|
|
1399
|
+
if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1400
|
+
return;
|
|
1401
|
+
// console.log(
|
|
1402
|
+
// `${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}`,
|
|
1403
|
+
// );
|
|
1404
|
+
clusters.push({
|
|
1405
|
+
endpoint: endpoint.number.toString(),
|
|
1406
|
+
number: endpoint.number,
|
|
1407
|
+
id: 'main',
|
|
1408
|
+
deviceTypes,
|
|
1409
|
+
clusterName: capitalizeFirstLetter(clusterName),
|
|
1410
|
+
clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
|
|
1411
|
+
attributeName,
|
|
1412
|
+
attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
|
|
1413
|
+
attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
|
|
1414
|
+
attributeLocalValue: attributeValue,
|
|
1415
|
+
});
|
|
1416
|
+
});
|
|
1417
|
+
// Get the child endpoints
|
|
1418
|
+
const childEndpoints = endpoint.getChildEndpoints();
|
|
1419
|
+
// if (childEndpoints.length === 0) {
|
|
1420
|
+
// this.log.debug(`***getClusters: found ${childEndpoints.length} child endpoints for device ${endpoint.deviceName} plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1421
|
+
// }
|
|
1422
|
+
childEndpoints.forEach((childEndpoint) => {
|
|
1423
|
+
// istanbul ignore if cause is not reachable: should never happen but ...
|
|
1424
|
+
if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
|
|
1425
|
+
this.log.error(`getClusters: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
// this.log.debug(`***getClusters: getting clusters for child endpoint ${childEndpoint.id} of device ${endpoint.deviceName} plugin ${pluginName} endpoint number ${childEndpoint.number}`);
|
|
1429
|
+
// Get the device types of the child endpoint
|
|
1430
|
+
const deviceTypes = [];
|
|
1431
|
+
childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
|
|
1432
|
+
deviceTypes.push(d.deviceType);
|
|
1433
|
+
});
|
|
1434
|
+
childEndpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
|
|
1435
|
+
if (typeof attributeValue === 'undefined' || attributeValue === undefined)
|
|
1436
|
+
return;
|
|
1437
|
+
// istanbul ignore if cause is not reachable without the EveHistory cluster
|
|
1438
|
+
if (clusterName === 'EveHistory' &&
|
|
1439
|
+
['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
|
|
1440
|
+
return;
|
|
1441
|
+
// console.log(
|
|
1442
|
+
// `${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}`,
|
|
1443
|
+
// );
|
|
1444
|
+
clusters.push({
|
|
1445
|
+
endpoint: childEndpoint.number.toString(),
|
|
1446
|
+
number: childEndpoint.number,
|
|
1447
|
+
id: childEndpoint.id,
|
|
1448
|
+
deviceTypes,
|
|
1449
|
+
clusterName: capitalizeFirstLetter(clusterName),
|
|
1450
|
+
clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
|
|
1451
|
+
attributeName,
|
|
1452
|
+
attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
|
|
1453
|
+
attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
|
|
1454
|
+
attributeLocalValue: attributeValue,
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
});
|
|
1458
|
+
return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, number: endpoint.number, id: endpoint.id, deviceTypes, clusters };
|
|
1459
|
+
}
|
|
1460
|
+
async generateDiagnostic() {
|
|
1461
|
+
this.log.debug('Generating diagnostic...');
|
|
1462
|
+
const serverNodes = [];
|
|
1463
|
+
// istanbul ignore else
|
|
1464
|
+
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
1465
|
+
if (this.matterbridge.serverNode)
|
|
1466
|
+
serverNodes.push(this.matterbridge.serverNode);
|
|
1467
|
+
}
|
|
1468
|
+
else if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
1469
|
+
for (const plugin of this.matterbridge.plugins.array()) {
|
|
1470
|
+
if (plugin.serverNode)
|
|
1471
|
+
serverNodes.push(plugin.serverNode);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
// istanbul ignore next
|
|
1475
|
+
for (const device of this.matterbridge.devices.array()) {
|
|
1476
|
+
if (device.serverNode)
|
|
1477
|
+
serverNodes.push(device.serverNode);
|
|
1478
|
+
}
|
|
1479
|
+
const fs = await import('node:fs');
|
|
1480
|
+
if (fs.existsSync(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE)))
|
|
1481
|
+
fs.unlinkSync(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE));
|
|
1482
|
+
const diagnosticDestination = LogDestination({ name: 'diagnostic', level: MatterLogLevel.INFO, format: MatterLogFormat.formats.plain });
|
|
1483
|
+
diagnosticDestination.write = async (text, _message) => {
|
|
1484
|
+
await fs.promises.appendFile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_DIAGNOSTIC_FILE), text + '\n', { encoding: 'utf8' });
|
|
1485
|
+
};
|
|
1486
|
+
Logger.destinations.diagnostic = diagnosticDestination;
|
|
1487
|
+
if (!diagnosticDestination.context) {
|
|
1488
|
+
diagnosticDestination.context = Diagnostic.Context();
|
|
1489
|
+
}
|
|
1490
|
+
diagnosticDestination.context.run(() => diagnosticDestination.add(Diagnostic.message({
|
|
1491
|
+
now: new Date(),
|
|
1492
|
+
facility: 'Server nodes:',
|
|
1493
|
+
level: MatterLogLevel.INFO,
|
|
1494
|
+
prefix: Logger.nestingLevel ? '⎸'.padEnd(Logger.nestingLevel * 2) : '',
|
|
1495
|
+
values: [...serverNodes],
|
|
1496
|
+
})));
|
|
1497
|
+
delete Logger.destinations.diagnostic;
|
|
1498
|
+
await wait(500); // Wait for the log to be written
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Handles incoming websocket api request messages from the Matterbridge frontend.
|
|
1502
|
+
*
|
|
1503
|
+
* @param {WebSocket} client - The websocket client that sent the message.
|
|
1504
|
+
* @param {WebSocket.RawData} message - The raw data of the message received from the client.
|
|
1505
|
+
* @returns {Promise<void>} A promise that resolves when the message has been handled.
|
|
1506
|
+
*/
|
|
1507
|
+
async wsMessageHandler(client, message) {
|
|
1508
|
+
let data;
|
|
1509
|
+
const sendResponse = (data) => {
|
|
1510
|
+
if (client.readyState === client.OPEN) {
|
|
1511
|
+
if ('response' in data) {
|
|
1512
|
+
const { response, ...rest } = data;
|
|
1513
|
+
this.log.debug(`Sending api response message: ${debugStringify(rest)}`);
|
|
1514
|
+
}
|
|
1515
|
+
else if ('error' in data) {
|
|
1516
|
+
this.log.debug(`Sending api error message: ${debugStringify(data)}`);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
this.log.debug(`Sending api response message: ${debugStringify(data)}`);
|
|
1520
|
+
}
|
|
1521
|
+
client.send(JSON.stringify(data));
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
// istanbul ignore next cause is only a safety check
|
|
1525
|
+
this.log.error('Cannot send api response, client not connected');
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
try {
|
|
1529
|
+
data = JSON.parse(message.toString());
|
|
1530
|
+
if (!isValidNumber(data.id) ||
|
|
1531
|
+
!isValidString(data.dst) ||
|
|
1532
|
+
!isValidString(data.src) ||
|
|
1533
|
+
!isValidString(data.method) /* || !isValidObject(data.params)*/ ||
|
|
1534
|
+
data.dst !== 'Matterbridge') {
|
|
1535
|
+
this.log.error(`Invalid message from websocket client: ${debugStringify(data)}`);
|
|
1536
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid message' });
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
this.log.debug(`Received message from websocket client: ${debugStringify(data)}`);
|
|
1540
|
+
if (data.method === 'ping') {
|
|
1541
|
+
sendResponse({ id: data.id, method: 'pong', src: 'Matterbridge', dst: data.src, success: true, response: 'pong' });
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
else if (data.method === '/api/login') {
|
|
1545
|
+
if (!this.matterbridge.nodeContext) {
|
|
1546
|
+
this.log.error('Login nodeContext not found');
|
|
1547
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Internal error: nodeContext not found' });
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const storedPassword = await this.matterbridge.nodeContext.get('password', '');
|
|
1551
|
+
if (storedPassword === '' || storedPassword === data.params.password) {
|
|
1552
|
+
this.log.debug('Login password valid');
|
|
1553
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
this.log.debug('Error wrong password');
|
|
1558
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong password' });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
else if (data.method === '/api/install') {
|
|
1563
|
+
if (isValidString(data.params.packageName, 12) && isValidBoolean(data.params.restart)) {
|
|
1564
|
+
this.wssSendSnackbarMessage(`Installing package ${data.params.packageName}...`, 0);
|
|
1565
|
+
this.server.request({ type: 'plugins_install', src: this.server.name, dst: 'plugins', params: { packageName: data.params.packageName } });
|
|
1566
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1567
|
+
}
|
|
1568
|
+
else {
|
|
1569
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/install' });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
else if (data.method === '/api/uninstall') {
|
|
1573
|
+
if (isValidString(data.params.packageName, 12)) {
|
|
1574
|
+
this.wssSendSnackbarMessage(`Uninstalling package ${data.params.packageName}...`, 0);
|
|
1575
|
+
this.server.request({ type: 'plugins_uninstall', src: this.server.name, dst: 'plugins', params: { packageName: data.params.packageName } });
|
|
1576
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1577
|
+
}
|
|
1578
|
+
else {
|
|
1579
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter packageName in /api/uninstall' });
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
else if (data.method === '/api/addplugin') {
|
|
1583
|
+
const localData = data;
|
|
1584
|
+
if (!isValidString(data.params.pluginNameOrPath, 10)) {
|
|
1585
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginNameOrPath in /api/addplugin' });
|
|
1586
|
+
this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
this.wssSendSnackbarMessage(`Adding plugin ${data.params.pluginNameOrPath}...`, 5);
|
|
1590
|
+
this.log.debug(`Adding plugin ${data.params.pluginNameOrPath}...`);
|
|
1591
|
+
data.params.pluginNameOrPath = data.params.pluginNameOrPath.replace(/@.*$/, ''); // Remove @version if present
|
|
1592
|
+
/*
|
|
1593
|
+
const plugin = (await this.server.fetch({ type: 'plugins_add', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginNameOrPath } }, 5000)).response.plugin;
|
|
1594
|
+
if (plugin) {
|
|
1595
|
+
this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
|
|
1596
|
+
await this.server.fetch({ type: 'plugins_load', src: this.server.name, dst: 'plugins', params: { plugin: plugin.name } }, 5000);
|
|
1597
|
+
this.wssSendRestartRequired();
|
|
1598
|
+
this.wssSendRefreshRequired('plugins');
|
|
1599
|
+
this.wssSendRefreshRequired('devices');
|
|
1600
|
+
this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
|
|
1601
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1602
|
+
} else {
|
|
1603
|
+
this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
|
|
1604
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
|
|
1605
|
+
}
|
|
1606
|
+
*/
|
|
1607
|
+
const plugin = await this.matterbridge.plugins.add(data.params.pluginNameOrPath);
|
|
1608
|
+
if (plugin) {
|
|
1609
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1610
|
+
this.wssSendSnackbarMessage(`Added plugin ${data.params.pluginNameOrPath}`, 5, 'success');
|
|
1611
|
+
this.matterbridge.plugins
|
|
1612
|
+
.load(plugin)
|
|
1613
|
+
.then(() => {
|
|
1614
|
+
this.wssSendRefreshRequired('plugins');
|
|
1615
|
+
this.wssSendRefreshRequired('devices');
|
|
1616
|
+
this.wssSendSnackbarMessage(`Loaded plugin ${localData.params.pluginNameOrPath}`, 5, 'success');
|
|
1617
|
+
return;
|
|
1618
|
+
})
|
|
1619
|
+
.catch(/* istanbul ignore next */ (_error) => { });
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Plugin ${data.params.pluginNameOrPath} not added` });
|
|
1623
|
+
this.wssSendSnackbarMessage(`Plugin ${data.params.pluginNameOrPath} not added`, 10, 'error');
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
else if (data.method === '/api/removeplugin') {
|
|
1627
|
+
if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
|
|
1628
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/removeplugin' });
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
this.wssSendSnackbarMessage(`Removing plugin ${data.params.pluginName}...`, 5);
|
|
1632
|
+
this.log.debug(`Removing plugin ${data.params.pluginName}...`);
|
|
1633
|
+
/*
|
|
1634
|
+
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);
|
|
1635
|
+
await this.server.fetch({ type: 'plugins_remove', src: this.server.name, dst: 'plugins', params: { nameOrPath: data.params.pluginName } }, 5000);
|
|
1636
|
+
*/
|
|
1637
|
+
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1638
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
1639
|
+
await this.matterbridge.plugins.remove(data.params.pluginName);
|
|
1640
|
+
this.wssSendSnackbarMessage(`Removed plugin ${data.params.pluginName}`, 5, 'success');
|
|
1641
|
+
this.wssSendRefreshRequired('plugins');
|
|
1642
|
+
this.wssSendRefreshRequired('devices');
|
|
1643
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1644
|
+
}
|
|
1645
|
+
else if (data.method === '/api/enableplugin') {
|
|
1646
|
+
const localData = data;
|
|
1647
|
+
if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
|
|
1648
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/enableplugin' });
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1652
|
+
plugin.locked = undefined;
|
|
1653
|
+
plugin.error = undefined;
|
|
1654
|
+
plugin.loaded = undefined;
|
|
1655
|
+
plugin.started = undefined;
|
|
1656
|
+
plugin.configured = undefined;
|
|
1657
|
+
plugin.platform = undefined;
|
|
1658
|
+
plugin.registeredDevices = undefined;
|
|
1659
|
+
plugin.matter = undefined;
|
|
1660
|
+
await this.matterbridge.plugins.enable(data.params.pluginName);
|
|
1661
|
+
this.wssSendSnackbarMessage(`Enabled plugin ${data.params.pluginName}`, 5, 'success');
|
|
1662
|
+
setImmediate(async () => {
|
|
1663
|
+
await this.matterbridge.plugins.load(plugin, true, 'The plugin has been enabled', true);
|
|
1664
|
+
// @ts-expect-error Accessing private method
|
|
1665
|
+
if (plugin.serverNode)
|
|
1666
|
+
await this.matterbridge.startServerNode(plugin.serverNode);
|
|
1667
|
+
for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
|
|
1668
|
+
// @ts-expect-error Accessing private method
|
|
1669
|
+
await this.matterbridge.startServerNode(device.serverNode);
|
|
1670
|
+
this.wssSendSnackbarMessage(`Started plugin ${localData.params.pluginName}`, 5, 'success');
|
|
1671
|
+
this.wssSendRefreshRequired('plugins');
|
|
1672
|
+
this.wssSendRefreshRequired('devices');
|
|
1673
|
+
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, success: true });
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
else if (data.method === '/api/disableplugin') {
|
|
1677
|
+
if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
|
|
1678
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/disableplugin' });
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1682
|
+
// Stop server nodes devices first
|
|
1683
|
+
for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode)) {
|
|
1684
|
+
// @ts-expect-error Accessing private method
|
|
1685
|
+
await this.matterbridge.stopServerNode(device.serverNode);
|
|
1686
|
+
device.serverNode = undefined;
|
|
1687
|
+
}
|
|
1688
|
+
// Then shutdown plugin removing devices, disable it and stop plugin server node
|
|
1689
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
|
|
1690
|
+
await this.matterbridge.plugins.disable(data.params.pluginName);
|
|
1691
|
+
// @ts-expect-error Accessing private method
|
|
1692
|
+
if (plugin.serverNode)
|
|
1693
|
+
await this.matterbridge.stopServerNode(plugin.serverNode);
|
|
1694
|
+
plugin.serverNode = undefined;
|
|
1695
|
+
this.wssSendSnackbarMessage(`Disabled plugin ${data.params.pluginName}`, 5, 'success');
|
|
1696
|
+
this.wssSendRefreshRequired('plugins');
|
|
1697
|
+
this.wssSendRefreshRequired('devices');
|
|
1698
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1699
|
+
}
|
|
1700
|
+
else if (data.method === '/api/restartplugin') {
|
|
1701
|
+
if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
|
|
1702
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/restartplugin' });
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
this.wssSendSnackbarMessage(`Restarting plugin ${data.params.pluginName}`, 5, 'info');
|
|
1706
|
+
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1707
|
+
await this.matterbridge.plugins.shutdown(plugin, 'The plugin is restarting.', false, true);
|
|
1708
|
+
// Stop server nodes
|
|
1709
|
+
if (plugin.serverNode) {
|
|
1710
|
+
// @ts-expect-error Accessing private method
|
|
1711
|
+
await this.matterbridge.stopServerNode(plugin.serverNode);
|
|
1712
|
+
plugin.serverNode = undefined;
|
|
1713
|
+
}
|
|
1714
|
+
for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name)) {
|
|
1715
|
+
// @ts-expect-error Accessing private method
|
|
1716
|
+
if (device.serverNode)
|
|
1717
|
+
await this.matterbridge.stopServerNode(device.serverNode);
|
|
1718
|
+
device.serverNode = undefined;
|
|
1719
|
+
this.log.debug(`Removing device ${device.deviceName} from plugin ${plugin.name}`);
|
|
1720
|
+
this.matterbridge.devices.remove(device);
|
|
1721
|
+
}
|
|
1722
|
+
// @ts-expect-error Accessing private method
|
|
1723
|
+
if (plugin.type === 'DynamicPlatform' && !plugin.locked)
|
|
1724
|
+
await this.matterbridge.createDynamicPlugin(plugin);
|
|
1725
|
+
await this.matterbridge.plugins.load(plugin, true, 'The plugin has been restarted', true);
|
|
1726
|
+
plugin.restartRequired = false; // Reset plugin restartRequired
|
|
1727
|
+
let needRestart = 0;
|
|
1728
|
+
for (const plugin of this.matterbridge.plugins) {
|
|
1729
|
+
if (plugin.restartRequired)
|
|
1730
|
+
needRestart++;
|
|
1731
|
+
}
|
|
1732
|
+
if (needRestart === 0)
|
|
1733
|
+
this.wssSendRestartNotRequired(true); // Reset global restart required message
|
|
1734
|
+
// Start server nodes
|
|
1735
|
+
// @ts-expect-error Accessing private method
|
|
1736
|
+
if (plugin.serverNode)
|
|
1737
|
+
await this.matterbridge.startServerNode(plugin.serverNode);
|
|
1738
|
+
// @ts-expect-error Accessing private method
|
|
1739
|
+
for (const device of this.matterbridge.devices.array().filter((d) => d.plugin === plugin.name && d.serverNode))
|
|
1740
|
+
await this.matterbridge.startServerNode(device.serverNode);
|
|
1741
|
+
this.wssSendSnackbarMessage(`Restarted plugin ${data.params.pluginName}`, 5, 'success');
|
|
1742
|
+
this.wssSendRefreshRequired('plugins');
|
|
1743
|
+
this.wssSendRefreshRequired('devices');
|
|
1744
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1745
|
+
}
|
|
1746
|
+
else if (data.method === '/api/savepluginconfig') {
|
|
1747
|
+
if (!isValidString(data.params.pluginName, 10) || !this.matterbridge.plugins.has(data.params.pluginName)) {
|
|
1748
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/savepluginconfig' });
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (!isValidObject(data.params.formData, 5)) {
|
|
1752
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter formData in /api/savepluginconfig' });
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
this.log.info(`Saving config for plugin ${plg}${data.params.pluginName}${nf}...`);
|
|
1756
|
+
const plugin = this.matterbridge.plugins.get(data.params.pluginName);
|
|
1757
|
+
if (plugin) {
|
|
1758
|
+
this.matterbridge.plugins.saveConfigFromJson(plugin, data.params.formData, true);
|
|
1759
|
+
this.wssSendSnackbarMessage(`Saved config for plugin ${data.params.pluginName}`);
|
|
1760
|
+
this.wssSendRestartRequired();
|
|
1761
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
else if (data.method === '/api/checkupdates') {
|
|
1765
|
+
const { createESMWorker } = await import('@matterbridge/thread');
|
|
1766
|
+
createESMWorker('CheckUpdates', this.matterbridge.resolveWorkerDistFilePath('workerCheckUpdates.js'));
|
|
1767
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1768
|
+
}
|
|
1769
|
+
else if (data.method === '/api/shellysysupdate') {
|
|
1770
|
+
/*
|
|
1771
|
+
const { triggerShellySysUpdate } = await import('./shelly.js');
|
|
1772
|
+
triggerShellySysUpdate(this.matterbridge);
|
|
1773
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1774
|
+
*/
|
|
1775
|
+
}
|
|
1776
|
+
else if (data.method === '/api/shellymainupdate') {
|
|
1777
|
+
/*
|
|
1778
|
+
const { triggerShellyMainUpdate } = await import('./shelly.js');
|
|
1779
|
+
triggerShellyMainUpdate(this.matterbridge);
|
|
1780
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1781
|
+
*/
|
|
1782
|
+
}
|
|
1783
|
+
else if (data.method === '/api/shellycreatesystemlog') {
|
|
1784
|
+
/*
|
|
1785
|
+
const { createShellySystemLog } = await import('./shelly.js');
|
|
1786
|
+
createShellySystemLog(this.matterbridge);
|
|
1787
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1788
|
+
*/
|
|
1789
|
+
}
|
|
1790
|
+
else if (data.method === '/api/shellynetconfig') {
|
|
1791
|
+
/*
|
|
1792
|
+
this.log.debug('/api/shellynetconfig:', data.params);
|
|
1793
|
+
const { triggerShellyChangeIp } = await import('./shelly.js');
|
|
1794
|
+
triggerShellyChangeIp(this.matterbridge, data.params as { type: 'static' | 'dhcp'; ip: string; subnet: string; gateway: string; dns: string });
|
|
1795
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1796
|
+
*/
|
|
1797
|
+
}
|
|
1798
|
+
else if (data.method === '/api/softreset') {
|
|
1799
|
+
/*
|
|
1800
|
+
const { triggerShellySoftReset } = await import('./shelly.js');
|
|
1801
|
+
triggerShellySoftReset(this.matterbridge);
|
|
1802
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1803
|
+
*/
|
|
1804
|
+
}
|
|
1805
|
+
else if (data.method === '/api/hardreset') {
|
|
1806
|
+
/*
|
|
1807
|
+
const { triggerShellyHardReset } = await import('./shelly.js');
|
|
1808
|
+
triggerShellyHardReset(this.matterbridge);
|
|
1809
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1810
|
+
*/
|
|
1811
|
+
}
|
|
1812
|
+
else if (data.method === '/api/reboot') {
|
|
1813
|
+
/*
|
|
1814
|
+
const { triggerShellyReboot } = await import('./shelly.js');
|
|
1815
|
+
triggerShellyReboot(this.matterbridge);
|
|
1816
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1817
|
+
*/
|
|
1818
|
+
}
|
|
1819
|
+
else if (data.method === '/api/restart') {
|
|
1820
|
+
this.wssSendSnackbarMessage(`Restarting matterbridge...`, 0);
|
|
1821
|
+
await this.matterbridge.restartProcess();
|
|
1822
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1823
|
+
}
|
|
1824
|
+
else if (data.method === '/api/shutdown') {
|
|
1825
|
+
this.wssSendSnackbarMessage(`Shutting down matterbridge...`, 0);
|
|
1826
|
+
await this.matterbridge.shutdownProcess();
|
|
1827
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1828
|
+
}
|
|
1829
|
+
else if (data.method === '/api/create-backup') {
|
|
1830
|
+
this.wssSendSnackbarMessage('Creating backup...', 0);
|
|
1831
|
+
this.log.notice(`Creating the backup...`);
|
|
1832
|
+
await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridge.matterbridgeDirectory), path.join(this.matterbridge.matterbridgePluginDirectory), path.join(this.matterbridge.matterbridgeCertDirectory));
|
|
1833
|
+
this.log.notice(`Backup ready to be downloaded.`);
|
|
1834
|
+
this.wssSendCloseSnackbarMessage('Creating backup...');
|
|
1835
|
+
this.wssSendSnackbarMessage('Backup ready to be downloaded', 10);
|
|
1836
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1837
|
+
}
|
|
1838
|
+
else if (data.method === '/api/unregister') {
|
|
1839
|
+
this.wssSendSnackbarMessage('Unregistering all bridged devices...', 0);
|
|
1840
|
+
await this.matterbridge.unregisterAndShutdownProcess();
|
|
1841
|
+
this.wssSendCloseSnackbarMessage('Unregistering all bridged devices...');
|
|
1842
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1843
|
+
}
|
|
1844
|
+
else if (data.method === '/api/reset') {
|
|
1845
|
+
this.wssSendSnackbarMessage('Resetting matterbridge commissioning...', 10);
|
|
1846
|
+
await this.matterbridge.shutdownProcessAndReset();
|
|
1847
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1848
|
+
}
|
|
1849
|
+
else if (data.method === '/api/factoryreset') {
|
|
1850
|
+
this.wssSendSnackbarMessage('Factory reset of matterbridge...', 10);
|
|
1851
|
+
await this.matterbridge.shutdownProcessAndFactoryReset();
|
|
1852
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1853
|
+
}
|
|
1854
|
+
else if (data.method === '/api/viewhistorypage') {
|
|
1855
|
+
generateHistoryPage({ outputPath: path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), pageTitle: `Matterbridge Cpu & Memory History` });
|
|
1856
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1857
|
+
}
|
|
1858
|
+
else if (data.method === '/api/downloadhistorypage') {
|
|
1859
|
+
generateHistoryPage({ outputPath: path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_HISTORY_FILE), pageTitle: `Matterbridge Cpu & Memory History` });
|
|
1860
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
1861
|
+
}
|
|
1862
|
+
else if (data.method === '/api/matter') {
|
|
1863
|
+
const localData = data;
|
|
1864
|
+
if (!isValidString(data.params.id)) {
|
|
1865
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
let serverNode;
|
|
1869
|
+
if (data.params.id === 'Matterbridge')
|
|
1870
|
+
serverNode = this.matterbridge.serverNode;
|
|
1871
|
+
else
|
|
1872
|
+
serverNode =
|
|
1873
|
+
this.matterbridge.plugins.array().find((p) => p.serverNode && p.serverNode.id === localData.params.id)?.serverNode ||
|
|
1874
|
+
this.matterbridge.devices.array().find((d) => d.serverNode && d.serverNode.id === localData.params.id)?.serverNode;
|
|
1875
|
+
if (!serverNode) {
|
|
1876
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown server node id ${localData.params.id} in /api/matter` });
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const matter = this.matterbridge.getServerNodeData(serverNode);
|
|
1880
|
+
this.log.debug(`*Server node ${serverNode.id}: commissioned ${serverNode.state.commissioning.commissioned} upTime ${serverNode.state.generalDiagnostics.upTime}.`);
|
|
1881
|
+
if (data.params.server) {
|
|
1882
|
+
this.log.debug(`*Sending data for node ${data.params.id}`);
|
|
1883
|
+
this.wssSendRefreshRequired('matter', { matter: { ...matter } });
|
|
1884
|
+
}
|
|
1885
|
+
if (data.params.startCommission) {
|
|
1886
|
+
await serverNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
|
|
1887
|
+
this.matterbridge.advertisingNodes.set(serverNode.id, Date.now());
|
|
1888
|
+
this.log.debug(`*Allow commissioning has been sent for node ${data.params.id}`);
|
|
1889
|
+
this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: Date.now(), advertising: true } });
|
|
1890
|
+
}
|
|
1891
|
+
if (data.params.stopCommission) {
|
|
1892
|
+
await serverNode.env.get(DeviceCommissioner)?.endCommissioning();
|
|
1893
|
+
this.matterbridge.advertisingNodes.delete(serverNode.id);
|
|
1894
|
+
this.log.debug(`*End commissioning has been sent for node ${data.params.id}`);
|
|
1895
|
+
this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
|
|
1896
|
+
}
|
|
1897
|
+
if (data.params.advertise) {
|
|
1898
|
+
serverNode.env.get(DeviceAdvertiser)?.restartAdvertisement();
|
|
1899
|
+
this.log.debug(`*Advertising has been sent for node ${data.params.id}`);
|
|
1900
|
+
this.wssSendRefreshRequired('matter', { matter: { ...matter, advertising: true } });
|
|
1901
|
+
}
|
|
1902
|
+
if (data.params.removeFabric) {
|
|
1903
|
+
const fabricIndex = FabricIndex(data.params.removeFabric);
|
|
1904
|
+
const fabricManager = serverNode.env.get(FabricManager);
|
|
1905
|
+
if (fabricManager.has(fabricIndex))
|
|
1906
|
+
await fabricManager.for(fabricIndex).leave();
|
|
1907
|
+
this.log.debug(`*Removed fabric index ${data.params.removeFabric} for node ${data.params.id}`);
|
|
1908
|
+
this.wssSendRefreshRequired('matter', { matter: { ...matter } });
|
|
1909
|
+
}
|
|
1910
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
|
|
1911
|
+
}
|
|
1912
|
+
else if (data.method === '/api/settings') {
|
|
1913
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: await this.getApiSettings() });
|
|
1914
|
+
}
|
|
1915
|
+
else if (data.method === '/api/plugins') {
|
|
1916
|
+
const plugins = this.matterbridge.hasCleanupStarted ? [] : this.getPlugins();
|
|
1917
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
|
|
1918
|
+
}
|
|
1919
|
+
else if (data.method === '/api/devices') {
|
|
1920
|
+
const devices = this.matterbridge.hasCleanupStarted ? [] : this.getDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
|
|
1921
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
|
|
1922
|
+
}
|
|
1923
|
+
else if (data.method === '/api/clusters') {
|
|
1924
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1925
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (!isValidNumber(data.params.endpoint, 1)) {
|
|
1929
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const response = this.getClusters(data.params.plugin, data.params.endpoint);
|
|
1933
|
+
if (response) {
|
|
1934
|
+
sendResponse({
|
|
1935
|
+
id: data.id,
|
|
1936
|
+
method: data.method,
|
|
1937
|
+
src: 'Matterbridge',
|
|
1938
|
+
dst: data.src,
|
|
1939
|
+
success: true,
|
|
1940
|
+
response,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Endpoint not found in /api/clusters' });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
else if (data.method === '/api/select/devices') {
|
|
1948
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1949
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/devices' });
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
1953
|
+
if (!plugin) {
|
|
1954
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
// istanbul ignore next
|
|
1958
|
+
const selectDeviceValues = !plugin.platform ? [] : plugin.platform.getSelectDevices().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
|
|
1959
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDeviceValues });
|
|
1960
|
+
}
|
|
1961
|
+
else if (data.method === '/api/select/entities') {
|
|
1962
|
+
if (!isValidString(data.params.plugin, 10)) {
|
|
1963
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/select/entities' });
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
1967
|
+
if (!plugin) {
|
|
1968
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
// istanbul ignore next
|
|
1972
|
+
const selectEntityValues = !plugin.platform ? [] : plugin.platform.getSelectEntities().sort((keyA, keyB) => keyA.name.localeCompare(keyB.name));
|
|
1973
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntityValues });
|
|
1974
|
+
}
|
|
1975
|
+
else if (data.method === '/api/action') {
|
|
1976
|
+
const localData = data;
|
|
1977
|
+
if (!isValidString(data.params.plugin, 5) || !isValidString(data.params.action, 1)) {
|
|
1978
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/action' });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
1982
|
+
if (!plugin) {
|
|
1983
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/action' });
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
this.log.notice(`Action ${CYAN}${data.params.action}${nt}${data.params.value ? ' with ' + CYAN + data.params.value + nt : ''} for plugin ${CYAN}${plugin.name}${nt}`);
|
|
1987
|
+
plugin.platform
|
|
1988
|
+
?.onAction(data.params.action, data.params.value, data.params.id, data.params.formData)
|
|
1989
|
+
.then(() => {
|
|
1990
|
+
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, success: true });
|
|
1991
|
+
return;
|
|
1992
|
+
})
|
|
1993
|
+
.catch((error) => {
|
|
1994
|
+
this.log.error(`Error in plugin ${plugin.name} action ${localData.params.action}: ${error}`);
|
|
1995
|
+
sendResponse({
|
|
1996
|
+
id: data.id,
|
|
1997
|
+
method: data.method,
|
|
1998
|
+
src: 'Matterbridge',
|
|
1999
|
+
dst: data.src,
|
|
2000
|
+
error: `Error in plugin ${plugin.name} action ${localData.params.action}: ${error}`,
|
|
2001
|
+
});
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
else if (data.method === '/api/config') {
|
|
2005
|
+
if (!isValidString(data.params.name, 5) || data.params.value === undefined) {
|
|
2006
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter in /api/config' });
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
this.log.debug(`Received /api/config name ${CYAN}${data.params.name}${db} value ${CYAN}${data.params.value}${db}`);
|
|
2010
|
+
switch (data.params.name) {
|
|
2011
|
+
case 'setpassword':
|
|
2012
|
+
if (isValidString(data.params.value)) {
|
|
2013
|
+
await this.matterbridge.nodeContext?.set('password', data.params.value);
|
|
2014
|
+
this.storedPassword = data.params.value;
|
|
2015
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2016
|
+
}
|
|
2017
|
+
break;
|
|
2018
|
+
case 'setbridgemode':
|
|
2019
|
+
if (isValidString(data.params.value) && ['bridge', 'childbridge'].includes(data.params.value)) {
|
|
2020
|
+
await this.matterbridge.nodeContext?.set('bridgeMode', data.params.value);
|
|
2021
|
+
this.wssSendRestartRequired();
|
|
2022
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2023
|
+
}
|
|
2024
|
+
break;
|
|
2025
|
+
case 'setmbloglevel':
|
|
2026
|
+
if (isValidString(data.params.value, 4)) {
|
|
2027
|
+
this.log.debug('Matterbridge logger level:', data.params.value);
|
|
2028
|
+
if (data.params.value === 'Debug') {
|
|
2029
|
+
await this.matterbridge.setLogLevel("debug" /* LogLevel.DEBUG */);
|
|
2030
|
+
}
|
|
2031
|
+
else if (data.params.value === 'Info') {
|
|
2032
|
+
await this.matterbridge.setLogLevel("info" /* LogLevel.INFO */);
|
|
2033
|
+
}
|
|
2034
|
+
else if (data.params.value === 'Notice') {
|
|
2035
|
+
await this.matterbridge.setLogLevel("notice" /* LogLevel.NOTICE */);
|
|
2036
|
+
}
|
|
2037
|
+
else if (data.params.value === 'Warn') {
|
|
2038
|
+
await this.matterbridge.setLogLevel("warn" /* LogLevel.WARN */);
|
|
2039
|
+
}
|
|
2040
|
+
else if (data.params.value === 'Error') {
|
|
2041
|
+
await this.matterbridge.setLogLevel("error" /* LogLevel.ERROR */);
|
|
2042
|
+
}
|
|
2043
|
+
else if (data.params.value === 'Fatal') {
|
|
2044
|
+
await this.matterbridge.setLogLevel("fatal" /* LogLevel.FATAL */);
|
|
2045
|
+
}
|
|
2046
|
+
await this.matterbridge.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
|
|
2047
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2048
|
+
}
|
|
2049
|
+
break;
|
|
2050
|
+
case 'setmblogfile':
|
|
2051
|
+
if (isValidBoolean(data.params.value)) {
|
|
2052
|
+
this.log.debug('Matterbridge file log:', data.params.value);
|
|
2053
|
+
this.matterbridge.fileLogger = data.params.value;
|
|
2054
|
+
await this.matterbridge.nodeContext?.set('matterbridgeFileLog', data.params.value);
|
|
2055
|
+
// Create the file logger for matterbridge
|
|
2056
|
+
if (data.params.value)
|
|
2057
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), await this.matterbridge.getLogLevel(), true);
|
|
2058
|
+
else
|
|
2059
|
+
AnsiLogger.setGlobalLogfile(undefined);
|
|
2060
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2061
|
+
}
|
|
2062
|
+
break;
|
|
2063
|
+
case 'setmjloglevel':
|
|
2064
|
+
if (isValidString(data.params.value, 4)) {
|
|
2065
|
+
this.log.debug('Matter logger level:', data.params.value);
|
|
2066
|
+
if (data.params.value === 'Debug') {
|
|
2067
|
+
Logger.level = MatterLogLevel.DEBUG;
|
|
2068
|
+
}
|
|
2069
|
+
else if (data.params.value === 'Info') {
|
|
2070
|
+
Logger.level = MatterLogLevel.INFO;
|
|
2071
|
+
}
|
|
2072
|
+
else if (data.params.value === 'Notice') {
|
|
2073
|
+
Logger.level = MatterLogLevel.NOTICE;
|
|
2074
|
+
}
|
|
2075
|
+
else if (data.params.value === 'Warn') {
|
|
2076
|
+
Logger.level = MatterLogLevel.WARN;
|
|
2077
|
+
}
|
|
2078
|
+
else if (data.params.value === 'Error') {
|
|
2079
|
+
Logger.level = MatterLogLevel.ERROR;
|
|
2080
|
+
}
|
|
2081
|
+
else if (data.params.value === 'Fatal') {
|
|
2082
|
+
Logger.level = MatterLogLevel.FATAL;
|
|
2083
|
+
}
|
|
2084
|
+
this.matterbridge.matterLogLevel = MatterLogLevel.names[Logger.level];
|
|
2085
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
2086
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
2087
|
+
if (this.matterbridge.getLogLevel() === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
2088
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
2089
|
+
if (this.matterbridge.getLogLevel() === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG)
|
|
2090
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
2091
|
+
AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
|
|
2092
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
2093
|
+
await this.matterbridge.nodeContext?.set('matterLogLevel', Logger.level);
|
|
2094
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2095
|
+
}
|
|
2096
|
+
break;
|
|
2097
|
+
case 'setmjlogfile':
|
|
2098
|
+
if (isValidBoolean(data.params.value)) {
|
|
2099
|
+
this.log.debug('Matter file log:', data.params.value);
|
|
2100
|
+
this.matterbridge.matterFileLogger = data.params.value;
|
|
2101
|
+
await this.matterbridge.nodeContext?.set('matterFileLog', data.params.value);
|
|
2102
|
+
if (data.params.value) {
|
|
2103
|
+
this.matterbridge.matterLog.logFilePath = path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE);
|
|
2104
|
+
}
|
|
2105
|
+
else {
|
|
2106
|
+
this.matterbridge.matterLog.logFilePath = undefined;
|
|
2107
|
+
}
|
|
2108
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2109
|
+
}
|
|
2110
|
+
break;
|
|
2111
|
+
case 'setmdnsinterface':
|
|
2112
|
+
if (isValidString(data.params.value)) {
|
|
2113
|
+
this.log.debug(`Matter.js mdns interface: ${data.params.value === '' ? 'all interfaces' : data.params.value}`);
|
|
2114
|
+
this.matterbridge.mdnsInterface = data.params.value === '' ? undefined : data.params.value;
|
|
2115
|
+
await this.matterbridge.nodeContext?.set('mattermdnsinterface', data.params.value);
|
|
2116
|
+
this.wssSendRestartRequired();
|
|
2117
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2118
|
+
this.wssSendSnackbarMessage(`Mdns interface changed to ${data.params.value === '' ? 'all interfaces' : data.params.value}`);
|
|
2119
|
+
}
|
|
2120
|
+
break;
|
|
2121
|
+
case 'setipv4address':
|
|
2122
|
+
if (isValidString(data.params.value)) {
|
|
2123
|
+
this.log.debug(`Matter.js ipv4 address: ${data.params.value === '' ? 'all ipv4 addresses' : data.params.value}`);
|
|
2124
|
+
this.matterbridge.ipv4Address = data.params.value === '' ? undefined : data.params.value;
|
|
2125
|
+
await this.matterbridge.nodeContext?.set('matteripv4address', data.params.value);
|
|
2126
|
+
this.wssSendRestartRequired();
|
|
2127
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2128
|
+
this.wssSendSnackbarMessage(`IPv4 address changed to ${data.params.value === '' ? 'all ipv4 addresses' : data.params.value}`);
|
|
2129
|
+
}
|
|
2130
|
+
break;
|
|
2131
|
+
case 'setipv6address':
|
|
2132
|
+
if (isValidString(data.params.value)) {
|
|
2133
|
+
this.log.debug(`Matter.js ipv6 address: ${data.params.value === '' ? 'all ipv6 addresses' : data.params.value}`);
|
|
2134
|
+
this.matterbridge.ipv6Address = data.params.value === '' ? undefined : data.params.value;
|
|
2135
|
+
await this.matterbridge.nodeContext?.set('matteripv6address', data.params.value);
|
|
2136
|
+
this.wssSendRestartRequired();
|
|
2137
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2138
|
+
this.wssSendSnackbarMessage(`IPv6 address changed to ${data.params.value === '' ? 'all ipv6 addresses' : data.params.value}`);
|
|
2139
|
+
}
|
|
2140
|
+
break;
|
|
2141
|
+
case 'setmatterport':
|
|
2142
|
+
// eslint-disable-next-line no-case-declarations
|
|
2143
|
+
const port = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
2144
|
+
if (isValidNumber(port, 5540, 5600)) {
|
|
2145
|
+
this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
|
|
2146
|
+
this.matterbridge.port = port;
|
|
2147
|
+
await this.matterbridge.nodeContext?.set('matterport', port);
|
|
2148
|
+
this.wssSendRestartRequired();
|
|
2149
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2150
|
+
this.wssSendSnackbarMessage(`Matter port changed to ${port}`);
|
|
2151
|
+
}
|
|
2152
|
+
else {
|
|
2153
|
+
this.log.debug(`Reset matter commissioning port to ${CYAN}5540${db}`);
|
|
2154
|
+
this.matterbridge.port = 5540;
|
|
2155
|
+
await this.matterbridge.nodeContext?.set('matterport', 5540);
|
|
2156
|
+
this.wssSendRestartRequired();
|
|
2157
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Invalid value: reset matter commissioning port to default 5540' });
|
|
2158
|
+
this.wssSendSnackbarMessage(`Matter port reset to default 5540`, undefined, 'error');
|
|
2159
|
+
}
|
|
2160
|
+
break;
|
|
2161
|
+
case 'setmatterdiscriminator':
|
|
2162
|
+
// eslint-disable-next-line no-case-declarations
|
|
2163
|
+
const discriminator = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
2164
|
+
if (isValidNumber(discriminator, 0, 4095)) {
|
|
2165
|
+
this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
|
|
2166
|
+
this.matterbridge.discriminator = discriminator;
|
|
2167
|
+
await this.matterbridge.nodeContext?.set('matterdiscriminator', discriminator);
|
|
2168
|
+
this.wssSendRestartRequired();
|
|
2169
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2170
|
+
this.wssSendSnackbarMessage(`Matter discriminator changed to ${discriminator}`);
|
|
2171
|
+
}
|
|
2172
|
+
else {
|
|
2173
|
+
this.log.debug(`Reset matter commissioning discriminator to ${CYAN}undefined${db}`);
|
|
2174
|
+
this.matterbridge.discriminator = undefined;
|
|
2175
|
+
await this.matterbridge.nodeContext?.remove('matterdiscriminator');
|
|
2176
|
+
this.wssSendRestartRequired();
|
|
2177
|
+
sendResponse({
|
|
2178
|
+
id: data.id,
|
|
2179
|
+
method: data.method,
|
|
2180
|
+
src: 'Matterbridge',
|
|
2181
|
+
dst: data.src,
|
|
2182
|
+
error: 'Invalid value: reset matter commissioning discriminator to default undefined',
|
|
2183
|
+
});
|
|
2184
|
+
this.wssSendSnackbarMessage(`Matter discriminator reset to default`, undefined, 'error');
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
case 'setmatterpasscode':
|
|
2188
|
+
// eslint-disable-next-line no-case-declarations
|
|
2189
|
+
const passcode = isValidString(data.params.value) ? parseInt(data.params.value) : 0;
|
|
2190
|
+
if (isValidNumber(passcode, 1, 99999998) && CommissioningOptions.FORBIDDEN_PASSCODES.includes(passcode) === false) {
|
|
2191
|
+
this.matterbridge.passcode = passcode;
|
|
2192
|
+
this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
|
|
2193
|
+
await this.matterbridge.nodeContext?.set('matterpasscode', passcode);
|
|
2194
|
+
this.wssSendRestartRequired();
|
|
2195
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2196
|
+
this.wssSendSnackbarMessage(`Matter passcode changed to ${passcode}`);
|
|
2197
|
+
}
|
|
2198
|
+
else {
|
|
2199
|
+
this.log.debug(`Reset matter commissioning passcode to ${CYAN}undefined${db}`);
|
|
2200
|
+
this.matterbridge.passcode = undefined;
|
|
2201
|
+
await this.matterbridge.nodeContext?.remove('matterpasscode');
|
|
2202
|
+
this.wssSendRestartRequired();
|
|
2203
|
+
sendResponse({
|
|
2204
|
+
id: data.id,
|
|
2205
|
+
method: data.method,
|
|
2206
|
+
src: 'Matterbridge',
|
|
2207
|
+
dst: data.src,
|
|
2208
|
+
error: 'Invalid value: reset matter commissioning passcode to default undefined',
|
|
2209
|
+
});
|
|
2210
|
+
this.wssSendSnackbarMessage(`Matter passcode reset to default`, undefined, 'error');
|
|
2211
|
+
}
|
|
2212
|
+
break;
|
|
2213
|
+
case 'setvirtualmode':
|
|
2214
|
+
if (isValidString(data.params.value, 1) && ['disabled', 'light', 'outlet', 'switch', 'mounted_switch'].includes(data.params.value)) {
|
|
2215
|
+
this.matterbridge.virtualMode = data.params.value;
|
|
2216
|
+
this.log.debug(`Set matterbridge virtual mode to ${CYAN}${data.params.value}${db}`);
|
|
2217
|
+
await this.matterbridge.nodeContext?.set('virtualmode', data.params.value);
|
|
2218
|
+
this.wssSendRestartRequired();
|
|
2219
|
+
}
|
|
2220
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2221
|
+
break;
|
|
2222
|
+
default:
|
|
2223
|
+
this.log.warn(`Unknown parameter ${data.params.name} in /api/config`);
|
|
2224
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `Unknown parameter ${data.params.name} in /api/config` });
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
else if (data.method === '/api/command') {
|
|
2228
|
+
const localData = data;
|
|
2229
|
+
if (!isValidString(data.params.command, 5)) {
|
|
2230
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter command in /api/command' });
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
if (data.params.command === 'selectdevice' && isValidString(data.params.plugin, 10) && isValidString(data.params.serial, 1) && isValidString(data.params.name, 1)) {
|
|
2234
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
2235
|
+
if (!plugin) {
|
|
2236
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' });
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const config = plugin.configJson;
|
|
2240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2241
|
+
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2242
|
+
// this.log.debug(`SelectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
2243
|
+
if (select === 'serial')
|
|
2244
|
+
this.log.info(`Selected device serial ${data.params.serial}`);
|
|
2245
|
+
if (select === 'name')
|
|
2246
|
+
this.log.info(`Selected device name ${data.params.name}`);
|
|
2247
|
+
if (config && select && (select === 'serial' || select === 'name')) {
|
|
2248
|
+
// Remove postfix from the serial if it exists
|
|
2249
|
+
if (config.postfix) {
|
|
2250
|
+
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
2251
|
+
}
|
|
2252
|
+
// Add the serial to the whiteList if the whiteList exists and the serial or name is not already in it
|
|
2253
|
+
if (isValidArray(config.whiteList, 1)) {
|
|
2254
|
+
if (select === 'serial' && !config.whiteList.includes(data.params.serial)) {
|
|
2255
|
+
config.whiteList.push(data.params.serial);
|
|
2256
|
+
}
|
|
2257
|
+
else if (select === 'name' && !config.whiteList.includes(data.params.name)) {
|
|
2258
|
+
config.whiteList.push(data.params.name);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
// Remove the serial from the blackList if the blackList exists and the serial or name is in it
|
|
2262
|
+
if (isValidArray(config.blackList, 1)) {
|
|
2263
|
+
if (select === 'serial' && config.blackList.includes(data.params.serial)) {
|
|
2264
|
+
config.blackList = config.blackList.filter((item) => item !== localData.params.serial);
|
|
2265
|
+
}
|
|
2266
|
+
else if (select === 'name' && config.blackList.includes(data.params.name)) {
|
|
2267
|
+
config.blackList = config.blackList.filter((item) => item !== localData.params.name);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
if (plugin.platform)
|
|
2271
|
+
plugin.platform.config = config;
|
|
2272
|
+
plugin.configJson = config;
|
|
2273
|
+
await this.matterbridge.plugins.saveConfigFromPlugin(plugin, true);
|
|
2274
|
+
this.wssSendRestartRequired(false);
|
|
2275
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
this.log.error(`SelectDevice: select ${select} not supported`);
|
|
2279
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `SelectDevice: select ${select} not supported` });
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
else if (data.params.command === 'unselectdevice' &&
|
|
2283
|
+
isValidString(data.params.plugin, 10) &&
|
|
2284
|
+
isValidString(data.params.serial, 1) &&
|
|
2285
|
+
isValidString(data.params.name, 1)) {
|
|
2286
|
+
const plugin = this.matterbridge.plugins.get(data.params.plugin);
|
|
2287
|
+
if (!plugin) {
|
|
2288
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/command' });
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const config = plugin.configJson;
|
|
2292
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2293
|
+
const select = plugin.schemaJson?.properties?.blackList?.selectFrom;
|
|
2294
|
+
// this.log.debug(`UnselectDevice(selectMode ${select}) data ${debugStringify(data)}`);
|
|
2295
|
+
if (select === 'serial')
|
|
2296
|
+
this.log.info(`Unselected device serial ${data.params.serial}`);
|
|
2297
|
+
if (select === 'name')
|
|
2298
|
+
this.log.info(`Unselected device name ${data.params.name}`);
|
|
2299
|
+
if (config && select && (select === 'serial' || select === 'name')) {
|
|
2300
|
+
if (config.postfix) {
|
|
2301
|
+
data.params.serial = data.params.serial.replace('-' + config.postfix, '');
|
|
2302
|
+
}
|
|
2303
|
+
// Remove the serial from the whiteList if the whiteList exists and the serial is in it
|
|
2304
|
+
if (isValidArray(config.whiteList, 1)) {
|
|
2305
|
+
if (select === 'serial' && config.whiteList.includes(data.params.serial)) {
|
|
2306
|
+
config.whiteList = config.whiteList.filter((item) => item !== localData.params.serial);
|
|
2307
|
+
}
|
|
2308
|
+
else if (select === 'name' && config.whiteList.includes(data.params.name)) {
|
|
2309
|
+
config.whiteList = config.whiteList.filter((item) => item !== localData.params.name);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
// Add the serial to the blackList
|
|
2313
|
+
if (isValidArray(config.blackList)) {
|
|
2314
|
+
if (select === 'serial' && !config.blackList.includes(data.params.serial)) {
|
|
2315
|
+
config.blackList.push(data.params.serial);
|
|
2316
|
+
}
|
|
2317
|
+
else if (select === 'name' && !config.blackList.includes(data.params.name)) {
|
|
2318
|
+
config.blackList.push(data.params.name);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (plugin.platform)
|
|
2322
|
+
plugin.platform.config = config;
|
|
2323
|
+
plugin.configJson = config;
|
|
2324
|
+
await this.matterbridge.plugins.saveConfigFromPlugin(plugin, true);
|
|
2325
|
+
this.wssSendRestartRequired(false);
|
|
2326
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true });
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
this.log.error(`SelectDevice: select ${select} not supported`);
|
|
2330
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: `SelectDevice: select ${select} not supported` });
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
else {
|
|
2335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2336
|
+
const localData = data;
|
|
2337
|
+
this.log.error(`Invalid method from websocket client: ${debugStringify(localData)}`);
|
|
2338
|
+
sendResponse({ id: localData.id, method: localData.method, src: 'Matterbridge', dst: localData.src, error: 'Invalid method' });
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
catch (error) {
|
|
2342
|
+
inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Sends a WebSocket log message to all connected clients. The function is called by AnsiLogger.setGlobalCallback.
|
|
2347
|
+
*
|
|
2348
|
+
* @param {string} level - The logger level of the message: debug info notice warn error fatal...
|
|
2349
|
+
* @param {string} time - The time string of the message
|
|
2350
|
+
* @param {string} name - The logger name of the message
|
|
2351
|
+
* @param {string} message - The content of the message.
|
|
2352
|
+
*
|
|
2353
|
+
* @remarks
|
|
2354
|
+
* The function removes ANSI escape codes, leading asterisks, non-printable characters, and replaces all occurrences of \t and \n.
|
|
2355
|
+
* It also replaces all occurrences of \" with " and angle-brackets with < and >.
|
|
2356
|
+
* The function sends the message to all connected clients.
|
|
2357
|
+
*/
|
|
2358
|
+
wssSendLogMessage(level, time, name, message) {
|
|
2359
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2360
|
+
return;
|
|
2361
|
+
if (!level || !time || !name || !message)
|
|
2362
|
+
return;
|
|
2363
|
+
// Remove ANSI escape codes from the message
|
|
2364
|
+
// eslint-disable-next-line no-control-regex
|
|
2365
|
+
message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
|
|
2366
|
+
// Remove leading asterisks from the message
|
|
2367
|
+
message = message.replace(/^\*+/, '');
|
|
2368
|
+
// Replace all occurrences of \t and \n
|
|
2369
|
+
message = message.replace(/[\t\n]/g, '');
|
|
2370
|
+
// Remove non-printable characters
|
|
2371
|
+
// eslint-disable-next-line no-control-regex
|
|
2372
|
+
message = message.replace(/[\x00-\x1F\x7F]/g, '');
|
|
2373
|
+
// Replace all occurrences of \" with "
|
|
2374
|
+
message = message.replace(/\\"/g, '"');
|
|
2375
|
+
// Define the maximum allowed length for continuous characters without a space
|
|
2376
|
+
const maxContinuousLength = 100;
|
|
2377
|
+
const keepStartLength = 20;
|
|
2378
|
+
const keepEndLength = 20;
|
|
2379
|
+
// Split the message into words
|
|
2380
|
+
if (level !== 'spawn') {
|
|
2381
|
+
message = message
|
|
2382
|
+
.split(' ')
|
|
2383
|
+
.map((word) => {
|
|
2384
|
+
// If the word length exceeds the max continuous length, insert spaces and truncate
|
|
2385
|
+
if (word.length > maxContinuousLength) {
|
|
2386
|
+
return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
|
|
2387
|
+
}
|
|
2388
|
+
return word;
|
|
2389
|
+
})
|
|
2390
|
+
.join(' ');
|
|
2391
|
+
}
|
|
2392
|
+
// Send the message to all connected clients
|
|
2393
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'log', success: true, response: { level, time, name, message } });
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Sends a need to refresh WebSocket message to all connected clients.
|
|
2397
|
+
*
|
|
2398
|
+
* @param {string} changed - The changed value.
|
|
2399
|
+
* @param {Record<string, unknown>} params - Additional parameters to send with the message.
|
|
2400
|
+
* possible values for changed:
|
|
2401
|
+
* - 'settings' (when the bridge has started in bridge mode or childbridge mode and when update finds a new version)
|
|
2402
|
+
* - 'plugins'
|
|
2403
|
+
* - 'devices'
|
|
2404
|
+
* - 'matter' with param 'matter' (QRDiv component)
|
|
2405
|
+
* @param {ApiMatter} params.matter - The matter device that has changed. Required if changed is 'matter'.
|
|
2406
|
+
*/
|
|
2407
|
+
wssSendRefreshRequired(changed, params) {
|
|
2408
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2409
|
+
return;
|
|
2410
|
+
this.log.debug('Sending a refresh required message to all connected clients');
|
|
2411
|
+
// Send the message to all connected clients
|
|
2412
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', success: true, response: { changed, ...params } });
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Sends a need to restart WebSocket message to all connected clients.
|
|
2416
|
+
*
|
|
2417
|
+
* @param {boolean} snackbar - If true, a snackbar message will be sent to all connected clients. Default is true.
|
|
2418
|
+
* @param {boolean} fixed - If true, the restart is fixed and will not be reset by plugin restarts. Default is false.
|
|
2419
|
+
*/
|
|
2420
|
+
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
2421
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2422
|
+
return;
|
|
2423
|
+
this.log.debug('Sending a restart required message to all connected clients');
|
|
2424
|
+
this.matterbridge.restartRequired = true;
|
|
2425
|
+
this.matterbridge.fixedRestartRequired = fixed;
|
|
2426
|
+
if (snackbar === true)
|
|
2427
|
+
this.wssSendSnackbarMessage(`Restart required`, 0);
|
|
2428
|
+
// Send the message to all connected clients
|
|
2429
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', success: true, response: { fixed } });
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Sends a no need to restart WebSocket message to all connected clients.
|
|
2433
|
+
*
|
|
2434
|
+
* @param {boolean} snackbar - If true, the snackbar message will be cleared from all connected clients. Default is true.
|
|
2435
|
+
*/
|
|
2436
|
+
wssSendRestartNotRequired(snackbar = true) {
|
|
2437
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2438
|
+
return;
|
|
2439
|
+
this.log.debug('Sending a restart not required message to all connected clients');
|
|
2440
|
+
this.matterbridge.restartRequired = false;
|
|
2441
|
+
if (snackbar === true)
|
|
2442
|
+
this.wssSendCloseSnackbarMessage(`Restart required`);
|
|
2443
|
+
// Send the message to all connected clients
|
|
2444
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'restart_not_required', success: true });
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Sends a need to update WebSocket message to all connected clients.
|
|
2448
|
+
*
|
|
2449
|
+
* @param {boolean} devVersion - If true, the update is for a development version. Default is false.
|
|
2450
|
+
*/
|
|
2451
|
+
wssSendUpdateRequired(devVersion = false) {
|
|
2452
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2453
|
+
return;
|
|
2454
|
+
this.log.debug('Sending an update required message to all connected clients');
|
|
2455
|
+
this.matterbridge.updateRequired = true;
|
|
2456
|
+
// Send the message to all connected clients
|
|
2457
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'update_required', success: true, response: { devVersion } });
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Sends a cpu update message to all connected clients.
|
|
2461
|
+
*
|
|
2462
|
+
* @param {number} cpuUsage - The CPU usage percentage to send.
|
|
2463
|
+
* @param {number} processCpuUsage - The CPU usage percentage of the process to send.
|
|
2464
|
+
*/
|
|
2465
|
+
wssSendCpuUpdate(cpuUsage, processCpuUsage) {
|
|
2466
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2467
|
+
return;
|
|
2468
|
+
// istanbul ignore else
|
|
2469
|
+
if (hasParameter('debug'))
|
|
2470
|
+
this.log.debug('Sending a cpu update message to all connected clients');
|
|
2471
|
+
// Send the message to all connected clients
|
|
2472
|
+
this.wssBroadcastMessage({
|
|
2473
|
+
id: 0,
|
|
2474
|
+
src: 'Matterbridge',
|
|
2475
|
+
dst: 'Frontend',
|
|
2476
|
+
method: 'cpu_update',
|
|
2477
|
+
success: true,
|
|
2478
|
+
response: { cpuUsage: Math.round(cpuUsage * 100) / 100, processCpuUsage: Math.round(processCpuUsage * 100) / 100 },
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Sends a memory update message to all connected clients.
|
|
2483
|
+
*
|
|
2484
|
+
* @param {string} totalMemory - The total memory in bytes.
|
|
2485
|
+
* @param {string} freeMemory - The free memory in bytes.
|
|
2486
|
+
* @param {string} rss - The resident set size in bytes.
|
|
2487
|
+
* @param {string} heapTotal - The total heap memory in bytes.
|
|
2488
|
+
* @param {string} heapUsed - The used heap memory in bytes.
|
|
2489
|
+
* @param {string} external - The external memory in bytes.
|
|
2490
|
+
* @param {string} arrayBuffers - The array buffers memory in bytes.
|
|
2491
|
+
*/
|
|
2492
|
+
wssSendMemoryUpdate(totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers) {
|
|
2493
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2494
|
+
return;
|
|
2495
|
+
// istanbul ignore else
|
|
2496
|
+
if (hasParameter('debug'))
|
|
2497
|
+
this.log.debug('Sending a memory update message to all connected clients');
|
|
2498
|
+
// Send the message to all connected clients
|
|
2499
|
+
this.wssBroadcastMessage({
|
|
2500
|
+
id: 0,
|
|
2501
|
+
src: 'Matterbridge',
|
|
2502
|
+
dst: 'Frontend',
|
|
2503
|
+
method: 'memory_update',
|
|
2504
|
+
success: true,
|
|
2505
|
+
response: { totalMemory, freeMemory, rss, heapTotal, heapUsed, external, arrayBuffers },
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Sends an uptime update message to all connected clients.
|
|
2510
|
+
*
|
|
2511
|
+
* @param {string} systemUptime - The system uptime in a human-readable format.
|
|
2512
|
+
* @param {string} processUptime - The process uptime in a human-readable format.
|
|
2513
|
+
*/
|
|
2514
|
+
wssSendUptimeUpdate(systemUptime, processUptime) {
|
|
2515
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2516
|
+
return;
|
|
2517
|
+
// istanbul ignore else
|
|
2518
|
+
if (hasParameter('debug'))
|
|
2519
|
+
this.log.debug('Sending a uptime update message to all connected clients');
|
|
2520
|
+
// Send the message to all connected clients
|
|
2521
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'uptime_update', success: true, response: { systemUptime, processUptime } });
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Sends an open snackbar message to all connected clients.
|
|
2525
|
+
*
|
|
2526
|
+
* @param {string} message - The message to send.
|
|
2527
|
+
* @param {number} timeout - The timeout in seconds for the snackbar message. Default is 5 seconds.
|
|
2528
|
+
* @param {'info' | 'warning' | 'error' | 'success'} severity - The severity of the message.
|
|
2529
|
+
* possible values are: 'info', 'warning', 'error', 'success'. Default is 'info'.
|
|
2530
|
+
*
|
|
2531
|
+
* @remarks
|
|
2532
|
+
* If timeout is 0, the snackbar message will be displayed until closed by the user.
|
|
2533
|
+
*/
|
|
2534
|
+
wssSendSnackbarMessage(message, timeout = 5, severity = 'info') {
|
|
2535
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2536
|
+
return;
|
|
2537
|
+
this.log.debug('Sending a snackbar message to all connected clients');
|
|
2538
|
+
// Send the message to all connected clients
|
|
2539
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'snackbar', success: true, response: { message, timeout, severity } });
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Sends a close snackbar message to all connected clients.
|
|
2543
|
+
* It will close the snackbar message with the same message and timeout = 0.
|
|
2544
|
+
*
|
|
2545
|
+
* @param {string} message - The message to send.
|
|
2546
|
+
*/
|
|
2547
|
+
wssSendCloseSnackbarMessage(message) {
|
|
2548
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2549
|
+
return;
|
|
2550
|
+
this.log.debug('Sending a close snackbar message to all connected clients');
|
|
2551
|
+
// Send the message to all connected clients
|
|
2552
|
+
this.wssBroadcastMessage({ id: 0, src: 'Matterbridge', dst: 'Frontend', method: 'close_snackbar', success: true, response: { message } });
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Sends an attribute update message to all connected WebSocket clients.
|
|
2556
|
+
*
|
|
2557
|
+
* @param {string | undefined} plugin - The name of the plugin.
|
|
2558
|
+
* @param {string | undefined} serialNumber - The serial number of the device.
|
|
2559
|
+
* @param {string | undefined} uniqueId - The unique identifier of the device.
|
|
2560
|
+
* @param {EndpointNumber} number - The endpoint number where the attribute belongs.
|
|
2561
|
+
* @param {string} id - The endpoint id where the attribute belongs.
|
|
2562
|
+
* @param {string} cluster - The cluster name where the attribute belongs.
|
|
2563
|
+
* @param {string} attribute - The name of the attribute that changed.
|
|
2564
|
+
* @param {number | string | boolean} value - The new value of the attribute.
|
|
2565
|
+
*
|
|
2566
|
+
* @remarks
|
|
2567
|
+
* This method logs a debug message and sends a JSON-formatted message to all connected WebSocket clients
|
|
2568
|
+
* with the updated attribute information.
|
|
2569
|
+
*/
|
|
2570
|
+
wssSendAttributeChangedMessage(plugin, serialNumber, uniqueId, number, id, cluster, attribute, value) {
|
|
2571
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2572
|
+
return;
|
|
2573
|
+
this.log.debug('Sending an attribute update message to all connected clients');
|
|
2574
|
+
// Send the message to all connected clients
|
|
2575
|
+
this.wssBroadcastMessage({
|
|
2576
|
+
id: 0,
|
|
2577
|
+
src: 'Matterbridge',
|
|
2578
|
+
dst: 'Frontend',
|
|
2579
|
+
method: 'state_update',
|
|
2580
|
+
success: true,
|
|
2581
|
+
response: { plugin, serialNumber, uniqueId, number, id, cluster, attribute, value },
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
/**
|
|
2585
|
+
* Sends a message to all connected clients.
|
|
2586
|
+
* This is an helper function to send a broadcast message to all connected clients.
|
|
2587
|
+
*
|
|
2588
|
+
* @param {WsMessageBroadcast} msg - The message to send.
|
|
2589
|
+
*/
|
|
2590
|
+
wssBroadcastMessage(msg) {
|
|
2591
|
+
if (!this.listening || this.webSocketServer?.clients.size === 0)
|
|
2592
|
+
return;
|
|
2593
|
+
// Send the message to all connected clients
|
|
2594
|
+
const stringifiedMsg = JSON.stringify(msg);
|
|
2595
|
+
if (msg.method !== 'log')
|
|
2596
|
+
this.log.debug(`Sending a broadcast message: ${debugStringify(msg)}`);
|
|
2597
|
+
this.webSocketServer?.clients.forEach((client) => {
|
|
2598
|
+
// istanbul ignore else
|
|
2599
|
+
if (client.readyState === client.OPEN) {
|
|
2600
|
+
client.send(stringifiedMsg);
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
//# sourceMappingURL=frontend.js.map
|