@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
|
@@ -0,0 +1,2880 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Matterbridge.
|
|
3
|
+
*
|
|
4
|
+
* @file matterbridge.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @created 2023-12-29
|
|
7
|
+
* @version 1.6.2
|
|
8
|
+
* @license Apache-2.0
|
|
9
|
+
*
|
|
10
|
+
* Copyright 2023, 2024, 2025, 2026 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
|
|
25
|
+
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
26
|
+
console.log('\u001B[32mMatterbridge loaded.\u001B[40;0m');
|
|
27
|
+
// Node.js modules
|
|
28
|
+
import os from 'node:os';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import fs, { unlinkSync } from 'node:fs';
|
|
32
|
+
import EventEmitter from 'node:events';
|
|
33
|
+
import { inspect } from 'node:util';
|
|
34
|
+
// AnsiLogger module
|
|
35
|
+
import { AnsiLogger, UNDERLINE, UNDERLINEOFF, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt, BLUE, or, } from 'node-ansi-logger';
|
|
36
|
+
// NodeStorage module
|
|
37
|
+
import { NodeStorageManager } from 'node-persist-manager';
|
|
38
|
+
// @matter
|
|
39
|
+
import '@matter/nodejs'; // Set up Node.js environment for matter.js
|
|
40
|
+
import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, UINT32_MAX, UINT16_MAX, Crypto, Environment, StorageService, } from '@matter/general';
|
|
41
|
+
import { PaseClient } from '@matter/protocol';
|
|
42
|
+
import { Endpoint, ServerNode } from '@matter/node';
|
|
43
|
+
import { DeviceTypeId, VendorId } from '@matter/types/datatype';
|
|
44
|
+
import { AggregatorEndpoint } from '@matter/node/endpoints';
|
|
45
|
+
import { BasicInformationServer } from '@matter/node/behaviors/basic-information';
|
|
46
|
+
// @matterbridge
|
|
47
|
+
import { copyDirectory, createDirectory, formatBytes, formatPercent, formatUptime, getIntParameter, getParameter, hasParameter, isValidNumber, isValidObject, isValidString, parseVersionString, } from '@matterbridge/utils';
|
|
48
|
+
import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, typ } from '@matterbridge/types';
|
|
49
|
+
import { BroadcastServer } from '@matterbridge/thread';
|
|
50
|
+
// Matterbridge
|
|
51
|
+
import { PluginManager } from './pluginManager.js';
|
|
52
|
+
import { DeviceManager } from './deviceManager.js';
|
|
53
|
+
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
54
|
+
import { bridge } from './matterbridgeDeviceTypes.js';
|
|
55
|
+
import { Frontend } from './frontend.js';
|
|
56
|
+
import { addVirtualDevice, addVirtualDevices } from './helpers.js';
|
|
57
|
+
/**
|
|
58
|
+
* Represents the Matterbridge application.
|
|
59
|
+
*/
|
|
60
|
+
export class Matterbridge extends EventEmitter {
|
|
61
|
+
/** Matterbridge system information */
|
|
62
|
+
systemInformation = {
|
|
63
|
+
// Network properties
|
|
64
|
+
interfaceName: '',
|
|
65
|
+
macAddress: '',
|
|
66
|
+
ipv4Address: '',
|
|
67
|
+
ipv6Address: '',
|
|
68
|
+
// Node.js properties
|
|
69
|
+
nodeVersion: '',
|
|
70
|
+
// Fixed system properties
|
|
71
|
+
hostname: '',
|
|
72
|
+
user: '',
|
|
73
|
+
osType: '',
|
|
74
|
+
osRelease: '',
|
|
75
|
+
osPlatform: '',
|
|
76
|
+
osArch: '',
|
|
77
|
+
// Cpu and memory properties
|
|
78
|
+
totalMemory: '',
|
|
79
|
+
freeMemory: '',
|
|
80
|
+
systemUptime: '',
|
|
81
|
+
processUptime: '',
|
|
82
|
+
cpuUsage: '',
|
|
83
|
+
processCpuUsage: '',
|
|
84
|
+
rss: '',
|
|
85
|
+
heapTotal: '',
|
|
86
|
+
heapUsed: '',
|
|
87
|
+
};
|
|
88
|
+
// Matterbridge settings
|
|
89
|
+
/** It indicates the home directory of the Matterbridge application. The home directory is the base directory where Matterbridge creates the matterbridge directories (os.homedir() if not overridden). */
|
|
90
|
+
homeDirectory = '';
|
|
91
|
+
/** It indicates the root directory of the Matterbridge application. The root directory is the directory where Matterbridge is executed. */
|
|
92
|
+
rootDirectory = '';
|
|
93
|
+
/** It indicates where the directory .matterbridge is located. */
|
|
94
|
+
matterbridgeDirectory = '';
|
|
95
|
+
/** It indicates where the directory Matterbridge is located. */
|
|
96
|
+
matterbridgePluginDirectory = '';
|
|
97
|
+
/** It indicates where the directory .mattercert is located. */
|
|
98
|
+
matterbridgeCertDirectory = '';
|
|
99
|
+
/** It indicates the global modules directory for npm. */
|
|
100
|
+
globalModulesDirectory = '';
|
|
101
|
+
matterbridgeVersion = '';
|
|
102
|
+
matterbridgeLatestVersion = '';
|
|
103
|
+
matterbridgeDevVersion = '';
|
|
104
|
+
frontendVersion = '';
|
|
105
|
+
/** It indicates the mode of the Matterbridge instance. It can be 'bridge', 'childbridge', 'controller' or ''. */
|
|
106
|
+
bridgeMode = '';
|
|
107
|
+
/** It indicates the restart mode of the Matterbridge instance. It can be 'service', 'docker' or ''. */
|
|
108
|
+
restartMode = '';
|
|
109
|
+
/** It indicates whether virtual mode is enabled and its type. The virtual mode control the creation of "Update matterbridge" and "Restart matterbridge" endpoints. */
|
|
110
|
+
virtualMode = 'outlet';
|
|
111
|
+
/** It indicates the Matterbridge profile in use. */
|
|
112
|
+
profile = getParameter('profile');
|
|
113
|
+
/** Matterbridge logger */
|
|
114
|
+
log = new AnsiLogger({
|
|
115
|
+
logName: 'Matterbridge',
|
|
116
|
+
logNameColor: '\x1b[38;5;115m',
|
|
117
|
+
logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */,
|
|
118
|
+
logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */,
|
|
119
|
+
});
|
|
120
|
+
/** Matterbridge logger level */
|
|
121
|
+
logLevel = this.log.logLevel;
|
|
122
|
+
/** Whether to log to a file */
|
|
123
|
+
fileLogger = false;
|
|
124
|
+
/** Matter logger */
|
|
125
|
+
matterLog = new AnsiLogger({ logName: 'Matter', logNameColor: '\x1b[34m', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
|
|
126
|
+
/** Matter logger level */
|
|
127
|
+
matterLogLevel = this.matterLog.logLevel;
|
|
128
|
+
/** Whether to log Matter to a file */
|
|
129
|
+
matterFileLogger = false;
|
|
130
|
+
// Frontend settings
|
|
131
|
+
readOnly = hasParameter('readonly') || hasParameter('shelly');
|
|
132
|
+
shellyBoard = hasParameter('shelly');
|
|
133
|
+
shellySysUpdate = false;
|
|
134
|
+
shellyMainUpdate = false;
|
|
135
|
+
/** It indicates whether a restart is required. It can be unset in childbridge mode by restarting the plugin that triggered the restart. */
|
|
136
|
+
restartRequired = false;
|
|
137
|
+
/** It indicates whether a fixed restart is required. It cannot be unset once set. */
|
|
138
|
+
fixedRestartRequired = false;
|
|
139
|
+
/** It indicates whether an update is available. */
|
|
140
|
+
updateRequired = false;
|
|
141
|
+
// Managers
|
|
142
|
+
plugins = new PluginManager(this);
|
|
143
|
+
devices = new DeviceManager();
|
|
144
|
+
// Frontend
|
|
145
|
+
frontend = new Frontend(this);
|
|
146
|
+
/** Matterbridge node storage manager created in the directory 'storage' in matterbridgeDirectory */
|
|
147
|
+
nodeStorage;
|
|
148
|
+
/** Matterbridge node context created with name 'matterbridge' */
|
|
149
|
+
nodeContext;
|
|
150
|
+
/** The main instance of the Matterbridge class (singleton) */
|
|
151
|
+
static instance;
|
|
152
|
+
// Instance properties
|
|
153
|
+
shutdown = false;
|
|
154
|
+
failCountLimit = hasParameter('shelly') ? 600 : 120;
|
|
155
|
+
hasCleanupStarted = false;
|
|
156
|
+
initialized = false;
|
|
157
|
+
startMatterInterval;
|
|
158
|
+
startMatterIntervalMs = 1000;
|
|
159
|
+
checkUpdateInterval;
|
|
160
|
+
checkUpdateTimeout;
|
|
161
|
+
configureTimeout;
|
|
162
|
+
reachabilityTimeout;
|
|
163
|
+
sigintHandler;
|
|
164
|
+
sigtermHandler;
|
|
165
|
+
exceptionHandler;
|
|
166
|
+
rejectionHandler;
|
|
167
|
+
/** Matter environment default */
|
|
168
|
+
environment = Environment.default;
|
|
169
|
+
/** Matter storage service from environment default */
|
|
170
|
+
matterStorageService;
|
|
171
|
+
/** Matter storage manager created with name 'Matterbridge' */
|
|
172
|
+
matterStorageManager;
|
|
173
|
+
/** Matter matterbridge storage context created in the storage manager with name 'persist' */
|
|
174
|
+
matterbridgeContext;
|
|
175
|
+
controllerContext;
|
|
176
|
+
/** Matter mdns interface e.g. 'eth0' or 'wlan0' or 'Wi-Fi' */
|
|
177
|
+
mdnsInterface;
|
|
178
|
+
/** Matter listeningAddressIpv4 address */
|
|
179
|
+
ipv4Address;
|
|
180
|
+
/** Matter listeningAddressIpv6 address */
|
|
181
|
+
ipv6Address;
|
|
182
|
+
/** Matter commissioning port */
|
|
183
|
+
port; // first server node port
|
|
184
|
+
/** Matter commissioning passcode */
|
|
185
|
+
passcode; // first server node passcode
|
|
186
|
+
/** Matter commissioning discriminator */
|
|
187
|
+
discriminator; // first server node discriminator
|
|
188
|
+
/** Matter device certification */
|
|
189
|
+
certification; // device certification
|
|
190
|
+
/** Matter server node in bridge mode */
|
|
191
|
+
serverNode;
|
|
192
|
+
/** Matter aggregator node in bridge mode */
|
|
193
|
+
aggregatorNode;
|
|
194
|
+
aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
|
|
195
|
+
aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
|
|
196
|
+
aggregatorProductId = getIntParameter('productId') ?? 0x8000;
|
|
197
|
+
aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
|
|
198
|
+
aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
|
|
199
|
+
aggregatorSerialNumber = getParameter('serialNumber');
|
|
200
|
+
aggregatorUniqueId = getParameter('uniqueId');
|
|
201
|
+
/** Advertising nodes map: time advertising started keyed by storeId */
|
|
202
|
+
advertisingNodes = new Map();
|
|
203
|
+
/** Broadcast server */
|
|
204
|
+
server;
|
|
205
|
+
verbose = hasParameter('verbose');
|
|
206
|
+
/** We load asyncronously so is private */
|
|
207
|
+
constructor() {
|
|
208
|
+
super();
|
|
209
|
+
this.server = new BroadcastServer('matterbridge', this.log);
|
|
210
|
+
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
211
|
+
}
|
|
212
|
+
/** Close the broadcast server */
|
|
213
|
+
destroy() {
|
|
214
|
+
this.server.close();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get a platform matterbridge object
|
|
218
|
+
*
|
|
219
|
+
* @returns {PlatformMatterbridge} The platform matterbridge object.
|
|
220
|
+
*/
|
|
221
|
+
getPlatformMatterbridge() {
|
|
222
|
+
return {
|
|
223
|
+
systemInformation: { ...this.systemInformation },
|
|
224
|
+
rootDirectory: this.rootDirectory,
|
|
225
|
+
homeDirectory: this.homeDirectory,
|
|
226
|
+
matterbridgeDirectory: this.matterbridgeDirectory,
|
|
227
|
+
matterbridgePluginDirectory: this.matterbridgePluginDirectory,
|
|
228
|
+
matterbridgeCertDirectory: this.matterbridgeCertDirectory,
|
|
229
|
+
globalModulesDirectory: this.globalModulesDirectory,
|
|
230
|
+
matterbridgeVersion: this.matterbridgeVersion,
|
|
231
|
+
matterbridgeLatestVersion: this.matterbridgeLatestVersion,
|
|
232
|
+
matterbridgeDevVersion: this.matterbridgeDevVersion,
|
|
233
|
+
frontendVersion: this.frontendVersion,
|
|
234
|
+
bridgeMode: this.bridgeMode,
|
|
235
|
+
restartMode: this.restartMode,
|
|
236
|
+
virtualMode: this.virtualMode,
|
|
237
|
+
aggregatorVendorId: this.aggregatorVendorId,
|
|
238
|
+
aggregatorVendorName: this.aggregatorVendorName,
|
|
239
|
+
aggregatorProductId: this.aggregatorProductId,
|
|
240
|
+
aggregatorProductName: this.aggregatorProductName,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get a shared matterbridge object
|
|
245
|
+
*
|
|
246
|
+
* @returns {SharedMatterbridge} The shared matterbridge object.
|
|
247
|
+
*/
|
|
248
|
+
getSharedMatterbridge() {
|
|
249
|
+
return {
|
|
250
|
+
systemInformation: { ...this.systemInformation },
|
|
251
|
+
rootDirectory: this.rootDirectory,
|
|
252
|
+
homeDirectory: this.homeDirectory,
|
|
253
|
+
matterbridgeDirectory: this.matterbridgeDirectory,
|
|
254
|
+
matterbridgePluginDirectory: this.matterbridgePluginDirectory,
|
|
255
|
+
matterbridgeCertDirectory: this.matterbridgeCertDirectory,
|
|
256
|
+
globalModulesDirectory: this.globalModulesDirectory,
|
|
257
|
+
matterbridgeVersion: this.matterbridgeVersion,
|
|
258
|
+
matterbridgeLatestVersion: this.matterbridgeLatestVersion,
|
|
259
|
+
matterbridgeDevVersion: this.matterbridgeDevVersion,
|
|
260
|
+
frontendVersion: this.frontendVersion,
|
|
261
|
+
bridgeMode: this.bridgeMode,
|
|
262
|
+
restartMode: this.restartMode,
|
|
263
|
+
virtualMode: this.virtualMode,
|
|
264
|
+
profile: this.profile,
|
|
265
|
+
logLevel: this.logLevel,
|
|
266
|
+
fileLogger: this.fileLogger,
|
|
267
|
+
matterLogLevel: this.matterLogLevel,
|
|
268
|
+
matterFileLogger: this.matterFileLogger,
|
|
269
|
+
mdnsInterface: this.mdnsInterface,
|
|
270
|
+
ipv4Address: this.ipv4Address,
|
|
271
|
+
ipv6Address: this.ipv6Address,
|
|
272
|
+
port: this.port,
|
|
273
|
+
discriminator: this.discriminator,
|
|
274
|
+
passcode: this.passcode,
|
|
275
|
+
shellySysUpdate: this.shellySysUpdate,
|
|
276
|
+
shellyMainUpdate: this.shellyMainUpdate,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async msgHandler(msg) {
|
|
280
|
+
if (this.server.isWorkerRequest(msg) && (msg.dst === 'all' || msg.dst === 'matterbridge')) {
|
|
281
|
+
if (this.verbose)
|
|
282
|
+
this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
283
|
+
switch (msg.type) {
|
|
284
|
+
case 'get_log_level':
|
|
285
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
286
|
+
break;
|
|
287
|
+
case 'set_log_level':
|
|
288
|
+
this.log.logLevel = msg.params.logLevel;
|
|
289
|
+
this.server.respond({ ...msg, result: { logLevel: this.log.logLevel } });
|
|
290
|
+
break;
|
|
291
|
+
case 'matterbridge_latest_version':
|
|
292
|
+
this.matterbridgeLatestVersion = msg.params.version;
|
|
293
|
+
await this.nodeContext?.set('matterbridgeLatestVersion', msg.params.version);
|
|
294
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
295
|
+
break;
|
|
296
|
+
case 'matterbridge_dev_version':
|
|
297
|
+
this.matterbridgeDevVersion = msg.params.version;
|
|
298
|
+
await this.nodeContext?.set('matterbridgeDevVersion', msg.params.version);
|
|
299
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
300
|
+
break;
|
|
301
|
+
case 'matterbridge_global_prefix':
|
|
302
|
+
this.globalModulesDirectory = msg.params.prefix;
|
|
303
|
+
await this.nodeContext?.set('globalModulesDirectory', msg.params.prefix);
|
|
304
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
305
|
+
break;
|
|
306
|
+
case 'matterbridge_sys_update':
|
|
307
|
+
this.shellySysUpdate = true;
|
|
308
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
309
|
+
break;
|
|
310
|
+
case 'matterbridge_main_update':
|
|
311
|
+
this.shellyMainUpdate = true;
|
|
312
|
+
this.server.respond({ ...msg, result: { success: true } });
|
|
313
|
+
break;
|
|
314
|
+
case 'matterbridge_platform':
|
|
315
|
+
this.server.respond({ ...msg, result: { data: this.getPlatformMatterbridge(), success: true } });
|
|
316
|
+
break;
|
|
317
|
+
case 'matterbridge_shared':
|
|
318
|
+
this.server.respond({ ...msg, result: { data: this.getSharedMatterbridge(), success: true } });
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
if (this.verbose)
|
|
322
|
+
this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
//* ************************************************************************************************************************************ */
|
|
327
|
+
// loadInstance() and cleanup() methods */
|
|
328
|
+
//* ************************************************************************************************************************************ */
|
|
329
|
+
/**
|
|
330
|
+
* Loads an instance of the Matterbridge class.
|
|
331
|
+
* If an instance already exists, return that instance.
|
|
332
|
+
*
|
|
333
|
+
* @param {boolean} initialize - Whether to initialize the Matterbridge instance after loading. Defaults to false.
|
|
334
|
+
* @returns {Matterbridge} A promise that resolves to the Matterbridge instance.
|
|
335
|
+
*/
|
|
336
|
+
static async loadInstance(initialize = false) {
|
|
337
|
+
if (!Matterbridge.instance) {
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
if (hasParameter('debug'))
|
|
340
|
+
console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
|
|
341
|
+
Matterbridge.instance = new Matterbridge();
|
|
342
|
+
if (initialize)
|
|
343
|
+
await Matterbridge.instance.initialize();
|
|
344
|
+
}
|
|
345
|
+
return Matterbridge.instance;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Initializes the Matterbridge application.
|
|
349
|
+
*
|
|
350
|
+
* @remarks
|
|
351
|
+
* This method performs the necessary setup and initialization steps for the Matterbridge application.
|
|
352
|
+
* It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
|
|
353
|
+
* node version, registers signal handlers, initializes storage, and parses the command line.
|
|
354
|
+
*
|
|
355
|
+
* @returns {Promise<void>} A Promise that resolves when the initialization is complete.
|
|
356
|
+
*/
|
|
357
|
+
async initialize() {
|
|
358
|
+
// for (let i = 1; i <= 255; i++) console.log(`\x1b[38;5;${i}mColor: ${i}`);
|
|
359
|
+
// Emit the initialize_started event
|
|
360
|
+
this.emit('initialize_started');
|
|
361
|
+
// Set the restart mode
|
|
362
|
+
if (hasParameter('service'))
|
|
363
|
+
this.restartMode = 'service';
|
|
364
|
+
if (hasParameter('docker'))
|
|
365
|
+
this.restartMode = 'docker';
|
|
366
|
+
// Set the matterbridge home directory
|
|
367
|
+
this.homeDirectory = getParameter('homedir') ?? os.homedir();
|
|
368
|
+
await createDirectory(this.homeDirectory, 'Matterbridge Home Directory', this.log);
|
|
369
|
+
// Set the matterbridge directory
|
|
370
|
+
this.matterbridgeDirectory = this.profile ? path.join(this.homeDirectory, '.matterbridge', 'profiles', this.profile) : path.join(this.homeDirectory, '.matterbridge');
|
|
371
|
+
await createDirectory(this.matterbridgeDirectory, 'Matterbridge Directory', this.log);
|
|
372
|
+
await createDirectory(path.join(this.matterbridgeDirectory, 'certs'), 'Matterbridge Frontend Certificate Directory', this.log);
|
|
373
|
+
await createDirectory(path.join(this.matterbridgeDirectory, 'uploads'), 'Matterbridge Frontend Uploads Directory', this.log);
|
|
374
|
+
// Set the matterbridge plugin directory
|
|
375
|
+
this.matterbridgePluginDirectory = this.profile ? path.join(this.homeDirectory, 'Matterbridge', 'profiles', this.profile) : path.join(this.homeDirectory, 'Matterbridge');
|
|
376
|
+
await createDirectory(this.matterbridgePluginDirectory, 'Matterbridge Plugin Directory', this.log);
|
|
377
|
+
// Set the matterbridge cert directory
|
|
378
|
+
this.matterbridgeCertDirectory = this.profile ? path.join(this.homeDirectory, '.mattercert', 'profiles', this.profile) : path.join(this.homeDirectory, '.mattercert');
|
|
379
|
+
await createDirectory(this.matterbridgeCertDirectory, 'Matterbridge Matter Certificate Directory', this.log);
|
|
380
|
+
// Set the matterbridge root directory
|
|
381
|
+
const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
382
|
+
// Adjust the path for packages core dist directory or node_modules @matterbridge core dist directory
|
|
383
|
+
this.rootDirectory = currentFileDirectory.includes(path.join('packages', 'core'))
|
|
384
|
+
? path.resolve(currentFileDirectory, '../', '../', '../')
|
|
385
|
+
: path.resolve(currentFileDirectory, '../', '../', '..', '../');
|
|
386
|
+
// Setup the matter environment with default values
|
|
387
|
+
this.environment.vars.set('log.level', MatterLogLevel.INFO);
|
|
388
|
+
this.environment.vars.set('log.format', hasParameter('no-ansi') || process.env.NO_COLOR === '1' ? MatterLogFormat.PLAIN : MatterLogFormat.ANSI);
|
|
389
|
+
this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
390
|
+
this.environment.vars.set('runtime.signals', false);
|
|
391
|
+
this.environment.vars.set('runtime.exitcode', false);
|
|
392
|
+
// Register process handlers
|
|
393
|
+
this.registerProcessHandlers();
|
|
394
|
+
// Initialize nodeStorage and nodeContext
|
|
395
|
+
try {
|
|
396
|
+
this.log.debug(`Creating node storage manager: ${CYAN}${NODE_STORAGE_DIR}${db}`);
|
|
397
|
+
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR), writeQueue: false, expiredInterval: undefined, logging: false });
|
|
398
|
+
this.log.debug('Creating node storage context for matterbridge');
|
|
399
|
+
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
400
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
401
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
402
|
+
const keys = (await this.nodeStorage?.storage.keys());
|
|
403
|
+
for (const key of keys) {
|
|
404
|
+
this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
|
|
405
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
406
|
+
await this.nodeStorage?.storage.get(key);
|
|
407
|
+
}
|
|
408
|
+
const storages = await this.nodeStorage.getStorageNames();
|
|
409
|
+
for (const storage of storages) {
|
|
410
|
+
this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
|
|
411
|
+
const nodeContext = await this.nodeStorage?.createStorage(storage);
|
|
412
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
413
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
414
|
+
const keys = (await nodeContext?.storage.keys());
|
|
415
|
+
keys.forEach(async (key) => {
|
|
416
|
+
this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
|
|
417
|
+
await nodeContext?.get(key);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Creating a backup of the node storage since it is not corrupted
|
|
421
|
+
this.log.debug('Creating node storage backup...');
|
|
422
|
+
await copyDirectory(path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR), path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup'));
|
|
423
|
+
this.log.debug('Created node storage backup');
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
// Restoring the backup of the node storage since it is corrupted
|
|
427
|
+
this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
|
|
428
|
+
if (hasParameter('norestore')) {
|
|
429
|
+
this.log.fatal(`The matterbridge storage is corrupted. Found -norestore parameter: exiting...`);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.log.notice(`The matterbridge storage is corrupted. Restoring it with backup...`);
|
|
433
|
+
await copyDirectory(path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup'), path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR));
|
|
434
|
+
this.log.notice(`The matterbridge storage has been restored with backup`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!this.nodeStorage || !this.nodeContext) {
|
|
438
|
+
throw new Error('Fatal error creating node storage manager and context for matterbridge');
|
|
439
|
+
}
|
|
440
|
+
// Set the first port to use for the commissioning server (will be incremented in childbridge mode)
|
|
441
|
+
this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
|
|
442
|
+
// Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
|
|
443
|
+
this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode(this.environment.get(Crypto));
|
|
444
|
+
// Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
|
|
445
|
+
this.discriminator =
|
|
446
|
+
getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator(this.environment.get(Crypto));
|
|
447
|
+
// Certificate management
|
|
448
|
+
const pairingFilePath = path.join(this.matterbridgeCertDirectory, 'pairing.json');
|
|
449
|
+
try {
|
|
450
|
+
await fs.promises.access(pairingFilePath, fs.constants.R_OK);
|
|
451
|
+
const pairingFileContent = await fs.promises.readFile(pairingFilePath, 'utf8');
|
|
452
|
+
const pairingFileJson = JSON.parse(pairingFileContent);
|
|
453
|
+
// Set the vendorId, vendorName, productId, productName, deviceType, serialNumber, uniqueId if they are present in the pairing file
|
|
454
|
+
if (isValidNumber(pairingFileJson.vendorId)) {
|
|
455
|
+
this.aggregatorVendorId = VendorId(pairingFileJson.vendorId);
|
|
456
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorId ${CYAN}${this.aggregatorVendorId}${nf} from pairing file.`);
|
|
457
|
+
}
|
|
458
|
+
if (isValidString(pairingFileJson.vendorName, 3)) {
|
|
459
|
+
this.aggregatorVendorName = pairingFileJson.vendorName;
|
|
460
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorName ${CYAN}${this.aggregatorVendorName}${nf} from pairing file.`);
|
|
461
|
+
}
|
|
462
|
+
if (isValidNumber(pairingFileJson.productId)) {
|
|
463
|
+
this.aggregatorProductId = pairingFileJson.productId;
|
|
464
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productId ${CYAN}${this.aggregatorProductId}${nf} from pairing file.`);
|
|
465
|
+
}
|
|
466
|
+
if (isValidString(pairingFileJson.productName, 3)) {
|
|
467
|
+
this.aggregatorProductName = pairingFileJson.productName;
|
|
468
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productName ${CYAN}${this.aggregatorProductName}${nf} from pairing file.`);
|
|
469
|
+
}
|
|
470
|
+
if (isValidNumber(pairingFileJson.deviceType)) {
|
|
471
|
+
this.aggregatorDeviceType = DeviceTypeId(pairingFileJson.deviceType);
|
|
472
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using deviceType ${CYAN}${this.aggregatorDeviceType}(0x${this.aggregatorDeviceType.toString(16).padStart(4, '0')})${nf} from pairing file.`);
|
|
473
|
+
}
|
|
474
|
+
if (isValidString(pairingFileJson.serialNumber, 3)) {
|
|
475
|
+
this.aggregatorSerialNumber = pairingFileJson.serialNumber;
|
|
476
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using serialNumber ${CYAN}${this.aggregatorSerialNumber}${nf} from pairing file.`);
|
|
477
|
+
}
|
|
478
|
+
if (isValidString(pairingFileJson.uniqueId, 3)) {
|
|
479
|
+
this.aggregatorUniqueId = pairingFileJson.uniqueId;
|
|
480
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using uniqueId ${CYAN}${this.aggregatorUniqueId}${nf} from pairing file.`);
|
|
481
|
+
}
|
|
482
|
+
// Override the passcode and discriminator if they are present in the pairing file
|
|
483
|
+
if (isValidNumber(pairingFileJson.passcode) && isValidNumber(pairingFileJson.discriminator)) {
|
|
484
|
+
this.passcode = pairingFileJson.passcode;
|
|
485
|
+
this.discriminator = pairingFileJson.discriminator;
|
|
486
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf} from pairing file.`);
|
|
487
|
+
}
|
|
488
|
+
// Set the certification for matter.js if it is present in the pairing file
|
|
489
|
+
if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
|
|
490
|
+
const { hexToBuffer } = await import('@matterbridge/utils');
|
|
491
|
+
this.certification = {
|
|
492
|
+
privateKey: hexToBuffer(pairingFileJson.privateKey),
|
|
493
|
+
certificate: hexToBuffer(pairingFileJson.certificate),
|
|
494
|
+
intermediateCertificate: hexToBuffer(pairingFileJson.intermediateCertificate),
|
|
495
|
+
declaration: hexToBuffer(pairingFileJson.declaration),
|
|
496
|
+
};
|
|
497
|
+
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`);
|
|
502
|
+
}
|
|
503
|
+
// Store the passcode, discriminator and port in the node context
|
|
504
|
+
await this.nodeContext.set('matterport', this.port);
|
|
505
|
+
await this.nodeContext.set('matterpasscode', this.passcode);
|
|
506
|
+
await this.nodeContext.set('matterdiscriminator', this.discriminator);
|
|
507
|
+
this.log.debug(`Initializing server node for Matterbridge on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
|
|
508
|
+
// Set matterbridge logger level (context: matterbridgeLogLevel)
|
|
509
|
+
if (hasParameter('logger')) {
|
|
510
|
+
const level = getParameter('logger');
|
|
511
|
+
if (level === 'debug') {
|
|
512
|
+
this.log.logLevel = "debug" /* LogLevel.DEBUG */;
|
|
513
|
+
}
|
|
514
|
+
else if (level === 'info') {
|
|
515
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
516
|
+
}
|
|
517
|
+
else if (level === 'notice') {
|
|
518
|
+
this.log.logLevel = "notice" /* LogLevel.NOTICE */;
|
|
519
|
+
}
|
|
520
|
+
else if (level === 'warn') {
|
|
521
|
+
this.log.logLevel = "warn" /* LogLevel.WARN */;
|
|
522
|
+
}
|
|
523
|
+
else if (level === 'error') {
|
|
524
|
+
this.log.logLevel = "error" /* LogLevel.ERROR */;
|
|
525
|
+
}
|
|
526
|
+
else if (level === 'fatal') {
|
|
527
|
+
this.log.logLevel = "fatal" /* LogLevel.FATAL */;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
|
|
531
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */);
|
|
536
|
+
}
|
|
537
|
+
this.logLevel = this.log.logLevel;
|
|
538
|
+
this.frontend.logLevel = this.log.logLevel;
|
|
539
|
+
MatterbridgeEndpoint.logLevel = this.log.logLevel;
|
|
540
|
+
// Create the file logger for matterbridge (context: matterbridgeFileLog)
|
|
541
|
+
if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
|
|
542
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), this.log.logLevel, true);
|
|
543
|
+
this.fileLogger = true;
|
|
544
|
+
}
|
|
545
|
+
this.log.notice('Matterbridge is starting...');
|
|
546
|
+
this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.fileLogger}.`);
|
|
547
|
+
if (this.profile !== undefined)
|
|
548
|
+
this.log.debug(`Matterbridge profile: ${this.profile}.`);
|
|
549
|
+
// Set matter.js logger level, format and logger (context: matterLogLevel)
|
|
550
|
+
if (hasParameter('matterlogger')) {
|
|
551
|
+
const level = getParameter('matterlogger');
|
|
552
|
+
if (level === 'debug') {
|
|
553
|
+
Logger.level = MatterLogLevel.DEBUG;
|
|
554
|
+
}
|
|
555
|
+
else if (level === 'info') {
|
|
556
|
+
Logger.level = MatterLogLevel.INFO;
|
|
557
|
+
}
|
|
558
|
+
else if (level === 'notice') {
|
|
559
|
+
Logger.level = MatterLogLevel.NOTICE;
|
|
560
|
+
}
|
|
561
|
+
else if (level === 'warn') {
|
|
562
|
+
Logger.level = MatterLogLevel.WARN;
|
|
563
|
+
}
|
|
564
|
+
else if (level === 'error') {
|
|
565
|
+
Logger.level = MatterLogLevel.ERROR;
|
|
566
|
+
}
|
|
567
|
+
else if (level === 'fatal') {
|
|
568
|
+
Logger.level = MatterLogLevel.FATAL;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
this.log.warn(`Invalid matter.js logger level: ${level}. Using default level "info".`);
|
|
572
|
+
Logger.level = MatterLogLevel.INFO;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
Logger.level = (await this.nodeContext.get('matterLogLevel', this.shellyBoard ? MatterLogLevel.NOTICE : MatterLogLevel.INFO));
|
|
577
|
+
}
|
|
578
|
+
Logger.format = MatterLogFormat.ANSI;
|
|
579
|
+
this.matterLogLevel = MatterLogLevel.names[Logger.level];
|
|
580
|
+
// Create the logger for matter.js with file logging (context: matterFileLog)
|
|
581
|
+
if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
|
|
582
|
+
this.matterFileLogger = true;
|
|
583
|
+
}
|
|
584
|
+
Logger.destinations.default.write = this.createDestinationMatterLogger(this.matterFileLogger);
|
|
585
|
+
this.log.debug(`Matter logLevel: ${this.matterLogLevel} fileLoger: ${this.matterFileLogger}.`);
|
|
586
|
+
// Log network interfaces
|
|
587
|
+
const networkInterfaces = os.networkInterfaces();
|
|
588
|
+
const availableAddresses = Object.entries(networkInterfaces);
|
|
589
|
+
const availableInterfaceNames = Object.keys(networkInterfaces);
|
|
590
|
+
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
591
|
+
if (ifaces && ifaces.length > 0) {
|
|
592
|
+
this.log.debug(`Network interface ${BLUE}${ifaceName}${db}:`);
|
|
593
|
+
ifaces.forEach((iface) => {
|
|
594
|
+
this.log.debug(`- ${CYAN}${iface.family}${db} address ${CYAN}${iface.address}${db} netmask ${CYAN}${iface.netmask}${db} mac ${CYAN}${iface.mac}${db}` +
|
|
595
|
+
`${iface.scopeid ? ` scopeid ${CYAN}${iface.scopeid}${db}` : ''}${iface.cidr ? ` cidr ${CYAN}${iface.cidr}${db}` : ''} ${CYAN}${iface.internal ? 'internal' : 'external'}${db}`);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// Set the interface to use for matter server node mdnsInterface
|
|
600
|
+
if (hasParameter('mdnsinterface')) {
|
|
601
|
+
this.mdnsInterface = getParameter('mdnsinterface');
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
this.mdnsInterface = await this.nodeContext.get('mattermdnsinterface', undefined);
|
|
605
|
+
if (this.mdnsInterface === '')
|
|
606
|
+
this.mdnsInterface = undefined;
|
|
607
|
+
}
|
|
608
|
+
// Validate mdnsInterface
|
|
609
|
+
if (this.mdnsInterface) {
|
|
610
|
+
if (!availableInterfaceNames.includes(this.mdnsInterface)) {
|
|
611
|
+
this.log.error(`Invalid mdnsinterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaceNames.join(', ')}. Using all available interfaces.`);
|
|
612
|
+
this.mdnsInterface = undefined;
|
|
613
|
+
await this.nodeContext.remove('mattermdnsinterface');
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
this.log.info(`Using mdnsinterface ${CYAN}${this.mdnsInterface}${nf} for the Matter MdnsBroadcaster.`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (this.mdnsInterface)
|
|
620
|
+
this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
|
|
621
|
+
// Set the listeningAddressIpv4 for the matter commissioning server
|
|
622
|
+
if (hasParameter('ipv4address')) {
|
|
623
|
+
this.ipv4Address = getParameter('ipv4address');
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
this.ipv4Address = await this.nodeContext.get('matteripv4address', undefined);
|
|
627
|
+
if (this.ipv4Address === '')
|
|
628
|
+
this.ipv4Address = undefined;
|
|
629
|
+
}
|
|
630
|
+
// Validate ipv4address
|
|
631
|
+
if (this.ipv4Address) {
|
|
632
|
+
let isValid = false;
|
|
633
|
+
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
634
|
+
if (ifaces && ifaces.find((iface) => iface.address === this.ipv4Address)) {
|
|
635
|
+
this.log.info(`Using ipv4address ${CYAN}${this.ipv4Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
|
|
636
|
+
isValid = true;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (!isValid) {
|
|
641
|
+
this.log.error(`Invalid ipv4address: ${this.ipv4Address}. Using all available addresses.`);
|
|
642
|
+
this.ipv4Address = undefined;
|
|
643
|
+
await this.nodeContext.remove('matteripv4address');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// Set the listeningAddressIpv6 for the matter commissioning server
|
|
647
|
+
if (hasParameter('ipv6address')) {
|
|
648
|
+
this.ipv6Address = getParameter('ipv6address');
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
this.ipv6Address = await this.nodeContext?.get('matteripv6address', undefined);
|
|
652
|
+
if (this.ipv6Address === '')
|
|
653
|
+
this.ipv6Address = undefined;
|
|
654
|
+
}
|
|
655
|
+
// Validate ipv6address
|
|
656
|
+
if (this.ipv6Address) {
|
|
657
|
+
let isValid = false;
|
|
658
|
+
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
659
|
+
if (ifaces && ifaces.find((iface) => (iface.scopeid === undefined || iface.scopeid === 0) && iface.address === this.ipv6Address)) {
|
|
660
|
+
this.log.info(`Using ipv6address ${CYAN}${this.ipv6Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
|
|
661
|
+
isValid = true;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
/* istanbul ignore next */
|
|
665
|
+
if (ifaces &&
|
|
666
|
+
ifaces.find((iface) => iface.scopeid && iface.scopeid > 0 && iface.address + '%' + (process.platform === 'win32' ? iface.scopeid : ifaceName) === this.ipv6Address)) {
|
|
667
|
+
this.log.info(`Using ipv6address ${CYAN}${this.ipv6Address}${nf} on interface ${CYAN}${ifaceName}${nf} for the Matter server node.`);
|
|
668
|
+
isValid = true;
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!isValid) {
|
|
673
|
+
this.log.error(`Invalid ipv6address: ${this.ipv6Address}. Using all available addresses.`);
|
|
674
|
+
this.ipv6Address = undefined;
|
|
675
|
+
await this.nodeContext.remove('matteripv6address');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// Initialize the virtual mode
|
|
679
|
+
if (hasParameter('novirtual')) {
|
|
680
|
+
this.virtualMode = 'disabled';
|
|
681
|
+
await this.nodeContext.set('virtualmode', 'disabled');
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
this.virtualMode = (await this.nodeContext.get('virtualmode', 'outlet'));
|
|
685
|
+
}
|
|
686
|
+
this.log.debug(`Virtual mode ${this.virtualMode}.`);
|
|
687
|
+
// Initialize PluginManager
|
|
688
|
+
this.plugins.logLevel = this.log.logLevel;
|
|
689
|
+
await this.plugins.loadFromStorage();
|
|
690
|
+
// Initialize DeviceManager
|
|
691
|
+
this.devices.logLevel = this.log.logLevel;
|
|
692
|
+
// Get the plugins from node storage and create the plugins node storage contexts
|
|
693
|
+
for (const plugin of this.plugins) {
|
|
694
|
+
// Try to reinstall the plugin from npm (for Docker pull and external plugins)
|
|
695
|
+
// We don't do this when the add and other shutdown parameters are set because we shut down the process after adding the plugin
|
|
696
|
+
if (!fs.existsSync(plugin.path) &&
|
|
697
|
+
!hasParameter('add') &&
|
|
698
|
+
!hasParameter('remove') &&
|
|
699
|
+
!hasParameter('enable') &&
|
|
700
|
+
!hasParameter('disable') &&
|
|
701
|
+
!hasParameter('reset') &&
|
|
702
|
+
!hasParameter('factoryreset')) {
|
|
703
|
+
this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm...`);
|
|
704
|
+
const { spawnCommand } = await import('./spawn.js');
|
|
705
|
+
if (await spawnCommand('npm', ['install', '-g', `${plugin.name}${plugin.version.includes('-dev-') ? '@dev' : ''}`, '--omit=dev', '--verbose'], 'install', plugin.name)) {
|
|
706
|
+
this.log.info(`Plugin ${plg}${plugin.name}${nf} reinstalled.`);
|
|
707
|
+
plugin.error = false;
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
this.log.error(`Error reinstalling plugin ${plg}${plugin.name}${nf}. The plugin is disabled.`);
|
|
711
|
+
plugin.error = true;
|
|
712
|
+
plugin.enabled = false;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if ((await this.plugins.parse(plugin)) === null) {
|
|
717
|
+
this.log.error(`Error parsing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`);
|
|
718
|
+
plugin.error = true;
|
|
719
|
+
plugin.enabled = false;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
|
|
723
|
+
plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
|
|
724
|
+
await plugin.nodeContext.set('name', plugin.name);
|
|
725
|
+
await plugin.nodeContext.set('type', plugin.type);
|
|
726
|
+
await plugin.nodeContext.set('path', plugin.path);
|
|
727
|
+
await plugin.nodeContext.set('version', plugin.version);
|
|
728
|
+
await plugin.nodeContext.set('description', plugin.description);
|
|
729
|
+
await plugin.nodeContext.set('author', plugin.author);
|
|
730
|
+
}
|
|
731
|
+
// Log system info and create .matterbridge directory
|
|
732
|
+
await this.logNodeAndSystemInfo();
|
|
733
|
+
this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
|
|
734
|
+
`${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
|
|
735
|
+
`${hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge') ? 'mode childbridge ' : ''}` +
|
|
736
|
+
`${hasParameter('controller') ? 'mode controller ' : ''}` +
|
|
737
|
+
`${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
|
|
738
|
+
`running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
|
|
739
|
+
// Check node version and throw error
|
|
740
|
+
const minNodeVersion = 20;
|
|
741
|
+
const nodeVersion = process.versions.node;
|
|
742
|
+
const versionMajor = parseInt(nodeVersion.split('.')[0]);
|
|
743
|
+
if (versionMajor < minNodeVersion) {
|
|
744
|
+
this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
745
|
+
throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
746
|
+
}
|
|
747
|
+
// Parse command line
|
|
748
|
+
await this.parseCommandLine();
|
|
749
|
+
// Emit the initialize_completed event
|
|
750
|
+
this.emit('initialize_completed');
|
|
751
|
+
this.initialized = true;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Resolve a file path located in the `@matterbridge/core` distribution directory.
|
|
755
|
+
*
|
|
756
|
+
* @remarks
|
|
757
|
+
* Matterbridge spawns ESM workers from built JavaScript files (e.g. `workerCheckUpdates.js`).
|
|
758
|
+
* Depending on how the code is executed:
|
|
759
|
+
* - **Production**: `import.meta.url` points inside `.../node_modules/@matterbridge/core/dist/...`
|
|
760
|
+
* and the worker file is usually alongside the current module.
|
|
761
|
+
* - **Development / tests**: `import.meta.url` may point inside `.../packages/core/src/...`
|
|
762
|
+
* while the worker file exists in `.../packages/core/dist/...`.
|
|
763
|
+
*
|
|
764
|
+
* This helper tries both locations and returns the first existing candidate.
|
|
765
|
+
*
|
|
766
|
+
* @param {string} fileName - Worker/build artifact file name, e.g. `workerGlobalPrefix.js`.
|
|
767
|
+
* @returns {string} Absolute path to the resolved file. If none exists, returns the first candidate (best effort).
|
|
768
|
+
*/
|
|
769
|
+
resolveWorkerDistFilePath(fileName) {
|
|
770
|
+
const currentModuleDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
771
|
+
// This core package's src or dist directory or the global installation dist directory for thread package
|
|
772
|
+
const candidates = [
|
|
773
|
+
path.join(currentModuleDirectory, fileName), // Current src directory for jest tests
|
|
774
|
+
path.join(currentModuleDirectory, '..', 'dist', fileName), // Dist directory for local development with local packages
|
|
775
|
+
path.join(this.rootDirectory, 'node_modules', '@matterbridge', 'thread', 'dist', fileName), // Global installation dist directory for production with thread package
|
|
776
|
+
];
|
|
777
|
+
for (const candidate of candidates) {
|
|
778
|
+
if (fs.existsSync(candidate))
|
|
779
|
+
return candidate;
|
|
780
|
+
}
|
|
781
|
+
return candidates[0];
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Parses the command line arguments and performs the corresponding actions.
|
|
785
|
+
*
|
|
786
|
+
* @private
|
|
787
|
+
* @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
|
|
788
|
+
*/
|
|
789
|
+
async parseCommandLine() {
|
|
790
|
+
if (hasParameter('list')) {
|
|
791
|
+
this.log.info(`│ Registered plugins (${this.plugins.length})`);
|
|
792
|
+
let index = 0;
|
|
793
|
+
for (const plugin of this.plugins) {
|
|
794
|
+
if (index !== this.plugins.length - 1) {
|
|
795
|
+
this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} version: ${plg}${plugin.version}${nf} ${plugin.enabled ? GREEN : RED}enabled${nf}`);
|
|
796
|
+
this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} version: ${plg}${plugin.version}${nf} ${plugin.enabled ? GREEN : RED}disabled${nf}`);
|
|
800
|
+
this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
|
|
801
|
+
}
|
|
802
|
+
index++;
|
|
803
|
+
}
|
|
804
|
+
/*
|
|
805
|
+
const serializedRegisteredDevices = await this.nodeContext?.get<SerializedMatterbridgeEndpoint[]>('devices', []);
|
|
806
|
+
this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`);
|
|
807
|
+
serializedRegisteredDevices?.forEach((device, index) => {
|
|
808
|
+
if (index !== serializedRegisteredDevices.length - 1) {
|
|
809
|
+
this.log.info(`├─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
|
|
810
|
+
this.log.info(`│ └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
|
|
811
|
+
} else {
|
|
812
|
+
this.log.info(`└─┬─ plugin ${plg}${device.pluginName}${nf} device: ${dev}${device.deviceName}${nf} uniqueId: ${YELLOW}${device.uniqueId}${nf}`);
|
|
813
|
+
this.log.info(` └─ endpoint ${RED}${device.endpoint}${nf} ${typ}${device.endpointName}${nf} ${debugStringify(device.clusterServersId)}`);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
*/
|
|
817
|
+
this.shutdown = true;
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (hasParameter('logstorage')) {
|
|
821
|
+
this.log.info(`${plg}Matterbridge${nf} storage log`);
|
|
822
|
+
await this.nodeContext?.logStorage();
|
|
823
|
+
for (const plugin of this.plugins) {
|
|
824
|
+
this.log.info(`${plg}${plugin.name}${nf} storage log`);
|
|
825
|
+
await plugin.nodeContext?.logStorage();
|
|
826
|
+
}
|
|
827
|
+
this.shutdown = true;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (hasParameter('loginterfaces')) {
|
|
831
|
+
const { logInterfaces } = await import('@matterbridge/utils');
|
|
832
|
+
logInterfaces();
|
|
833
|
+
this.shutdown = true;
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (getParameter('add')) {
|
|
837
|
+
this.log.debug(`Adding plugin ${getParameter('add')}`);
|
|
838
|
+
await this.plugins.add(getParameter('add'));
|
|
839
|
+
this.shutdown = true;
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (getParameter('remove')) {
|
|
843
|
+
this.log.debug(`Removing plugin ${getParameter('remove')}`);
|
|
844
|
+
await this.plugins.remove(getParameter('remove'));
|
|
845
|
+
this.shutdown = true;
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (getParameter('enable')) {
|
|
849
|
+
this.log.debug(`Enabling plugin ${getParameter('enable')}`);
|
|
850
|
+
await this.plugins.enable(getParameter('enable'));
|
|
851
|
+
this.shutdown = true;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (getParameter('disable')) {
|
|
855
|
+
this.log.debug(`Disabling plugin ${getParameter('disable')}`);
|
|
856
|
+
await this.plugins.disable(getParameter('disable'));
|
|
857
|
+
this.shutdown = true;
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (hasParameter('factoryreset')) {
|
|
861
|
+
this.initialized = true;
|
|
862
|
+
await this.shutdownProcessAndFactoryReset();
|
|
863
|
+
this.shutdown = true;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
// Initialize frontend
|
|
867
|
+
if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
|
|
868
|
+
await this.frontend.start(getIntParameter('frontend'));
|
|
869
|
+
// Start the matter storage and create the matterbridge context
|
|
870
|
+
try {
|
|
871
|
+
await this.startMatterStorage();
|
|
872
|
+
if (this.aggregatorSerialNumber && this.aggregatorUniqueId && this.matterStorageService) {
|
|
873
|
+
const storageManager = await this.matterStorageService.open('Matterbridge');
|
|
874
|
+
const storageContext = storageManager?.createContext('persist');
|
|
875
|
+
await storageContext?.set('serialNumber', this.aggregatorSerialNumber);
|
|
876
|
+
await storageContext?.set('uniqueId', this.aggregatorUniqueId);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
881
|
+
throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
882
|
+
}
|
|
883
|
+
// Clear the matterbridge context if the reset parameter is set (bridge mode)
|
|
884
|
+
if (hasParameter('reset') && getParameter('reset') === undefined) {
|
|
885
|
+
this.initialized = true;
|
|
886
|
+
await this.shutdownProcessAndReset();
|
|
887
|
+
this.shutdown = true;
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
// Clear matterbridge plugin context if the reset parameter is set (childbridge mode)
|
|
891
|
+
if (hasParameter('reset') && getParameter('reset') !== undefined) {
|
|
892
|
+
this.log.debug(`Reset plugin ${getParameter('reset')}`);
|
|
893
|
+
const plugin = this.plugins.get(getParameter('reset'));
|
|
894
|
+
if (plugin) {
|
|
895
|
+
const matterStorageManager = await this.matterStorageService?.open(plugin.name);
|
|
896
|
+
if (!matterStorageManager) {
|
|
897
|
+
/* istanbul ignore next */
|
|
898
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
await matterStorageManager.createContext('events')?.clearAll();
|
|
902
|
+
await matterStorageManager.createContext('fabrics')?.clearAll();
|
|
903
|
+
await matterStorageManager.createContext('root')?.clearAll();
|
|
904
|
+
await matterStorageManager.createContext('sessions')?.clearAll();
|
|
905
|
+
await matterStorageManager.createContext('persist')?.clearAll();
|
|
906
|
+
this.log.notice(`Reset commissioning for plugin ${plg}${plugin.name}${nt} done! Remove the device from the controller.`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`);
|
|
911
|
+
}
|
|
912
|
+
await this.stopMatterStorage();
|
|
913
|
+
this.shutdown = true;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
// Check in 5 minutes the latest and dev versions of matterbridge and the plugins
|
|
917
|
+
clearTimeout(this.checkUpdateTimeout);
|
|
918
|
+
this.checkUpdateTimeout = setTimeout(async () => {
|
|
919
|
+
// const { checkUpdates } = await import('./checkUpdates.js');
|
|
920
|
+
// checkUpdates(this);
|
|
921
|
+
const { createESMWorker } = await import('@matterbridge/thread');
|
|
922
|
+
createESMWorker('CheckUpdates', this.resolveWorkerDistFilePath('workerCheckUpdates.js'));
|
|
923
|
+
}, 300 * 1000).unref();
|
|
924
|
+
// Check each 12 hours the latest and dev versions of matterbridge and the plugins
|
|
925
|
+
clearInterval(this.checkUpdateInterval);
|
|
926
|
+
this.checkUpdateInterval = setInterval(async () => {
|
|
927
|
+
// const { checkUpdates } = await import('./checkUpdates.js');
|
|
928
|
+
// checkUpdates(this);
|
|
929
|
+
const { createESMWorker } = await import('@matterbridge/thread');
|
|
930
|
+
createESMWorker('CheckUpdates', this.resolveWorkerDistFilePath('workerCheckUpdates.js'));
|
|
931
|
+
}, 12 * 60 * 60 * 1000).unref();
|
|
932
|
+
// Start the matterbridge in mode test
|
|
933
|
+
if (hasParameter('test')) {
|
|
934
|
+
this.bridgeMode = 'bridge';
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
// Start the matterbridge in mode controller
|
|
938
|
+
if (hasParameter('controller')) {
|
|
939
|
+
this.bridgeMode = 'controller';
|
|
940
|
+
await this.startController();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
// Check if the bridge mode is set and start matterbridge in bridge mode if not set
|
|
944
|
+
if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
|
|
945
|
+
this.log.info('Setting default matterbridge start mode to bridge');
|
|
946
|
+
await this.nodeContext?.set('bridgeMode', 'bridge');
|
|
947
|
+
}
|
|
948
|
+
// Wait delay if specified (default 2 minutes) and the system uptime is less than 5 minutes. It solves race conditions on system startup.
|
|
949
|
+
if (hasParameter('delay') && os.uptime() <= 60 * 5) {
|
|
950
|
+
const { wait } = await import('@matterbridge/utils');
|
|
951
|
+
const delay = getIntParameter('delay') || 120;
|
|
952
|
+
this.log.warn('Delay switch found with system uptime less then 5 minutes. Waiting for ' + delay + ' seconds before starting matterbridge...');
|
|
953
|
+
await wait(delay * 1000, 'Race condition delay', true);
|
|
954
|
+
}
|
|
955
|
+
// Wait delay if specified (default 2 minutes). It solves race conditions on docker compose startup.
|
|
956
|
+
if (hasParameter('fixed_delay')) {
|
|
957
|
+
const { wait } = await import('@matterbridge/utils');
|
|
958
|
+
const delay = getIntParameter('fixed_delay') || 120;
|
|
959
|
+
this.log.warn('Fixed delay switch found. Waiting for ' + delay + ' seconds before starting matterbridge...');
|
|
960
|
+
await wait(delay * 1000, 'Fixed race condition delay', true);
|
|
961
|
+
}
|
|
962
|
+
// Start matterbridge in bridge mode
|
|
963
|
+
if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
|
|
964
|
+
this.bridgeMode = 'bridge';
|
|
965
|
+
this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
|
|
966
|
+
await this.startBridge();
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// Start matterbridge in childbridge mode
|
|
970
|
+
if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
|
|
971
|
+
this.bridgeMode = 'childbridge';
|
|
972
|
+
this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
|
|
973
|
+
await this.startChildbridge();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Asynchronously loads and starts the registered plugins.
|
|
979
|
+
*
|
|
980
|
+
* This method is responsible for initializing and starting all enabled plugins.
|
|
981
|
+
* It ensures that each plugin is properly loaded and started before the bridge starts.
|
|
982
|
+
*
|
|
983
|
+
* @param {boolean} [wait] - If true, the method will wait for all plugins to be fully loaded and started before resolving. Defaults to false.
|
|
984
|
+
* @param {boolean} [start] - If true, the method will start the plugins after loading them. Defaults to true.
|
|
985
|
+
* @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
|
|
986
|
+
*/
|
|
987
|
+
async startPlugins(wait = false, start = true) {
|
|
988
|
+
// Check, load and start the plugins
|
|
989
|
+
for (const plugin of this.plugins) {
|
|
990
|
+
plugin.configJson = await this.plugins.loadConfig(plugin);
|
|
991
|
+
plugin.schemaJson = await this.plugins.loadSchema(plugin);
|
|
992
|
+
// Check if the plugin is available
|
|
993
|
+
if (!(await this.plugins.resolve(plugin.path))) {
|
|
994
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
|
|
995
|
+
plugin.enabled = false;
|
|
996
|
+
plugin.error = true;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (!plugin.enabled) {
|
|
1000
|
+
this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
plugin.error = false;
|
|
1004
|
+
plugin.locked = false;
|
|
1005
|
+
plugin.loaded = false;
|
|
1006
|
+
plugin.started = false;
|
|
1007
|
+
plugin.configured = false;
|
|
1008
|
+
plugin.registeredDevices = undefined;
|
|
1009
|
+
if (wait)
|
|
1010
|
+
await this.plugins.load(plugin, start, 'Matterbridge is starting');
|
|
1011
|
+
else
|
|
1012
|
+
this.plugins.load(plugin, start, 'Matterbridge is starting'); // No await do it asyncronously
|
|
1013
|
+
}
|
|
1014
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
|
|
1018
|
+
* - When an uncaught exception occurs, the exceptionHandler logs the error message and stack trace.
|
|
1019
|
+
* - When an unhandled promise rejection occurs, the rejectionHandler logs the reason and stack trace.
|
|
1020
|
+
* - When either of SIGINT and SIGTERM signals are received, the cleanup method is called with an appropriate message.
|
|
1021
|
+
*/
|
|
1022
|
+
registerProcessHandlers() {
|
|
1023
|
+
this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
|
|
1024
|
+
process.removeAllListeners('uncaughtException');
|
|
1025
|
+
process.removeAllListeners('unhandledRejection');
|
|
1026
|
+
this.exceptionHandler = async (error) => {
|
|
1027
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
1028
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
1029
|
+
this.log.error(`Unhandled Exception detected: ${errorMessage}\nstack: ${errorInspect}}`);
|
|
1030
|
+
};
|
|
1031
|
+
process.on('uncaughtException', this.exceptionHandler);
|
|
1032
|
+
this.rejectionHandler = async (reason, promise) => {
|
|
1033
|
+
const errorMessage = reason instanceof Error ? reason.message : reason;
|
|
1034
|
+
const errorInspect = inspect(reason, { depth: 10 });
|
|
1035
|
+
this.log.error(`Unhandled Rejection detected: ${promise}\nreason: ${errorMessage}\nstack: ${errorInspect}`);
|
|
1036
|
+
};
|
|
1037
|
+
process.on('unhandledRejection', this.rejectionHandler);
|
|
1038
|
+
this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
|
|
1039
|
+
this.sigintHandler = async () => {
|
|
1040
|
+
await this.cleanup('SIGINT received, cleaning up...');
|
|
1041
|
+
};
|
|
1042
|
+
process.on('SIGINT', this.sigintHandler);
|
|
1043
|
+
this.sigtermHandler = async () => {
|
|
1044
|
+
await this.cleanup('SIGTERM received, cleaning up...');
|
|
1045
|
+
};
|
|
1046
|
+
process.on('SIGTERM', this.sigtermHandler);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
|
|
1050
|
+
*/
|
|
1051
|
+
deregisterProcessHandlers() {
|
|
1052
|
+
this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
|
|
1053
|
+
if (this.exceptionHandler)
|
|
1054
|
+
process.off('uncaughtException', this.exceptionHandler);
|
|
1055
|
+
this.exceptionHandler = undefined;
|
|
1056
|
+
if (this.rejectionHandler)
|
|
1057
|
+
process.off('unhandledRejection', this.rejectionHandler);
|
|
1058
|
+
this.rejectionHandler = undefined;
|
|
1059
|
+
this.log.debug(`Deregistering SIGINT and SIGTERM signal handlers...`);
|
|
1060
|
+
if (this.sigintHandler)
|
|
1061
|
+
process.off('SIGINT', this.sigintHandler);
|
|
1062
|
+
this.sigintHandler = undefined;
|
|
1063
|
+
if (this.sigtermHandler)
|
|
1064
|
+
process.off('SIGTERM', this.sigtermHandler);
|
|
1065
|
+
this.sigtermHandler = undefined;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Logs the node and system information.
|
|
1069
|
+
*
|
|
1070
|
+
* @remarks
|
|
1071
|
+
* This method retrieves and logs various details about the host system, including:
|
|
1072
|
+
* - IP address information (IPv4, IPv6, MAC address)
|
|
1073
|
+
* - Node.js version
|
|
1074
|
+
* - Hostname and user information
|
|
1075
|
+
* - Operating system details (type, release, platform, architecture)
|
|
1076
|
+
* - Memory usage statistics
|
|
1077
|
+
* - Uptime information for both the system and the process
|
|
1078
|
+
*/
|
|
1079
|
+
async logNodeAndSystemInfo() {
|
|
1080
|
+
// IP address information
|
|
1081
|
+
const excludedInterfaceNamePattern = /(tailscale|wireguard|openvpn|zerotier|hamachi|\bwg\d+\b|\btun\d+\b|\btap\d+\b|\butun\d+\b|docker|podman|\bveth[a-z0-9]*\b|\bbr-[a-z0-9]+\b|cni|kube|flannel|calico|virbr\d*\b|vmware|vmnet\d*\b|virtualbox|vboxnet\d*\b|teredo|isatap)/i;
|
|
1082
|
+
const networkInterfaces = os.networkInterfaces();
|
|
1083
|
+
this.systemInformation.interfaceName = '';
|
|
1084
|
+
this.systemInformation.ipv4Address = '';
|
|
1085
|
+
this.systemInformation.ipv6Address = '';
|
|
1086
|
+
this.systemInformation.macAddress = '';
|
|
1087
|
+
for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
|
|
1088
|
+
if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
|
|
1089
|
+
continue;
|
|
1090
|
+
if (!this.mdnsInterface && excludedInterfaceNamePattern.test(interfaceName))
|
|
1091
|
+
continue;
|
|
1092
|
+
if (!interfaceDetails)
|
|
1093
|
+
continue;
|
|
1094
|
+
for (const detail of interfaceDetails) {
|
|
1095
|
+
if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
|
|
1096
|
+
this.systemInformation.interfaceName = interfaceName;
|
|
1097
|
+
this.systemInformation.ipv4Address = detail.address;
|
|
1098
|
+
this.systemInformation.macAddress = detail.mac;
|
|
1099
|
+
}
|
|
1100
|
+
else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
|
|
1101
|
+
this.systemInformation.interfaceName = interfaceName;
|
|
1102
|
+
this.systemInformation.ipv6Address = detail.address;
|
|
1103
|
+
this.systemInformation.macAddress = detail.mac;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
if (this.systemInformation.ipv4Address !== '' || this.systemInformation.ipv6Address !== '') {
|
|
1107
|
+
this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
|
|
1108
|
+
this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
|
|
1109
|
+
this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
|
|
1110
|
+
this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// Node information
|
|
1115
|
+
this.systemInformation.nodeVersion = process.versions.node;
|
|
1116
|
+
const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
|
|
1117
|
+
const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
|
|
1118
|
+
const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
|
|
1119
|
+
// Host system information
|
|
1120
|
+
this.systemInformation.hostname = os.hostname();
|
|
1121
|
+
this.systemInformation.user = os.userInfo().username;
|
|
1122
|
+
this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
|
|
1123
|
+
this.systemInformation.osRelease = os.release(); // Kernel version
|
|
1124
|
+
this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
|
|
1125
|
+
this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
|
|
1126
|
+
this.systemInformation.totalMemory = formatBytes(os.totalmem());
|
|
1127
|
+
this.systemInformation.freeMemory = formatBytes(os.freemem());
|
|
1128
|
+
this.systemInformation.systemUptime = formatUptime(os.uptime());
|
|
1129
|
+
this.systemInformation.processUptime = formatUptime(process.uptime());
|
|
1130
|
+
this.systemInformation.cpuUsage = formatPercent(0);
|
|
1131
|
+
this.systemInformation.processCpuUsage = formatPercent(0);
|
|
1132
|
+
this.systemInformation.rss = formatBytes(process.memoryUsage().rss);
|
|
1133
|
+
this.systemInformation.heapTotal = formatBytes(process.memoryUsage().heapTotal);
|
|
1134
|
+
this.systemInformation.heapUsed = formatBytes(process.memoryUsage().heapUsed);
|
|
1135
|
+
// Log the system information
|
|
1136
|
+
this.log.debug('Host System Information:');
|
|
1137
|
+
this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
|
|
1138
|
+
this.log.debug(`- User: ${this.systemInformation.user}`);
|
|
1139
|
+
this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
|
|
1140
|
+
this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
|
|
1141
|
+
this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
|
|
1142
|
+
this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
|
|
1143
|
+
this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
|
|
1144
|
+
this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
|
|
1145
|
+
this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
|
|
1146
|
+
this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
|
|
1147
|
+
this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
|
|
1148
|
+
this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
|
|
1149
|
+
this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
|
|
1150
|
+
this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
|
|
1151
|
+
this.log.debug(`- Process Uptime: ${this.systemInformation.processUptime}`);
|
|
1152
|
+
this.log.debug(`- RSS: ${this.systemInformation.rss}`);
|
|
1153
|
+
this.log.debug(`- Heap Total: ${this.systemInformation.heapTotal}`);
|
|
1154
|
+
this.log.debug(`- Heap Used: ${this.systemInformation.heapUsed}`);
|
|
1155
|
+
// Log directories
|
|
1156
|
+
this.log.debug(`Root Directory: ${this.rootDirectory}`);
|
|
1157
|
+
this.log.debug(`Home Directory: ${this.homeDirectory}`);
|
|
1158
|
+
this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
1159
|
+
this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
1160
|
+
this.log.debug(`Matterbridge Matter Certificate Directory: ${this.matterbridgeCertDirectory}`);
|
|
1161
|
+
// Global node_modules directory
|
|
1162
|
+
if (this.nodeContext)
|
|
1163
|
+
this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
1164
|
+
if (this.globalModulesDirectory === '') {
|
|
1165
|
+
// First run of Matterbridge so the node storage is empty
|
|
1166
|
+
this.log.debug(`Getting global node_modules directory...`);
|
|
1167
|
+
try {
|
|
1168
|
+
const { getGlobalNodeModules } = await import('@matterbridge/utils');
|
|
1169
|
+
this.globalModulesDirectory = await getGlobalNodeModules();
|
|
1170
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
1171
|
+
await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
// The global node_modules directory is already set in the node storage and we check if it is still valid
|
|
1179
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
1180
|
+
const { createESMWorker } = await import('@matterbridge/thread');
|
|
1181
|
+
createESMWorker('NpmGlobalPrefix', this.resolveWorkerDistFilePath('workerGlobalPrefix.js'));
|
|
1182
|
+
}
|
|
1183
|
+
// Matterbridge version
|
|
1184
|
+
this.log.debug(`Reading matterbridge package.json...`);
|
|
1185
|
+
const packageJson = JSON.parse(await fs.promises.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
|
|
1186
|
+
this.matterbridgeVersion = this.matterbridgeLatestVersion = this.matterbridgeDevVersion = packageJson.version;
|
|
1187
|
+
this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
|
|
1188
|
+
// Matterbridge latest version (will be set in the checkUpdate function)
|
|
1189
|
+
if (this.nodeContext)
|
|
1190
|
+
this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
|
|
1191
|
+
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
1192
|
+
// Matterbridge dev version (will be set in the checkUpdate function)
|
|
1193
|
+
if (this.nodeContext)
|
|
1194
|
+
this.matterbridgeDevVersion = await this.nodeContext.get('matterbridgeDevVersion', this.matterbridgeVersion);
|
|
1195
|
+
this.log.debug(`Matterbridge Dev Version: ${this.matterbridgeDevVersion}`);
|
|
1196
|
+
// Frontend version
|
|
1197
|
+
this.log.debug(`Reading frontend package.json...`);
|
|
1198
|
+
const frontendPackageJson = JSON.parse(await fs.promises.readFile(path.join(this.rootDirectory, 'apps', 'frontend', 'package.json'), 'utf8'));
|
|
1199
|
+
this.frontendVersion = frontendPackageJson.version;
|
|
1200
|
+
this.log.debug(`Frontend version ${CYAN}${this.frontendVersion}${db}`);
|
|
1201
|
+
// Current working directory
|
|
1202
|
+
const currentDir = process.cwd();
|
|
1203
|
+
this.log.debug(`Current Working Directory: ${currentDir}`);
|
|
1204
|
+
// Command line arguments (excluding 'node' and the script name)
|
|
1205
|
+
const cmdArgs = process.argv.slice(2).join(' ');
|
|
1206
|
+
this.log.debug(`Command Line Arguments: ${cmdArgs}`);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Set the logger logLevel for the Matterbridge classes and call onChangeLoggerLevel() for each plugin.
|
|
1210
|
+
*
|
|
1211
|
+
* @param {LogLevel} logLevel The logger logLevel to set.
|
|
1212
|
+
* @returns {Promise<LogLevel>} A promise that resolves when the logLevel has been set.
|
|
1213
|
+
*/
|
|
1214
|
+
async setLogLevel(logLevel) {
|
|
1215
|
+
this.logLevel = logLevel;
|
|
1216
|
+
this.log.logLevel = logLevel;
|
|
1217
|
+
this.frontend.logLevel = logLevel;
|
|
1218
|
+
MatterbridgeEndpoint.logLevel = logLevel;
|
|
1219
|
+
this.devices.logLevel = logLevel;
|
|
1220
|
+
this.plugins.logLevel = logLevel;
|
|
1221
|
+
let pluginDebug = false;
|
|
1222
|
+
for (const plugin of this.plugins) {
|
|
1223
|
+
if (!plugin.platform || !plugin.platform.log || !plugin.platform.config)
|
|
1224
|
+
continue;
|
|
1225
|
+
if (plugin.platform.config.debug === true)
|
|
1226
|
+
pluginDebug = true;
|
|
1227
|
+
plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : logLevel;
|
|
1228
|
+
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : logLevel);
|
|
1229
|
+
}
|
|
1230
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
1231
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
1232
|
+
if (logLevel === "info" /* LogLevel.INFO */ || Logger.level === MatterLogLevel.INFO)
|
|
1233
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
1234
|
+
if (logLevel === "debug" /* LogLevel.DEBUG */ || Logger.level === MatterLogLevel.DEBUG || pluginDebug)
|
|
1235
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
1236
|
+
AnsiLogger.setGlobalCallbackLevel(callbackLogLevel);
|
|
1237
|
+
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
1238
|
+
return logLevel;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Get the current logger logLevel.
|
|
1242
|
+
*
|
|
1243
|
+
* @returns {LogLevel} The current logger logLevel.
|
|
1244
|
+
*/
|
|
1245
|
+
getLogLevel() {
|
|
1246
|
+
return this.log.logLevel;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
|
|
1250
|
+
* It also logs to file (matter.log) if fileLogger is true.
|
|
1251
|
+
*
|
|
1252
|
+
* @param {boolean} fileLogger - Whether to log to file or not.
|
|
1253
|
+
* @returns {(text: string, message: Diagnostic.Message) => void} The MatterLogger function. \x1b[35m for violet \x1b[34m is blue
|
|
1254
|
+
*/
|
|
1255
|
+
createDestinationMatterLogger(fileLogger) {
|
|
1256
|
+
if (fileLogger) {
|
|
1257
|
+
this.matterLog.logFilePath = path.join(this.matterbridgeDirectory, MATTER_LOGGER_FILE);
|
|
1258
|
+
}
|
|
1259
|
+
return (text, message) => {
|
|
1260
|
+
// 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
|
|
1261
|
+
const logger = text.slice(44, 44 + 20).trim();
|
|
1262
|
+
const msg = text.slice(65);
|
|
1263
|
+
this.matterLog.logName = logger;
|
|
1264
|
+
switch (message.level) {
|
|
1265
|
+
case MatterLogLevel.DEBUG:
|
|
1266
|
+
this.matterLog.log("debug" /* LogLevel.DEBUG */, msg);
|
|
1267
|
+
break;
|
|
1268
|
+
case MatterLogLevel.INFO:
|
|
1269
|
+
this.matterLog.log("info" /* LogLevel.INFO */, msg);
|
|
1270
|
+
break;
|
|
1271
|
+
case MatterLogLevel.NOTICE:
|
|
1272
|
+
this.matterLog.log("notice" /* LogLevel.NOTICE */, msg);
|
|
1273
|
+
break;
|
|
1274
|
+
case MatterLogLevel.WARN:
|
|
1275
|
+
this.matterLog.log("warn" /* LogLevel.WARN */, msg);
|
|
1276
|
+
break;
|
|
1277
|
+
case MatterLogLevel.ERROR:
|
|
1278
|
+
this.matterLog.log("error" /* LogLevel.ERROR */, msg);
|
|
1279
|
+
break;
|
|
1280
|
+
case MatterLogLevel.FATAL:
|
|
1281
|
+
this.matterLog.log("fatal" /* LogLevel.FATAL */, msg);
|
|
1282
|
+
break;
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Restarts the process by exiting the current instance and loading a new instance (/api/restart).
|
|
1288
|
+
*
|
|
1289
|
+
* @returns {Promise<void>} A promise that resolves when the restart is completed.
|
|
1290
|
+
*/
|
|
1291
|
+
async restartProcess() {
|
|
1292
|
+
await this.cleanup('restarting...', true);
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Shut down the process (/api/shutdown).
|
|
1296
|
+
*
|
|
1297
|
+
* @returns {Promise<void>} A promise that resolves when the shutdown is completed.
|
|
1298
|
+
*/
|
|
1299
|
+
async shutdownProcess() {
|
|
1300
|
+
await this.cleanup('shutting down...', false);
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Update matterbridge and shut down the process (virtual device 'Update Matterbridge').
|
|
1304
|
+
*
|
|
1305
|
+
* @returns {Promise<void>} A promise that resolves when the update is completed.
|
|
1306
|
+
*/
|
|
1307
|
+
async updateProcess() {
|
|
1308
|
+
this.log.info('Updating matterbridge...');
|
|
1309
|
+
const { spawnCommand } = await import('./spawn.js');
|
|
1310
|
+
if (await spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose'], 'install', 'matterbridge')) {
|
|
1311
|
+
this.log.info('Matterbridge has been updated. Full restart required.');
|
|
1312
|
+
}
|
|
1313
|
+
else {
|
|
1314
|
+
this.log.error('Error updating matterbridge.');
|
|
1315
|
+
}
|
|
1316
|
+
this.frontend.wssSendRestartRequired();
|
|
1317
|
+
await this.cleanup('updating...', false);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Unregister all devices and shut down the process (/api/unregister).
|
|
1321
|
+
*
|
|
1322
|
+
* @param {number} [timeout] - The timeout duration to wait for the message exchange to complete in milliseconds. Default is 1000.
|
|
1323
|
+
*
|
|
1324
|
+
* @returns {Promise<void>} A promise that resolves when the cleanup is completed.
|
|
1325
|
+
*/
|
|
1326
|
+
async unregisterAndShutdownProcess(timeout = 1000) {
|
|
1327
|
+
const { wait } = await import('@matterbridge/utils');
|
|
1328
|
+
this.log.info('Unregistering all devices and shutting down...');
|
|
1329
|
+
for (const plugin of this.plugins.array()) {
|
|
1330
|
+
if (plugin.error || !plugin.enabled)
|
|
1331
|
+
continue;
|
|
1332
|
+
const registeredDevices = plugin.registeredDevices;
|
|
1333
|
+
await this.plugins.shutdown(plugin, 'unregistering all devices and shutting down...', false, true);
|
|
1334
|
+
plugin.registeredDevices = registeredDevices;
|
|
1335
|
+
await this.removeAllBridgedEndpoints(plugin.name, 100);
|
|
1336
|
+
}
|
|
1337
|
+
this.log.debug('Waiting for the MessageExchange to finish...');
|
|
1338
|
+
await wait(timeout); // Wait for MessageExchange to finish
|
|
1339
|
+
this.log.debug('Cleaning up and shutting down...');
|
|
1340
|
+
await this.cleanup('unregistered all devices and shutting down...', false, timeout);
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Reset commissioning and shut down the process (/api/reset).
|
|
1344
|
+
*
|
|
1345
|
+
* @returns {Promise<void>} A promise that resolves when the cleanup is completed.
|
|
1346
|
+
*/
|
|
1347
|
+
async shutdownProcessAndReset() {
|
|
1348
|
+
await this.cleanup('shutting down with reset...', false);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Factory reset and shut down the process (/api/factory-reset).
|
|
1352
|
+
*
|
|
1353
|
+
* @returns {Promise<void>} A promise that resolves when the cleanup is completed.
|
|
1354
|
+
*/
|
|
1355
|
+
async shutdownProcessAndFactoryReset() {
|
|
1356
|
+
await this.cleanup('shutting down with factory reset...', false);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Cleans up the Matterbridge instance.
|
|
1360
|
+
*
|
|
1361
|
+
* @param {string} message - The cleanup message.
|
|
1362
|
+
* @param {boolean} [restart] - Indicates whether to restart the instance after cleanup. Default is `false`.
|
|
1363
|
+
* @param {number} [pause] - The pause in ms to wait for the message exchange to complete in milliseconds. Default is 1000.
|
|
1364
|
+
*
|
|
1365
|
+
* @returns {Promise<void>} A promise that resolves when the cleanup is completed.
|
|
1366
|
+
*/
|
|
1367
|
+
async cleanup(message, restart = false, pause = 1000) {
|
|
1368
|
+
if (this.initialized && !this.hasCleanupStarted) {
|
|
1369
|
+
this.emit('cleanup_started');
|
|
1370
|
+
this.hasCleanupStarted = true;
|
|
1371
|
+
this.log.info(message);
|
|
1372
|
+
// Clear the start matter interval
|
|
1373
|
+
if (this.startMatterInterval) {
|
|
1374
|
+
clearInterval(this.startMatterInterval);
|
|
1375
|
+
this.startMatterInterval = undefined;
|
|
1376
|
+
this.log.debug('Start matter interval cleared');
|
|
1377
|
+
}
|
|
1378
|
+
// Clear the check update timeout
|
|
1379
|
+
if (this.checkUpdateTimeout) {
|
|
1380
|
+
clearTimeout(this.checkUpdateTimeout);
|
|
1381
|
+
this.checkUpdateTimeout = undefined;
|
|
1382
|
+
this.log.debug('Check update timeout cleared');
|
|
1383
|
+
}
|
|
1384
|
+
// Clear the check update interval
|
|
1385
|
+
if (this.checkUpdateInterval) {
|
|
1386
|
+
clearInterval(this.checkUpdateInterval);
|
|
1387
|
+
this.checkUpdateInterval = undefined;
|
|
1388
|
+
this.log.debug('Check update interval cleared');
|
|
1389
|
+
}
|
|
1390
|
+
// Clear the configure timeout
|
|
1391
|
+
if (this.configureTimeout) {
|
|
1392
|
+
clearTimeout(this.configureTimeout);
|
|
1393
|
+
this.configureTimeout = undefined;
|
|
1394
|
+
this.log.debug('Matterbridge configure timeout cleared');
|
|
1395
|
+
}
|
|
1396
|
+
// Clear the reachability timeout
|
|
1397
|
+
if (this.reachabilityTimeout) {
|
|
1398
|
+
clearTimeout(this.reachabilityTimeout);
|
|
1399
|
+
this.reachabilityTimeout = undefined;
|
|
1400
|
+
this.log.debug('Matterbridge reachability timeout cleared');
|
|
1401
|
+
}
|
|
1402
|
+
// Call the shutdown method of each plugin and clear the plugins reachability timeout
|
|
1403
|
+
for (const plugin of this.plugins) {
|
|
1404
|
+
if (!plugin.enabled || plugin.error)
|
|
1405
|
+
continue;
|
|
1406
|
+
await this.plugins.shutdown(plugin, 'Matterbridge is closing: ' + message, false);
|
|
1407
|
+
if (plugin.reachabilityTimeout) {
|
|
1408
|
+
clearTimeout(plugin.reachabilityTimeout);
|
|
1409
|
+
plugin.reachabilityTimeout = undefined;
|
|
1410
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
// Stop matter server nodes
|
|
1414
|
+
this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
|
|
1415
|
+
if (pause > 0) {
|
|
1416
|
+
const { wait } = await import('@matterbridge/utils');
|
|
1417
|
+
this.log.debug(`Waiting ${pause}ms for the MessageExchange to finish...`);
|
|
1418
|
+
await wait(pause, `Waiting ${pause}ms for the MessageExchange to finish...`, false);
|
|
1419
|
+
}
|
|
1420
|
+
if (this.bridgeMode === 'bridge') {
|
|
1421
|
+
if (this.serverNode) {
|
|
1422
|
+
await this.stopServerNode(this.serverNode);
|
|
1423
|
+
this.serverNode = undefined;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (this.bridgeMode === 'childbridge') {
|
|
1427
|
+
for (const plugin of this.plugins.array()) {
|
|
1428
|
+
if (plugin.serverNode) {
|
|
1429
|
+
await this.stopServerNode(plugin.serverNode);
|
|
1430
|
+
plugin.serverNode = undefined;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
for (const device of this.devices.array()) {
|
|
1435
|
+
if (device.mode === 'server' && device.serverNode) {
|
|
1436
|
+
await this.stopServerNode(device.serverNode);
|
|
1437
|
+
device.serverNode = undefined;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
this.log.notice('Stopped matter server nodes');
|
|
1441
|
+
// Matter commisioning reset
|
|
1442
|
+
if (message === 'shutting down with reset...') {
|
|
1443
|
+
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1444
|
+
await this.matterStorageManager?.createContext('events')?.clearAll();
|
|
1445
|
+
await this.matterStorageManager?.createContext('fabrics')?.clearAll();
|
|
1446
|
+
await this.matterStorageManager?.createContext('root')?.clearAll();
|
|
1447
|
+
await this.matterStorageManager?.createContext('sessions')?.clearAll();
|
|
1448
|
+
await this.matterbridgeContext?.clearAll();
|
|
1449
|
+
this.log.info('Matter storage reset done! Remove the bridge from the controller.');
|
|
1450
|
+
}
|
|
1451
|
+
// Unregister all devices
|
|
1452
|
+
if (message === 'unregistered all devices and shutting down...') {
|
|
1453
|
+
if (this.bridgeMode === 'bridge') {
|
|
1454
|
+
await this.matterStorageManager?.createContext('root')?.createContext('parts')?.createContext('Matterbridge')?.createContext('parts')?.clearAll();
|
|
1455
|
+
await this.matterStorageManager?.createContext('root')?.createContext('subscription')?.clearAll();
|
|
1456
|
+
await this.matterStorageManager?.createContext('sessions')?.clearAll();
|
|
1457
|
+
}
|
|
1458
|
+
else if (this.bridgeMode === 'childbridge') {
|
|
1459
|
+
for (const plugin of this.plugins.array()) {
|
|
1460
|
+
if (plugin.type === 'DynamicPlatform') {
|
|
1461
|
+
await plugin.storageContext?.createContext('root')?.createContext('parts')?.createContext(plugin.name)?.createContext('parts')?.clearAll();
|
|
1462
|
+
}
|
|
1463
|
+
await plugin.storageContext?.createContext('root')?.createContext('subscription')?.clearAll();
|
|
1464
|
+
await plugin.storageContext?.createContext('sessions')?.clearAll();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
this.log.info('Matter storage reset done!');
|
|
1468
|
+
}
|
|
1469
|
+
// Stop matter storage
|
|
1470
|
+
await this.stopMatterStorage();
|
|
1471
|
+
/**
|
|
1472
|
+
* Unlink a file safely, ignoring errors.
|
|
1473
|
+
*
|
|
1474
|
+
* @param {string} path - The path to the file to unlink.
|
|
1475
|
+
* @param {AnsiLogger} log - The logger to use for logging.
|
|
1476
|
+
*/
|
|
1477
|
+
function unlinkSafe(path, log) {
|
|
1478
|
+
try {
|
|
1479
|
+
log.debug(`Removing ${path}...`);
|
|
1480
|
+
unlinkSync(path);
|
|
1481
|
+
log.debug(`Removed ${path}`);
|
|
1482
|
+
}
|
|
1483
|
+
catch {
|
|
1484
|
+
// Ignore errors if the file does not exist
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
// Remove the resumption records for Matterbridge (bridge mode)
|
|
1488
|
+
this.log.debug(`Cleaning matter storage context for ${GREEN}Matterbridge${db}...`);
|
|
1489
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, 'Matterbridge', 'sessions.resumptionRecords'), this.log);
|
|
1490
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, 'Matterbridge', 'root.subscriptions.subscriptions'), this.log);
|
|
1491
|
+
for (const plugin of this.plugins.array()) {
|
|
1492
|
+
// Remove the resumption records for the plugins (childbridge mode)
|
|
1493
|
+
this.log.debug(`Cleaning matter storage context for plugin ${plg}${plugin.name}${db}...`);
|
|
1494
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, plugin.name, 'sessions.resumptionRecords'), this.log);
|
|
1495
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, plugin.name, 'root.subscriptions.subscriptions'), this.log);
|
|
1496
|
+
}
|
|
1497
|
+
for (const device of this.devices.array().filter((d) => d.mode === 'server')) {
|
|
1498
|
+
if (!device.deviceName)
|
|
1499
|
+
continue;
|
|
1500
|
+
// Remove the resumption records for the server mode devices
|
|
1501
|
+
this.log.debug(`Cleaning matter storage context for server node device ${dev}${device.deviceName}${db}...`);
|
|
1502
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, device.deviceName.replace(/[ .]/g, ''), 'sessions.resumptionRecords'), this.log);
|
|
1503
|
+
unlinkSafe(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME, device.deviceName.replace(/[ .]/g, ''), 'root.subscriptions.subscriptions'), this.log);
|
|
1504
|
+
}
|
|
1505
|
+
// Stop the frontend
|
|
1506
|
+
await this.frontend.stop();
|
|
1507
|
+
this.frontend.destroy();
|
|
1508
|
+
// Close PluginManager and DeviceManager
|
|
1509
|
+
this.plugins.destroy();
|
|
1510
|
+
this.devices.destroy();
|
|
1511
|
+
// Stop thread messaging server
|
|
1512
|
+
this.server.close();
|
|
1513
|
+
// Close the matterbridge node storage and context
|
|
1514
|
+
if (this.nodeStorage && this.nodeContext) {
|
|
1515
|
+
/*
|
|
1516
|
+
TODO: Implement serialization of registered devices
|
|
1517
|
+
this.log.info('Saving registered devices...');
|
|
1518
|
+
const serializedRegisteredDevices: SerializedMatterbridgeEndpoint[] = [];
|
|
1519
|
+
this.devices.forEach(async (device) => {
|
|
1520
|
+
const serializedMatterbridgeDevice = MatterbridgeEndpoint.serialize(device);
|
|
1521
|
+
this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
|
|
1522
|
+
if (serializedMatterbridgeDevice) serializedRegisteredDevices.push(serializedMatterbridgeDevice);
|
|
1523
|
+
});
|
|
1524
|
+
await this.nodeContext.set<SerializedMatterbridgeEndpoint[]>('devices', serializedRegisteredDevices);
|
|
1525
|
+
this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
|
|
1526
|
+
*/
|
|
1527
|
+
// Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
|
|
1528
|
+
this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
|
|
1529
|
+
await this.nodeContext.close();
|
|
1530
|
+
this.nodeContext = undefined;
|
|
1531
|
+
// Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
|
|
1532
|
+
for (const plugin of this.plugins) {
|
|
1533
|
+
if (plugin.nodeContext) {
|
|
1534
|
+
this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
|
|
1535
|
+
await plugin.nodeContext.close();
|
|
1536
|
+
plugin.nodeContext = undefined;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
this.log.debug('Closing node storage manager...');
|
|
1540
|
+
await this.nodeStorage.close();
|
|
1541
|
+
this.nodeStorage = undefined;
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
this.log.error('Error close the matterbridge node storage and context: nodeStorage or nodeContext not found!');
|
|
1545
|
+
}
|
|
1546
|
+
this.plugins.clear();
|
|
1547
|
+
this.devices.clear();
|
|
1548
|
+
// Factory reset
|
|
1549
|
+
if (message === 'shutting down with factory reset...') {
|
|
1550
|
+
try {
|
|
1551
|
+
// Delete matter storage directory with its subdirectories and backup
|
|
1552
|
+
const dir = path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME);
|
|
1553
|
+
this.log.info(`Removing matter storage directory: ${dir}`);
|
|
1554
|
+
await fs.promises.rm(dir, { recursive: true });
|
|
1555
|
+
const backup = path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup');
|
|
1556
|
+
this.log.info(`Removing matter storage backup directory: ${backup}`);
|
|
1557
|
+
await fs.promises.rm(backup, { recursive: true });
|
|
1558
|
+
}
|
|
1559
|
+
catch (error) {
|
|
1560
|
+
// istanbul ignore next if
|
|
1561
|
+
if (error instanceof Error && error.code !== 'ENOENT') {
|
|
1562
|
+
this.log.error(`Error removing matter storage directory: ${error}`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
// Delete matterbridge storage directory with its subdirectories and backup
|
|
1567
|
+
const dir = path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR);
|
|
1568
|
+
this.log.info(`Removing matterbridge storage directory: ${dir}`);
|
|
1569
|
+
await fs.promises.rm(dir, { recursive: true });
|
|
1570
|
+
const backup = path.join(this.matterbridgeDirectory, NODE_STORAGE_DIR + '.backup');
|
|
1571
|
+
this.log.info(`Removing matterbridge storage backup directory: ${backup}`);
|
|
1572
|
+
await fs.promises.rm(backup, { recursive: true });
|
|
1573
|
+
}
|
|
1574
|
+
catch (error) {
|
|
1575
|
+
// istanbul ignore next if
|
|
1576
|
+
if (error instanceof Error && error.code !== 'ENOENT') {
|
|
1577
|
+
this.log.error(`Error removing matterbridge storage directory: ${error}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
|
|
1581
|
+
}
|
|
1582
|
+
// Deregisters the process handlers
|
|
1583
|
+
this.deregisterProcessHandlers();
|
|
1584
|
+
if (restart) {
|
|
1585
|
+
if (message === 'updating...') {
|
|
1586
|
+
this.log.info('Cleanup completed. Updating...');
|
|
1587
|
+
Matterbridge.instance = undefined;
|
|
1588
|
+
this.emit('update'); // Restart the process but the update has been done before. TODO move all updates to the cli
|
|
1589
|
+
}
|
|
1590
|
+
else if (message === 'restarting...') {
|
|
1591
|
+
this.log.info('Cleanup completed. Restarting...');
|
|
1592
|
+
Matterbridge.instance = undefined;
|
|
1593
|
+
this.emit('restart');
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
this.log.notice('Cleanup completed. Shutting down...');
|
|
1598
|
+
Matterbridge.instance = undefined;
|
|
1599
|
+
this.emit('shutdown');
|
|
1600
|
+
}
|
|
1601
|
+
this.hasCleanupStarted = false;
|
|
1602
|
+
this.initialized = false;
|
|
1603
|
+
this.emit('cleanup_completed');
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
if (!this.initialized) {
|
|
1607
|
+
this.log.debug('Cleanup with instance not initialized...');
|
|
1608
|
+
this.destroy();
|
|
1609
|
+
this.frontend.destroy();
|
|
1610
|
+
this.plugins.destroy();
|
|
1611
|
+
this.devices.destroy();
|
|
1612
|
+
}
|
|
1613
|
+
if (this.hasCleanupStarted)
|
|
1614
|
+
this.log.debug('Cleanup already started...');
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Starts the Matterbridge in bridge mode.
|
|
1619
|
+
*
|
|
1620
|
+
* @private
|
|
1621
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1622
|
+
*/
|
|
1623
|
+
async startBridge() {
|
|
1624
|
+
// Plugins are configured by a timer when matter server is started and plugin.configured is set to true
|
|
1625
|
+
if (!this.matterStorageManager)
|
|
1626
|
+
throw new Error('No storage manager initialized');
|
|
1627
|
+
if (!this.matterbridgeContext)
|
|
1628
|
+
throw new Error('No storage context initialized');
|
|
1629
|
+
this.serverNode = await this.createServerNode(this.matterbridgeContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
1630
|
+
this.aggregatorNode = await this.createAggregatorNode(this.matterbridgeContext);
|
|
1631
|
+
await this.serverNode.add(this.aggregatorNode);
|
|
1632
|
+
await addVirtualDevices(this, this.aggregatorNode);
|
|
1633
|
+
await this.startPlugins();
|
|
1634
|
+
this.log.debug('Starting start matter interval in bridge mode...');
|
|
1635
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is starting...`, 0, 'info');
|
|
1636
|
+
let failCount = 0;
|
|
1637
|
+
this.startMatterInterval = setInterval(async () => {
|
|
1638
|
+
// istanbul ignore if cause is just a logging statement
|
|
1639
|
+
if (failCount && failCount % 10 === 0) {
|
|
1640
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is still starting...`, 10, 'info');
|
|
1641
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1642
|
+
}
|
|
1643
|
+
for (const plugin of this.plugins) {
|
|
1644
|
+
if (!plugin.enabled)
|
|
1645
|
+
continue;
|
|
1646
|
+
if (plugin.error) {
|
|
1647
|
+
clearInterval(this.startMatterInterval);
|
|
1648
|
+
this.startMatterInterval = undefined;
|
|
1649
|
+
this.log.debug('Cleared startMatterInterval interval for Matterbridge for plugin in error state');
|
|
1650
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
|
|
1651
|
+
this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
|
|
1652
|
+
this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
|
|
1653
|
+
this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} is in error state. Check the logs.`, 0, 'error');
|
|
1654
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is offline. Startup halted due to plugin errors.`, 0, 'error');
|
|
1655
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1656
|
+
this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (!plugin.loaded || !plugin.started) {
|
|
1660
|
+
this.log.debug(`Waiting (failSafeCount=${failCount}/${this.failCountLimit}) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
|
|
1661
|
+
failCount++;
|
|
1662
|
+
if (failCount > this.failCountLimit) {
|
|
1663
|
+
this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
|
|
1664
|
+
plugin.error = true;
|
|
1665
|
+
}
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
clearInterval(this.startMatterInterval);
|
|
1670
|
+
this.startMatterInterval = undefined;
|
|
1671
|
+
this.log.debug('Cleared startMatterInterval interval in bridge mode');
|
|
1672
|
+
// Start the Matter server node
|
|
1673
|
+
this.startServerNode(this.serverNode); // We don't await this, because the server node is started in the background
|
|
1674
|
+
// Start the Matter server node of single devices in mode 'server'
|
|
1675
|
+
for (const device of this.devices.array()) {
|
|
1676
|
+
if (device.mode === 'server' && device.serverNode) {
|
|
1677
|
+
this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
|
|
1678
|
+
this.startServerNode(device.serverNode); // We don't await this, because the server node is started in the background
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// Configure the plugins
|
|
1682
|
+
this.configureTimeout = setTimeout(async () => {
|
|
1683
|
+
for (const plugin of this.plugins.array()) {
|
|
1684
|
+
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
1685
|
+
continue;
|
|
1686
|
+
try {
|
|
1687
|
+
if ((await this.plugins.configure(plugin)) === undefined) {
|
|
1688
|
+
if (plugin.configured !== true)
|
|
1689
|
+
this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} failed to configure. Check the logs.`, 0, 'error');
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
catch (error) {
|
|
1693
|
+
plugin.error = true;
|
|
1694
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1698
|
+
}, 30 * 1000).unref();
|
|
1699
|
+
// Setting reachability to true
|
|
1700
|
+
this.reachabilityTimeout = setTimeout(() => {
|
|
1701
|
+
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
1702
|
+
if (this.aggregatorNode)
|
|
1703
|
+
this.setAggregatorReachability(this.aggregatorNode, true);
|
|
1704
|
+
}, 60 * 1000).unref();
|
|
1705
|
+
// Logger.get('LogServerNode').info(this.serverNode);
|
|
1706
|
+
this.emit('bridge_started');
|
|
1707
|
+
this.log.notice('Matterbridge bridge started successfully');
|
|
1708
|
+
this.frontend.wssSendRefreshRequired('settings');
|
|
1709
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1710
|
+
this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
|
|
1711
|
+
}, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Starts the Matterbridge in childbridge mode.
|
|
1715
|
+
*
|
|
1716
|
+
* @param {number} [delay] - The delay before starting the childbridge. Default is 1000 milliseconds.
|
|
1717
|
+
*
|
|
1718
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1719
|
+
*/
|
|
1720
|
+
async startChildbridge(delay = 1000) {
|
|
1721
|
+
if (!this.matterStorageManager)
|
|
1722
|
+
throw new Error('No storage manager initialized');
|
|
1723
|
+
const { wait } = await import('@matterbridge/utils');
|
|
1724
|
+
// Load with await all plugins but don't start them. We get the platform.type to pre-create server nodes for DynamicPlatform plugins
|
|
1725
|
+
this.log.debug('Loading all plugins in childbridge mode...');
|
|
1726
|
+
await this.startPlugins(true, false);
|
|
1727
|
+
// Create server nodes for DynamicPlatform plugins and start all plugins in the background
|
|
1728
|
+
this.log.debug('Creating server nodes for DynamicPlatform plugins and starting all plugins in childbridge mode...');
|
|
1729
|
+
for (const plugin of this.plugins.array().filter((p) => p.enabled && !p.error)) {
|
|
1730
|
+
if (plugin.type === 'DynamicPlatform')
|
|
1731
|
+
await this.createDynamicPlugin(plugin);
|
|
1732
|
+
this.plugins.start(plugin, 'Matterbridge is starting'); // Start the plugin in the background
|
|
1733
|
+
}
|
|
1734
|
+
// Start the Matterbridge in childbridge mode when all plugins are loaded and started
|
|
1735
|
+
this.log.debug('Starting start matter interval in childbridge mode...');
|
|
1736
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is starting...`, 0, 'info');
|
|
1737
|
+
let failCount = 0;
|
|
1738
|
+
this.startMatterInterval = setInterval(async () => {
|
|
1739
|
+
// istanbul ignore if cause is just a logging statement
|
|
1740
|
+
if (failCount && failCount % 10 === 0) {
|
|
1741
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is still starting...`, 10, 'info');
|
|
1742
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1743
|
+
}
|
|
1744
|
+
let allStarted = true;
|
|
1745
|
+
for (const plugin of this.plugins.array()) {
|
|
1746
|
+
if (!plugin.enabled)
|
|
1747
|
+
continue;
|
|
1748
|
+
if (plugin.error) {
|
|
1749
|
+
clearInterval(this.startMatterInterval);
|
|
1750
|
+
this.startMatterInterval = undefined;
|
|
1751
|
+
this.log.debug('Cleared startMatterInterval interval for a plugin in error state');
|
|
1752
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
|
|
1753
|
+
this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
|
|
1754
|
+
this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
|
|
1755
|
+
this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} is in error state. Check the logs.`, 0, 'error');
|
|
1756
|
+
this.frontend.wssSendSnackbarMessage(`The bridge is offline. Startup halted due to plugin errors.`, 0, 'error');
|
|
1757
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1758
|
+
this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
this.log.debug(`Checking plugin ${plg}${plugin.name}${db} to start matter in childbridge mode...`);
|
|
1762
|
+
if (!plugin.loaded || !plugin.started) {
|
|
1763
|
+
allStarted = false;
|
|
1764
|
+
this.log.debug(`Waiting (failSafeCount=${failCount}/${this.failCountLimit}) for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
|
|
1765
|
+
failCount++;
|
|
1766
|
+
if (failCount > this.failCountLimit) {
|
|
1767
|
+
this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
|
|
1768
|
+
plugin.error = true;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
if (!allStarted)
|
|
1773
|
+
return;
|
|
1774
|
+
clearInterval(this.startMatterInterval);
|
|
1775
|
+
this.startMatterInterval = undefined;
|
|
1776
|
+
if (delay > 0)
|
|
1777
|
+
await wait(Number(process.env['MATTERBRIDGE_PAUSE_MATTER_INTERVAL_MS']) || delay); // Wait for the specified delay to ensure all plugins server nodes are ready
|
|
1778
|
+
this.log.debug('Cleared startMatterInterval interval in childbridge mode');
|
|
1779
|
+
// Configure the plugins
|
|
1780
|
+
this.configureTimeout = setTimeout(async () => {
|
|
1781
|
+
for (const plugin of this.plugins.array()) {
|
|
1782
|
+
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
1783
|
+
continue;
|
|
1784
|
+
try {
|
|
1785
|
+
if ((await this.plugins.configure(plugin)) === undefined) {
|
|
1786
|
+
if (plugin.configured !== true)
|
|
1787
|
+
this.frontend.wssSendSnackbarMessage(`The plugin ${plugin.name} failed to configure. Check the logs.`, 0, 'error');
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
catch (error) {
|
|
1791
|
+
plugin.error = true;
|
|
1792
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1796
|
+
}, 30 * 1000).unref();
|
|
1797
|
+
for (const plugin of this.plugins.array()) {
|
|
1798
|
+
if (!plugin.enabled || plugin.error)
|
|
1799
|
+
continue;
|
|
1800
|
+
if (plugin.type !== 'DynamicPlatform' && (!plugin.registeredDevices || plugin.registeredDevices === 0)) {
|
|
1801
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} didn't register any devices to Matterbridge. Verify the plugin configuration.`);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
// istanbul ignore next if cause is just a safety check
|
|
1805
|
+
if (!plugin.serverNode) {
|
|
1806
|
+
this.log.error(`Server node not found for plugin ${plg}${plugin.name}${er}`);
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
if (!plugin.storageContext) {
|
|
1810
|
+
this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (!plugin.nodeContext) {
|
|
1814
|
+
this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
// Start the Matter server node
|
|
1818
|
+
this.startServerNode(plugin.serverNode); // We don't await this, because the server node is started in the background
|
|
1819
|
+
// Setting reachability to true
|
|
1820
|
+
plugin.reachabilityTimeout = setTimeout(() => {
|
|
1821
|
+
this.log.info(`Setting reachability to true for ${plg}${plugin.name}${nf}`);
|
|
1822
|
+
if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
|
|
1823
|
+
this.setAggregatorReachability(plugin.aggregatorNode, true);
|
|
1824
|
+
}, 60 * 1000).unref();
|
|
1825
|
+
}
|
|
1826
|
+
// Start the Matter server node of single devices in mode 'server'
|
|
1827
|
+
for (const device of this.devices.array()) {
|
|
1828
|
+
if (device.mode === 'server' && device.serverNode) {
|
|
1829
|
+
this.log.debug(`Starting server node for device ${dev}${device.deviceName}${db} in server mode...`);
|
|
1830
|
+
this.startServerNode(device.serverNode); // We don't await this, because the server node is started in the background
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
// Logger.get('LogServerNode').info(this.serverNode);
|
|
1834
|
+
this.emit('childbridge_started');
|
|
1835
|
+
this.log.notice('Matterbridge childbridge started successfully');
|
|
1836
|
+
this.frontend.wssSendRefreshRequired('settings');
|
|
1837
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1838
|
+
this.frontend.wssSendCloseSnackbarMessage(`The bridge is starting...`);
|
|
1839
|
+
}, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Starts the Matterbridge controller.
|
|
1843
|
+
*
|
|
1844
|
+
* @private
|
|
1845
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1846
|
+
*/
|
|
1847
|
+
async startController() {
|
|
1848
|
+
/*
|
|
1849
|
+
if (!this.matterStorageManager) {
|
|
1850
|
+
this.log.error('No storage manager initialized');
|
|
1851
|
+
await this.cleanup('No storage manager initialized');
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
this.log.info('Creating context: mattercontrollerContext');
|
|
1855
|
+
this.controllerContext = this.matterStorageManager.createContext('mattercontrollerContext');
|
|
1856
|
+
if (!this.controllerContext) {
|
|
1857
|
+
this.log.error('No storage context mattercontrollerContext initialized');
|
|
1858
|
+
await this.cleanup('No storage context mattercontrollerContext initialized');
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
this.log.debug('Starting matterbridge in mode', this.bridgeMode);
|
|
1863
|
+
this.matterServer = await this.createMatterServer(this.storageManager);
|
|
1864
|
+
this.log.info('Creating matter commissioning controller');
|
|
1865
|
+
this.commissioningController = new CommissioningController({
|
|
1866
|
+
autoConnect: false,
|
|
1867
|
+
});
|
|
1868
|
+
this.log.info('Adding matter commissioning controller to matter server');
|
|
1869
|
+
await this.matterServer.addCommissioningController(this.commissioningController);
|
|
1870
|
+
|
|
1871
|
+
this.log.info('Starting matter server');
|
|
1872
|
+
await this.matterServer.start();
|
|
1873
|
+
this.log.info('Matter server started');
|
|
1874
|
+
const commissioningOptions: ControllerCommissioningFlowOptions = {
|
|
1875
|
+
regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
|
|
1876
|
+
regulatoryCountryCode: 'XX',
|
|
1877
|
+
};
|
|
1878
|
+
const commissioningController = new CommissioningController({
|
|
1879
|
+
environment: {
|
|
1880
|
+
environment,
|
|
1881
|
+
id: uniqueId,
|
|
1882
|
+
},
|
|
1883
|
+
autoConnect: false, // Do not auto connect to the commissioned nodes
|
|
1884
|
+
adminFabricLabel,
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
if (hasParameter('pairingcode')) {
|
|
1888
|
+
this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
|
|
1889
|
+
const pairingCode = getParameter('pairingcode');
|
|
1890
|
+
const ip = this.controllerContext.has('ip') ? this.controllerContext.get<string>('ip') : undefined;
|
|
1891
|
+
const port = this.controllerContext.has('port') ? this.controllerContext.get<number>('port') : undefined;
|
|
1892
|
+
|
|
1893
|
+
let longDiscriminator, setupPin, shortDiscriminator;
|
|
1894
|
+
if (pairingCode !== undefined) {
|
|
1895
|
+
const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
|
|
1896
|
+
shortDiscriminator = pairingCodeCodec.shortDiscriminator;
|
|
1897
|
+
longDiscriminator = undefined;
|
|
1898
|
+
setupPin = pairingCodeCodec.passcode;
|
|
1899
|
+
this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
|
|
1900
|
+
} else {
|
|
1901
|
+
longDiscriminator = await this.controllerContext.get('longDiscriminator', 3840);
|
|
1902
|
+
if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
|
|
1903
|
+
setupPin = this.controllerContext.get('pin', 20202021);
|
|
1904
|
+
}
|
|
1905
|
+
if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
|
|
1906
|
+
throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
const options = {
|
|
1910
|
+
commissioning: commissioningOptions,
|
|
1911
|
+
discovery: {
|
|
1912
|
+
knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
|
|
1913
|
+
identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
|
|
1914
|
+
},
|
|
1915
|
+
passcode: setupPin,
|
|
1916
|
+
} as NodeCommissioningOptions;
|
|
1917
|
+
this.log.info('Commissioning with options:', options);
|
|
1918
|
+
const nodeId = await this.commissioningController.commissionNode(options);
|
|
1919
|
+
this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
|
|
1920
|
+
this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
|
|
1921
|
+
} // (hasParameter('pairingcode'))
|
|
1922
|
+
|
|
1923
|
+
if (hasParameter('unpairall')) {
|
|
1924
|
+
this.log.info('***Commissioning controller unpairing all nodes...');
|
|
1925
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1926
|
+
for (const nodeId of nodeIds) {
|
|
1927
|
+
this.log.info('***Commissioning controller unpairing node:', nodeId);
|
|
1928
|
+
await this.commissioningController.removeNode(nodeId);
|
|
1929
|
+
}
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (hasParameter('discover')) {
|
|
1934
|
+
// const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
|
|
1935
|
+
// console.log(discover);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (!this.commissioningController.isCommissioned()) {
|
|
1939
|
+
this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1944
|
+
this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
|
|
1945
|
+
for (const nodeId of nodeIds) {
|
|
1946
|
+
this.log.info(`***Connecting to commissioned node: ${nodeId}`);
|
|
1947
|
+
|
|
1948
|
+
const node = await this.commissioningController.connectNode(nodeId, {
|
|
1949
|
+
autoSubscribe: false,
|
|
1950
|
+
attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
|
|
1951
|
+
this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
|
|
1952
|
+
eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
|
|
1953
|
+
this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
|
|
1954
|
+
stateInformationCallback: (peerNodeId, info) => {
|
|
1955
|
+
switch (info) {
|
|
1956
|
+
case NodeStateInformation.Connected:
|
|
1957
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
|
|
1958
|
+
break;
|
|
1959
|
+
case NodeStateInformation.Disconnected:
|
|
1960
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
|
|
1961
|
+
break;
|
|
1962
|
+
case NodeStateInformation.Reconnecting:
|
|
1963
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
|
|
1964
|
+
break;
|
|
1965
|
+
case NodeStateInformation.WaitingForDeviceDiscovery:
|
|
1966
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
|
|
1967
|
+
break;
|
|
1968
|
+
case NodeStateInformation.StructureChanged:
|
|
1969
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
|
|
1970
|
+
break;
|
|
1971
|
+
case NodeStateInformation.Decommissioned:
|
|
1972
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
|
|
1973
|
+
break;
|
|
1974
|
+
default:
|
|
1975
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
node.logStructure();
|
|
1982
|
+
|
|
1983
|
+
// Get the interaction client
|
|
1984
|
+
this.log.info('Getting the interaction client');
|
|
1985
|
+
const interactionClient = await node.getInteractionClient();
|
|
1986
|
+
let cluster;
|
|
1987
|
+
let attributes;
|
|
1988
|
+
|
|
1989
|
+
// Log BasicInformationCluster
|
|
1990
|
+
cluster = BasicInformationCluster;
|
|
1991
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1992
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1993
|
+
});
|
|
1994
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1995
|
+
attributes.forEach((attribute) => {
|
|
1996
|
+
this.log.info(
|
|
1997
|
+
`- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
|
|
1998
|
+
);
|
|
1999
|
+
});
|
|
2000
|
+
|
|
2001
|
+
// Log PowerSourceCluster
|
|
2002
|
+
cluster = PowerSourceCluster;
|
|
2003
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
2004
|
+
attributes: [{ clusterId: cluster.id }],
|
|
2005
|
+
});
|
|
2006
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
2007
|
+
attributes.forEach((attribute) => {
|
|
2008
|
+
this.log.info(
|
|
2009
|
+
`- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
|
|
2010
|
+
);
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
// Log ThreadNetworkDiagnostics
|
|
2014
|
+
cluster = ThreadNetworkDiagnosticsCluster;
|
|
2015
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
2016
|
+
attributes: [{ clusterId: cluster.id }],
|
|
2017
|
+
});
|
|
2018
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
2019
|
+
attributes.forEach((attribute) => {
|
|
2020
|
+
this.log.info(
|
|
2021
|
+
`- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
|
|
2022
|
+
);
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
// Log SwitchCluster
|
|
2026
|
+
cluster = SwitchCluster;
|
|
2027
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
2028
|
+
attributes: [{ clusterId: cluster.id }],
|
|
2029
|
+
});
|
|
2030
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
2031
|
+
attributes.forEach((attribute) => {
|
|
2032
|
+
this.log.info(
|
|
2033
|
+
`- endpoint ${or}${attribute.path.endpointId}${nf} cluster ${hk}${getClusterNameById(attribute.path.clusterId)}${nf} (${hk}0x${attribute.path.clusterId.toString(16)}${nf}) attribute ${zb}${attribute.path.attributeName}${nf} (${zb}0x${attribute.path.attributeId.toString(16)}${nf}): ${typeof attribute.value === 'object' ? stringify(attribute.value) : attribute.value}`,
|
|
2034
|
+
);
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
this.log.info('Subscribing to all attributes and events');
|
|
2038
|
+
await node.subscribeAllAttributesAndEvents({
|
|
2039
|
+
ignoreInitialTriggers: false,
|
|
2040
|
+
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
|
|
2041
|
+
this.log.info(
|
|
2042
|
+
`***${db}Commissioning controller attributeChangedCallback version ${version}: attribute ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${attributeName}${db} changed to ${typeof value === 'object' ? debugStringify(value ?? { none: true }) : value}`,
|
|
2043
|
+
),
|
|
2044
|
+
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
|
|
2045
|
+
this.log.info(
|
|
2046
|
+
`***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
|
|
2047
|
+
);
|
|
2048
|
+
},
|
|
2049
|
+
});
|
|
2050
|
+
this.log.info('Subscribed to all attributes and events');
|
|
2051
|
+
}
|
|
2052
|
+
*/
|
|
2053
|
+
}
|
|
2054
|
+
/** */
|
|
2055
|
+
/** Matter.js methods */
|
|
2056
|
+
/** */
|
|
2057
|
+
/**
|
|
2058
|
+
* Starts the matter storage with name Matterbridge, create the matterbridge context and performs a backup.
|
|
2059
|
+
*
|
|
2060
|
+
* @returns {Promise<void>} - A promise that resolves when the storage is started.
|
|
2061
|
+
*/
|
|
2062
|
+
async startMatterStorage() {
|
|
2063
|
+
// Setup Matter storage
|
|
2064
|
+
this.log.info(`Starting matter node storage...`);
|
|
2065
|
+
this.matterStorageService = this.environment.get(StorageService);
|
|
2066
|
+
this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
|
|
2067
|
+
this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
|
|
2068
|
+
this.log.info('Matter node storage manager "Matterbridge" created');
|
|
2069
|
+
this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, this.aggregatorSerialNumber, this.aggregatorUniqueId);
|
|
2070
|
+
this.log.info('Matter node storage started');
|
|
2071
|
+
// Backup matter storage since it is created/opened correctly
|
|
2072
|
+
await this.backupMatterStorage(path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME), path.join(this.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup'));
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Makes a backup copy of the specified matter storage directory.
|
|
2076
|
+
*
|
|
2077
|
+
* @param {string} storageName - The name of the storage directory to be backed up.
|
|
2078
|
+
* @param {string} backupName - The name of the backup directory to be created.
|
|
2079
|
+
* @private
|
|
2080
|
+
* @returns {Promise<void>} A promise that resolves when the has been done.
|
|
2081
|
+
*/
|
|
2082
|
+
async backupMatterStorage(storageName, backupName) {
|
|
2083
|
+
this.log.info('Creating matter node storage backup...');
|
|
2084
|
+
try {
|
|
2085
|
+
await copyDirectory(storageName, backupName);
|
|
2086
|
+
this.log.info('Created matter node storage backup');
|
|
2087
|
+
}
|
|
2088
|
+
catch (error) {
|
|
2089
|
+
this.log.error(`Error creating matter node storage backup from ${storageName} to ${backupName}:`, error);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Stops the matter storage.
|
|
2094
|
+
*
|
|
2095
|
+
* @returns {Promise<void>} A promise that resolves when the storage is stopped.
|
|
2096
|
+
*/
|
|
2097
|
+
async stopMatterStorage() {
|
|
2098
|
+
this.log.info('Closing matter node storage...');
|
|
2099
|
+
await this.matterStorageManager?.close();
|
|
2100
|
+
this.matterStorageService = undefined;
|
|
2101
|
+
this.matterStorageManager = undefined;
|
|
2102
|
+
this.matterbridgeContext = undefined;
|
|
2103
|
+
this.log.info('Matter node storage closed');
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Creates a server node storage context.
|
|
2107
|
+
*
|
|
2108
|
+
* @param {string} storeId - The storeId.
|
|
2109
|
+
* @param {string} deviceName - The name of the device.
|
|
2110
|
+
* @param {DeviceTypeId} deviceType - The device type of the device.
|
|
2111
|
+
* @param {number} vendorId - The vendor ID.
|
|
2112
|
+
* @param {string} vendorName - The vendor name.
|
|
2113
|
+
* @param {number} productId - The product ID.
|
|
2114
|
+
* @param {string} productName - The product name.
|
|
2115
|
+
* @param {string} [serialNumber] - The serial number of the device (optional).
|
|
2116
|
+
* @param {string} [uniqueId] - The unique ID of the device (optional).
|
|
2117
|
+
* @returns {Promise<StorageContext>} The storage context for the commissioning server.
|
|
2118
|
+
*/
|
|
2119
|
+
async createServerNodeContext(storeId, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
|
|
2120
|
+
const { randomBytes } = await import('node:crypto');
|
|
2121
|
+
if (!this.matterStorageService)
|
|
2122
|
+
throw new Error('No storage service initialized');
|
|
2123
|
+
this.log.info(`Creating server node storage context "${storeId}.persist" for ${storeId}...`);
|
|
2124
|
+
const storageManager = await this.matterStorageService.open(storeId);
|
|
2125
|
+
const storageContext = storageManager.createContext('persist');
|
|
2126
|
+
const random = randomBytes(8).toString('hex');
|
|
2127
|
+
await storageContext.set('storeId', storeId);
|
|
2128
|
+
await storageContext.set('deviceName', deviceName);
|
|
2129
|
+
await storageContext.set('deviceType', deviceType);
|
|
2130
|
+
await storageContext.set('vendorId', vendorId);
|
|
2131
|
+
await storageContext.set('vendorName', vendorName.slice(0, 32));
|
|
2132
|
+
await storageContext.set('productId', productId);
|
|
2133
|
+
await storageContext.set('productName', productName.slice(0, 32));
|
|
2134
|
+
await storageContext.set('productLabel', productName.slice(0, 32));
|
|
2135
|
+
await storageContext.set('nodeLabel', deviceName.slice(0, 32));
|
|
2136
|
+
await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
|
|
2137
|
+
await storageContext.set('uniqueId', await storageContext.get('uniqueId', uniqueId ? uniqueId.slice(0, 32) : 'UI' + random));
|
|
2138
|
+
await storageContext.set('softwareVersion', isValidNumber(parseVersionString(this.matterbridgeVersion), 0, UINT32_MAX) ? parseVersionString(this.matterbridgeVersion) : 1);
|
|
2139
|
+
await storageContext.set('softwareVersionString', isValidString(this.matterbridgeVersion, 5, 64) ? this.matterbridgeVersion : '1.0.0');
|
|
2140
|
+
await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.systemInformation.osRelease) : 1);
|
|
2141
|
+
await storageContext.set('hardwareVersionString', isValidString(this.systemInformation.osRelease, 5, 64) ? this.systemInformation.osRelease : '1.0.0');
|
|
2142
|
+
this.log.debug(`Created server node storage context "${storeId}.persist" for ${storeId}:`);
|
|
2143
|
+
this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
|
|
2144
|
+
this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
|
|
2145
|
+
this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
|
|
2146
|
+
this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
|
|
2147
|
+
this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
|
|
2148
|
+
this.log.debug(`- productId: ${await storageContext.get('productId')}`);
|
|
2149
|
+
this.log.debug(`- productName: ${await storageContext.get('productName')}`);
|
|
2150
|
+
this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
|
|
2151
|
+
this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
|
|
2152
|
+
this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
|
|
2153
|
+
this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
2154
|
+
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
|
|
2155
|
+
this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
2156
|
+
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
|
|
2157
|
+
this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
2158
|
+
return storageContext;
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Creates a server node.
|
|
2162
|
+
*
|
|
2163
|
+
* @param {StorageContext} storageContext - The storage context for the server node.
|
|
2164
|
+
* @param {number} [port] - The port number for the server node. Defaults to 5540.
|
|
2165
|
+
* @param {number} [passcode] - The passcode for the server node. Defaults to 20242025.
|
|
2166
|
+
* @param {number} [discriminator] - The discriminator for the server node. Defaults to 3850.
|
|
2167
|
+
* @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
|
|
2168
|
+
*/
|
|
2169
|
+
async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
|
|
2170
|
+
const storeId = await storageContext.get('storeId');
|
|
2171
|
+
this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
|
|
2172
|
+
this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
|
|
2173
|
+
this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
|
|
2174
|
+
this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
|
|
2175
|
+
this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
|
|
2176
|
+
this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
|
|
2177
|
+
this.log.debug(`- productId: ${await storageContext.get('productId')}`);
|
|
2178
|
+
this.log.debug(`- productName: ${await storageContext.get('productName')}`);
|
|
2179
|
+
this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
|
|
2180
|
+
this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
|
|
2181
|
+
this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
|
|
2182
|
+
this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
2183
|
+
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
|
|
2184
|
+
this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
2185
|
+
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
|
|
2186
|
+
this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
2187
|
+
/**
|
|
2188
|
+
* Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
|
|
2189
|
+
*/
|
|
2190
|
+
const serverNode = await ServerNode.create({
|
|
2191
|
+
// Required: Give the Node a unique ID which is used to store the state of this node
|
|
2192
|
+
id: storeId,
|
|
2193
|
+
// Environment to run the server node in
|
|
2194
|
+
environment: this.environment,
|
|
2195
|
+
// Provide Network relevant configuration like the port
|
|
2196
|
+
network: {
|
|
2197
|
+
listeningAddressIpv4: this.ipv4Address,
|
|
2198
|
+
listeningAddressIpv6: this.ipv6Address,
|
|
2199
|
+
port,
|
|
2200
|
+
},
|
|
2201
|
+
// Provide the certificate for the device
|
|
2202
|
+
operationalCredentials: {
|
|
2203
|
+
certification: this.certification,
|
|
2204
|
+
},
|
|
2205
|
+
// Provide Commissioning relevant settings
|
|
2206
|
+
commissioning: {
|
|
2207
|
+
passcode,
|
|
2208
|
+
discriminator,
|
|
2209
|
+
},
|
|
2210
|
+
// Provide Node announcement settings
|
|
2211
|
+
productDescription: {
|
|
2212
|
+
name: await storageContext.get('deviceName'),
|
|
2213
|
+
deviceType: DeviceTypeId(await storageContext.get('deviceType')),
|
|
2214
|
+
},
|
|
2215
|
+
// Provide defaults for the BasicInformation cluster on the Root endpoint
|
|
2216
|
+
basicInformation: {
|
|
2217
|
+
nodeLabel: await storageContext.get('nodeLabel'),
|
|
2218
|
+
vendorId: VendorId(await storageContext.get('vendorId')),
|
|
2219
|
+
vendorName: await storageContext.get('vendorName'),
|
|
2220
|
+
productId: await storageContext.get('productId'),
|
|
2221
|
+
productName: await storageContext.get('productName'),
|
|
2222
|
+
productLabel: await storageContext.get('productName'),
|
|
2223
|
+
serialNumber: await storageContext.get('serialNumber'),
|
|
2224
|
+
uniqueId: await storageContext.get('uniqueId'),
|
|
2225
|
+
softwareVersion: await storageContext.get('softwareVersion'),
|
|
2226
|
+
softwareVersionString: await storageContext.get('softwareVersionString'),
|
|
2227
|
+
hardwareVersion: await storageContext.get('hardwareVersion'),
|
|
2228
|
+
hardwareVersionString: await storageContext.get('hardwareVersionString'),
|
|
2229
|
+
reachable: true,
|
|
2230
|
+
},
|
|
2231
|
+
});
|
|
2232
|
+
/**
|
|
2233
|
+
* This event is triggered when the device is initially commissioned successfully.
|
|
2234
|
+
* This means: It is added to the first fabric.
|
|
2235
|
+
*/
|
|
2236
|
+
serverNode.lifecycle.commissioned.on(() => {
|
|
2237
|
+
this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`);
|
|
2238
|
+
this.advertisingNodes.delete(storeId);
|
|
2239
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2240
|
+
});
|
|
2241
|
+
/** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
|
|
2242
|
+
serverNode.lifecycle.decommissioned.on(() => {
|
|
2243
|
+
this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`);
|
|
2244
|
+
this.advertisingNodes.delete(storeId);
|
|
2245
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2246
|
+
this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
|
|
2247
|
+
});
|
|
2248
|
+
/** This event is triggered when the device went online. This means that it is discoverable in the network. */
|
|
2249
|
+
serverNode.lifecycle.online.on(async () => {
|
|
2250
|
+
this.log.notice(`Server node for ${storeId} is online`);
|
|
2251
|
+
if (!serverNode.lifecycle.isCommissioned) {
|
|
2252
|
+
this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
|
|
2253
|
+
this.advertisingNodes.set(storeId, Date.now());
|
|
2254
|
+
const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
|
|
2255
|
+
this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
|
|
2256
|
+
this.log.notice(`Manual pairing code: ${manualPairingCode}`);
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
// istanbul ignore next
|
|
2260
|
+
this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
|
|
2261
|
+
// istanbul ignore next
|
|
2262
|
+
this.advertisingNodes.delete(storeId);
|
|
2263
|
+
}
|
|
2264
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2265
|
+
this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
|
|
2266
|
+
this.emit('online', storeId);
|
|
2267
|
+
});
|
|
2268
|
+
/** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
|
|
2269
|
+
serverNode.lifecycle.offline.on(() => {
|
|
2270
|
+
this.log.notice(`Server node for ${storeId} is offline`);
|
|
2271
|
+
this.advertisingNodes.delete(storeId);
|
|
2272
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2273
|
+
this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
|
|
2274
|
+
this.emit('offline', storeId);
|
|
2275
|
+
});
|
|
2276
|
+
/**
|
|
2277
|
+
* This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
|
|
2278
|
+
* information is needed.
|
|
2279
|
+
*/
|
|
2280
|
+
serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
|
|
2281
|
+
let action = '';
|
|
2282
|
+
switch (fabricAction) {
|
|
2283
|
+
case 'added':
|
|
2284
|
+
this.advertisingNodes.delete(storeId); // The advertising stops when a fabric is added
|
|
2285
|
+
action = 'added';
|
|
2286
|
+
break;
|
|
2287
|
+
case 'deleted':
|
|
2288
|
+
action = 'removed';
|
|
2289
|
+
break;
|
|
2290
|
+
case 'updated':
|
|
2291
|
+
action = 'updated';
|
|
2292
|
+
break;
|
|
2293
|
+
}
|
|
2294
|
+
this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
|
|
2295
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2296
|
+
});
|
|
2297
|
+
/**
|
|
2298
|
+
* This event is triggered when an operative new session was opened by a Controller.
|
|
2299
|
+
* It is not triggered for the initial commissioning process, just afterwards for real connections.
|
|
2300
|
+
*/
|
|
2301
|
+
serverNode.events.sessions.opened.on((session) => {
|
|
2302
|
+
this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
|
|
2303
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2304
|
+
});
|
|
2305
|
+
/**
|
|
2306
|
+
* This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
|
|
2307
|
+
*/
|
|
2308
|
+
serverNode.events.sessions.closed.on((session) => {
|
|
2309
|
+
this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
2310
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2311
|
+
});
|
|
2312
|
+
/** This event is triggered when a subscription gets added or removed on an operative session. */
|
|
2313
|
+
serverNode.events.sessions.subscriptionsChanged.on((session) => {
|
|
2314
|
+
this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
2315
|
+
this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
|
|
2316
|
+
});
|
|
2317
|
+
this.log.info(`Created server node for ${storeId}`);
|
|
2318
|
+
return serverNode;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Gets the matter sanitized data of the specified server node.
|
|
2322
|
+
*
|
|
2323
|
+
* @param {ServerNode} [serverNode] - The server node to start.
|
|
2324
|
+
* @returns {ApiMatter} The sanitized data of the server node.
|
|
2325
|
+
*/
|
|
2326
|
+
getServerNodeData(serverNode) {
|
|
2327
|
+
const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
|
|
2328
|
+
return {
|
|
2329
|
+
id: serverNode.id,
|
|
2330
|
+
online: serverNode.lifecycle.isOnline,
|
|
2331
|
+
commissioned: serverNode.state.commissioning.commissioned,
|
|
2332
|
+
advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
|
|
2333
|
+
advertiseTime,
|
|
2334
|
+
windowStatus: serverNode.state.administratorCommissioning.windowStatus,
|
|
2335
|
+
qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
|
|
2336
|
+
manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
|
|
2337
|
+
fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
|
|
2338
|
+
sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
|
|
2339
|
+
serialNumber: serverNode.state.basicInformation.serialNumber,
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Starts the specified server node.
|
|
2344
|
+
*
|
|
2345
|
+
* @param {ServerNode} [matterServerNode] - The server node to start.
|
|
2346
|
+
* @returns {Promise<void>} A promise that resolves when the server node has started.
|
|
2347
|
+
*/
|
|
2348
|
+
async startServerNode(matterServerNode) {
|
|
2349
|
+
if (!matterServerNode)
|
|
2350
|
+
return;
|
|
2351
|
+
this.log.notice(`Starting ${matterServerNode.id} server node`);
|
|
2352
|
+
await matterServerNode.start();
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Stops the specified server node.
|
|
2356
|
+
*
|
|
2357
|
+
* @param {ServerNode} matterServerNode - The server node to stop.
|
|
2358
|
+
* @param {number} [timeout] - The timeout in milliseconds for stopping the server node. Defaults to 10 seconds.
|
|
2359
|
+
* @returns {Promise<void>} A promise that resolves when the server node has stopped.
|
|
2360
|
+
*/
|
|
2361
|
+
async stopServerNode(matterServerNode, timeout = 10000) {
|
|
2362
|
+
const { withTimeout } = await import('@matterbridge/utils');
|
|
2363
|
+
if (!matterServerNode)
|
|
2364
|
+
return;
|
|
2365
|
+
this.log.notice(`Closing ${matterServerNode.id} server node`);
|
|
2366
|
+
try {
|
|
2367
|
+
await withTimeout(matterServerNode.close(), timeout);
|
|
2368
|
+
this.log.info(`Closed ${matterServerNode.id} server node`);
|
|
2369
|
+
}
|
|
2370
|
+
catch (error) {
|
|
2371
|
+
this.log.error(`Failed to close ${matterServerNode.id} server node: ${error instanceof Error ? error.message : error}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Creates an aggregator node with the specified storage context.
|
|
2376
|
+
*
|
|
2377
|
+
* @param {StorageContext} storageContext - The storage context for the aggregator node.
|
|
2378
|
+
* @returns {Promise<Endpoint<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
|
|
2379
|
+
*/
|
|
2380
|
+
async createAggregatorNode(storageContext) {
|
|
2381
|
+
this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator...`);
|
|
2382
|
+
const aggregatorNode = new Endpoint(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
|
|
2383
|
+
this.log.info(`Created ${await storageContext.get('storeId')} aggregator`);
|
|
2384
|
+
return aggregatorNode;
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Creates and configures the server node for an accessory plugin for a given device.
|
|
2388
|
+
*
|
|
2389
|
+
* @param {Plugin} plugin - The plugin to configure.
|
|
2390
|
+
* @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
|
|
2391
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
|
|
2392
|
+
*/
|
|
2393
|
+
async createAccessoryPlugin(plugin, device) {
|
|
2394
|
+
if (!plugin.locked && device.deviceType && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
|
|
2395
|
+
plugin.locked = true;
|
|
2396
|
+
plugin.device = device;
|
|
2397
|
+
this.log.debug(`Creating accessory plugin ${plg}${plugin.name}${db} server node...`);
|
|
2398
|
+
plugin.storageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
|
|
2399
|
+
plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
2400
|
+
this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node...`);
|
|
2401
|
+
await plugin.serverNode.add(device);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Creates and configures the server node and the aggregator node for a dynamic plugin.
|
|
2406
|
+
*
|
|
2407
|
+
* @param {Plugin} plugin - The plugin to configure.
|
|
2408
|
+
* @returns {Promise<void>} A promise that resolves when the server node and the aggregator node for the dynamic plugin is created and configured.
|
|
2409
|
+
*/
|
|
2410
|
+
async createDynamicPlugin(plugin) {
|
|
2411
|
+
if (!plugin.locked) {
|
|
2412
|
+
plugin.locked = true;
|
|
2413
|
+
this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
|
|
2414
|
+
plugin.storageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
|
|
2415
|
+
plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
2416
|
+
this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
|
|
2417
|
+
plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
|
|
2418
|
+
this.log.debug(`Adding dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
|
|
2419
|
+
await plugin.serverNode.add(plugin.aggregatorNode);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Creates and configures the server node for a single not bridged device.
|
|
2424
|
+
*
|
|
2425
|
+
* @param {Plugin} plugin - The plugin to configure.
|
|
2426
|
+
* @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
|
|
2427
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
|
|
2428
|
+
*/
|
|
2429
|
+
async createDeviceServerNode(plugin, device) {
|
|
2430
|
+
if (device.mode === 'server' &&
|
|
2431
|
+
!device.serverNode &&
|
|
2432
|
+
device.deviceType &&
|
|
2433
|
+
device.deviceName &&
|
|
2434
|
+
device.vendorId &&
|
|
2435
|
+
device.vendorName &&
|
|
2436
|
+
device.productId &&
|
|
2437
|
+
device.productName) {
|
|
2438
|
+
this.log.debug(`Creating device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node...`);
|
|
2439
|
+
const context = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
|
|
2440
|
+
device.serverNode = await this.createServerNode(context, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
2441
|
+
this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
|
|
2442
|
+
await device.serverNode.add(device);
|
|
2443
|
+
this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Adds a MatterbridgeEndpoint to the specified plugin.
|
|
2448
|
+
*
|
|
2449
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2450
|
+
* @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
|
|
2451
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been added.
|
|
2452
|
+
*/
|
|
2453
|
+
async addBridgedEndpoint(pluginName, device) {
|
|
2454
|
+
const { waiter } = await import('@matterbridge/utils');
|
|
2455
|
+
// Check if the plugin is registered
|
|
2456
|
+
const plugin = this.plugins.get(pluginName);
|
|
2457
|
+
if (!plugin) {
|
|
2458
|
+
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (device.mode === 'server') {
|
|
2462
|
+
try {
|
|
2463
|
+
this.log.debug(`Creating server node for device ${dev}${device.deviceName}${db} of plugin ${plg}${plugin.name}${db}...`);
|
|
2464
|
+
await this.createDeviceServerNode(plugin, device);
|
|
2465
|
+
}
|
|
2466
|
+
catch (error) {
|
|
2467
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2468
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2469
|
+
this.log.error(`Error creating server node for device ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) of plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
else if (this.bridgeMode === 'bridge') {
|
|
2474
|
+
if (device.mode === 'matter') {
|
|
2475
|
+
// Register and add the device to the matterbridge server node
|
|
2476
|
+
this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge server node...`);
|
|
2477
|
+
if (!this.serverNode) {
|
|
2478
|
+
this.log.error('Server node not found for Matterbridge');
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
try {
|
|
2482
|
+
await this.serverNode.add(device);
|
|
2483
|
+
}
|
|
2484
|
+
catch (error) {
|
|
2485
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2486
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2487
|
+
this.log.error(`Error adding matter endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
else {
|
|
2492
|
+
// Register and add the device to the matterbridge aggregator node
|
|
2493
|
+
this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
|
|
2494
|
+
if (!this.aggregatorNode) {
|
|
2495
|
+
this.log.error('Aggregator node not found for Matterbridge');
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
try {
|
|
2499
|
+
await this.aggregatorNode.add(device);
|
|
2500
|
+
}
|
|
2501
|
+
catch (error) {
|
|
2502
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2503
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2504
|
+
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
else if (this.bridgeMode === 'childbridge') {
|
|
2510
|
+
// Register and add the device to the plugin server node
|
|
2511
|
+
if (plugin.type === 'AccessoryPlatform') {
|
|
2512
|
+
try {
|
|
2513
|
+
this.log.debug(`Creating endpoint ${dev}${device.deviceName}${db} for AccessoryPlatform plugin ${plg}${plugin.name}${db} server node`);
|
|
2514
|
+
if (plugin.serverNode) {
|
|
2515
|
+
if (device.mode === 'matter') {
|
|
2516
|
+
await plugin.serverNode.add(device);
|
|
2517
|
+
}
|
|
2518
|
+
else {
|
|
2519
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} has already added a device. Only one device is allowed per AccessoryPlatform plugin.`);
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
else {
|
|
2524
|
+
await this.createAccessoryPlugin(plugin, device);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
catch (error) {
|
|
2528
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2529
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2530
|
+
this.log.error(`Error creating endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for AccessoryPlatform plugin ${plg}${pluginName}${er} server node: ${errorMessage}\nstack: ${errorInspect}`);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
// Register and add the device to the plugin aggregator node
|
|
2535
|
+
if (plugin.type === 'DynamicPlatform') {
|
|
2536
|
+
try {
|
|
2537
|
+
this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
|
|
2538
|
+
await this.createDynamicPlugin(plugin);
|
|
2539
|
+
// Fast plugins can add another device before the server node is ready, so we wait for the server node to be ready
|
|
2540
|
+
await waiter(`createDynamicPlugin(${plugin.name})`, () => plugin.serverNode?.hasParts === true);
|
|
2541
|
+
if (!plugin.aggregatorNode) {
|
|
2542
|
+
this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
if (device.mode === 'matter')
|
|
2546
|
+
await plugin.serverNode?.add(device);
|
|
2547
|
+
else
|
|
2548
|
+
await plugin.aggregatorNode.add(device);
|
|
2549
|
+
}
|
|
2550
|
+
catch (error) {
|
|
2551
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2552
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2553
|
+
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for DynamicPlatform plugin ${plg}${pluginName}${er} aggregator node: ${errorMessage}\nstack: ${errorInspect}`);
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (plugin.registeredDevices !== undefined)
|
|
2559
|
+
plugin.registeredDevices++;
|
|
2560
|
+
// Add the device to the DeviceManager
|
|
2561
|
+
this.devices.set(device);
|
|
2562
|
+
// Subscribe to the attributes changed event
|
|
2563
|
+
await this.subscribeAttributeChanged(plugin, device);
|
|
2564
|
+
this.log.info(`Added and registered bridged endpoint (${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) for plugin ${plg}${pluginName}${nf}`);
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Removes a MatterbridgeEndpoint from the specified plugin.
|
|
2568
|
+
*
|
|
2569
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2570
|
+
* @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
|
|
2571
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been removed.
|
|
2572
|
+
*/
|
|
2573
|
+
async removeBridgedEndpoint(pluginName, device) {
|
|
2574
|
+
this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
|
|
2575
|
+
// Check if the plugin is registered
|
|
2576
|
+
const plugin = this.plugins.get(pluginName);
|
|
2577
|
+
if (!plugin) {
|
|
2578
|
+
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (device.mode === 'server') {
|
|
2582
|
+
this.log.info(`Removed mode server bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
|
|
2583
|
+
}
|
|
2584
|
+
else if (this.bridgeMode === 'bridge') {
|
|
2585
|
+
// Unregister and remove the device from the matterbridge aggregator node
|
|
2586
|
+
if (!this.aggregatorNode) {
|
|
2587
|
+
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
await device.delete();
|
|
2591
|
+
this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
|
|
2592
|
+
if (plugin.registeredDevices !== undefined)
|
|
2593
|
+
plugin.registeredDevices--;
|
|
2594
|
+
}
|
|
2595
|
+
else if (this.bridgeMode === 'childbridge') {
|
|
2596
|
+
if (plugin.type === 'AccessoryPlatform') {
|
|
2597
|
+
// Nothing to do here since the server node has no aggregator node but only the device itself
|
|
2598
|
+
}
|
|
2599
|
+
else if (plugin.type === 'DynamicPlatform') {
|
|
2600
|
+
// Unregister and remove the device from the plugin aggregator node
|
|
2601
|
+
if (!plugin.aggregatorNode) {
|
|
2602
|
+
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
await device.delete();
|
|
2606
|
+
}
|
|
2607
|
+
this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
|
|
2608
|
+
if (plugin.registeredDevices !== undefined)
|
|
2609
|
+
plugin.registeredDevices--;
|
|
2610
|
+
}
|
|
2611
|
+
// Remove the device from the DeviceManager
|
|
2612
|
+
this.devices.remove(device);
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Removes all bridged endpoints from the specified plugin.
|
|
2616
|
+
*
|
|
2617
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2618
|
+
* @param {number} [delay] - The delay in milliseconds between removing each bridged endpoint (default: 0).
|
|
2619
|
+
* @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
|
|
2620
|
+
*
|
|
2621
|
+
* @remarks
|
|
2622
|
+
* This method iterates through all devices in the DeviceManager and removes each bridged endpoint associated with the specified plugin.
|
|
2623
|
+
* It also applies a delay between each removal if specified.
|
|
2624
|
+
* The delay is useful to allow the controllers to receive a single subscription for each device removed.
|
|
2625
|
+
*/
|
|
2626
|
+
async removeAllBridgedEndpoints(pluginName, delay = 0) {
|
|
2627
|
+
const { wait } = await import('@matterbridge/utils');
|
|
2628
|
+
this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}`);
|
|
2629
|
+
for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
|
|
2630
|
+
await this.removeBridgedEndpoint(pluginName, device);
|
|
2631
|
+
if (delay > 0)
|
|
2632
|
+
await wait(delay);
|
|
2633
|
+
}
|
|
2634
|
+
if (delay > 0)
|
|
2635
|
+
await wait(2000);
|
|
2636
|
+
}
|
|
2637
|
+
/**
|
|
2638
|
+
* Registers a virtual device.
|
|
2639
|
+
* Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform.
|
|
2640
|
+
*
|
|
2641
|
+
* The virtual device is created as an instance of `Endpoint` with the provided device type.
|
|
2642
|
+
* When the virtual device is turned on, the provided callback function is executed.
|
|
2643
|
+
* The onOff state of the virtual device always reverts to false when the device is turned on.
|
|
2644
|
+
*
|
|
2645
|
+
* @param { string } pluginName - The name of the plugin to register the virtual device under.
|
|
2646
|
+
* @param { string } name - The name of the virtual device.
|
|
2647
|
+
* @param { 'light' | 'outlet' | 'switch' | 'mounted_switch' } type - The type of the virtual device.
|
|
2648
|
+
* @param { () => Promise<void> } callback - The callback to call when the virtual device is turned on.
|
|
2649
|
+
*
|
|
2650
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the virtual device was successfully registered, false otherwise.
|
|
2651
|
+
*
|
|
2652
|
+
* @remarks
|
|
2653
|
+
* The virtual devices don't show up in the device list of the frontend.
|
|
2654
|
+
* Type 'switch' is not supported by Alexa and 'mounted_switch' is not supported by Apple Home.
|
|
2655
|
+
*/
|
|
2656
|
+
async addVirtualEndpoint(pluginName, name, type, callback) {
|
|
2657
|
+
this.log.debug(`Adding virtual endpoint ${plg}${pluginName}${db}:${dev}${name}${db}...`);
|
|
2658
|
+
// Check if the plugin is registered
|
|
2659
|
+
const plugin = this.plugins.get(pluginName);
|
|
2660
|
+
if (!plugin) {
|
|
2661
|
+
this.log.error(`Error adding virtual endpoint ${dev}${name}${er} for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
let aggregator;
|
|
2665
|
+
if (this.bridgeMode === 'bridge') {
|
|
2666
|
+
aggregator = this.aggregatorNode;
|
|
2667
|
+
}
|
|
2668
|
+
else if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') {
|
|
2669
|
+
aggregator = plugin.aggregatorNode;
|
|
2670
|
+
}
|
|
2671
|
+
if (aggregator) {
|
|
2672
|
+
if (aggregator.parts.has(name.replaceAll(' ', '') + ':' + type)) {
|
|
2673
|
+
this.log.error(`Virtual endpoint ${dev}${name}${er} already registered for plugin ${plg}${pluginName}${er}. Please use a different name.`);
|
|
2674
|
+
return false;
|
|
2675
|
+
}
|
|
2676
|
+
else {
|
|
2677
|
+
await addVirtualDevice(aggregator, name.slice(0, 32), type, callback);
|
|
2678
|
+
this.log.info(`Created virtual endpoint ${dev}${name}${nf} for plugin ${plg}${pluginName}${nf}`);
|
|
2679
|
+
return true;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
this.log.error(`Virtual endpoint ${dev}${name}${er} for plugin ${plg}${pluginName}${er} not created. Virtual endpoints are only supported in bridge mode and childbridge mode with a DynamicPlatform.`);
|
|
2683
|
+
return false;
|
|
2684
|
+
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Subscribes to the attribute change event for the given device and plugin.
|
|
2687
|
+
* Specifically, it listens for changes in the 'reachable' attribute of the
|
|
2688
|
+
* BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
|
|
2689
|
+
*
|
|
2690
|
+
* @param {Plugin} plugin - The plugin associated with the device.
|
|
2691
|
+
* @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
|
|
2692
|
+
* @returns {Promise<void>} A promise that resolves when the subscription is set up.
|
|
2693
|
+
*/
|
|
2694
|
+
async subscribeAttributeChanged(plugin, device) {
|
|
2695
|
+
if (!plugin || !device || !device.plugin || !device.serialNumber || !device.uniqueId || !device.maybeNumber)
|
|
2696
|
+
return;
|
|
2697
|
+
this.log.info(`Subscribing attributes for endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) plugin ${plg}${plugin.name}${nf}`);
|
|
2698
|
+
// Subscribe to the reachable$Changed event of the BasicInformationServer cluster server of the server node in childbridge mode
|
|
2699
|
+
if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && plugin.serverNode) {
|
|
2700
|
+
plugin.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
|
|
2701
|
+
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) is ${reachable ? 'reachable' : 'unreachable'}`);
|
|
2702
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2703
|
+
this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, device.number, device.id, 'BasicInformation', 'reachable', reachable);
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
const subscriptions = [
|
|
2707
|
+
{ cluster: 'BridgedDeviceBasicInformation', attribute: 'reachable' },
|
|
2708
|
+
{ cluster: 'OnOff', attribute: 'onOff' },
|
|
2709
|
+
{ cluster: 'LevelControl', attribute: 'currentLevel' },
|
|
2710
|
+
{ cluster: 'ColorControl', attribute: 'colorMode' },
|
|
2711
|
+
{ cluster: 'ColorControl', attribute: 'currentHue' },
|
|
2712
|
+
{ cluster: 'ColorControl', attribute: 'currentSaturation' },
|
|
2713
|
+
{ cluster: 'ColorControl', attribute: 'currentX' },
|
|
2714
|
+
{ cluster: 'ColorControl', attribute: 'currentY' },
|
|
2715
|
+
{ cluster: 'ColorControl', attribute: 'colorTemperatureMireds' },
|
|
2716
|
+
{ cluster: 'Thermostat', attribute: 'localTemperature' },
|
|
2717
|
+
{ cluster: 'Thermostat', attribute: 'occupiedCoolingSetpoint' },
|
|
2718
|
+
{ cluster: 'Thermostat', attribute: 'occupiedHeatingSetpoint' },
|
|
2719
|
+
{ cluster: 'Thermostat', attribute: 'systemMode' },
|
|
2720
|
+
{ cluster: 'Thermostat', attribute: 'thermostatRunningMode' },
|
|
2721
|
+
{ cluster: 'Thermostat', attribute: 'thermostatRunningState' },
|
|
2722
|
+
{ cluster: 'WindowCovering', attribute: 'operationalStatus' },
|
|
2723
|
+
{ cluster: 'WindowCovering', attribute: 'currentPositionLiftPercent100ths' },
|
|
2724
|
+
{ cluster: 'DoorLock', attribute: 'lockState' },
|
|
2725
|
+
{ cluster: 'PumpConfigurationAndControl', attribute: 'pumpStatus' },
|
|
2726
|
+
{ cluster: 'FanControl', attribute: 'fanMode' },
|
|
2727
|
+
{ cluster: 'FanControl', attribute: 'fanModeSequence' },
|
|
2728
|
+
{ cluster: 'FanControl', attribute: 'percentSetting' },
|
|
2729
|
+
{ cluster: 'ModeSelect', attribute: 'currentMode' },
|
|
2730
|
+
{ cluster: 'RvcRunMode', attribute: 'currentMode' },
|
|
2731
|
+
{ cluster: 'RvcCleanMode', attribute: 'currentMode' },
|
|
2732
|
+
{ cluster: 'RvcOperationalState', attribute: 'operationalState' },
|
|
2733
|
+
{ cluster: 'RvcOperationalState', attribute: 'operationalError' },
|
|
2734
|
+
{ cluster: 'ServiceArea', attribute: 'currentArea' },
|
|
2735
|
+
{ cluster: 'AirQuality', attribute: 'airQuality' },
|
|
2736
|
+
{ cluster: 'TotalVolatileOrganicCompoundsConcentrationMeasurement', attribute: 'measuredValue' },
|
|
2737
|
+
{ cluster: 'BooleanState', attribute: 'stateValue' },
|
|
2738
|
+
{ cluster: 'OccupancySensing', attribute: 'occupancy' },
|
|
2739
|
+
{ cluster: 'IlluminanceMeasurement', attribute: 'measuredValue' },
|
|
2740
|
+
{ cluster: 'TemperatureMeasurement', attribute: 'measuredValue' },
|
|
2741
|
+
{ cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue' },
|
|
2742
|
+
{ cluster: 'PressureMeasurement', attribute: 'measuredValue' },
|
|
2743
|
+
{ cluster: 'FlowMeasurement', attribute: 'measuredValue' },
|
|
2744
|
+
{ cluster: 'SmokeCoAlarm', attribute: 'smokeState' },
|
|
2745
|
+
{ cluster: 'SmokeCoAlarm', attribute: 'coState' },
|
|
2746
|
+
];
|
|
2747
|
+
for (const sub of subscriptions) {
|
|
2748
|
+
if (device.hasAttributeServer(sub.cluster, sub.attribute)) {
|
|
2749
|
+
this.log.debug(`Subscribing to endpoint ${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
|
|
2750
|
+
await device.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
|
|
2751
|
+
this.log.debug(`Bridged endpoint ${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${isValidObject(value) ? debugStringify(value) : value}${db}`);
|
|
2752
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2753
|
+
this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, device.number, device.id, sub.cluster, sub.attribute, value);
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
for (const child of device.getChildEndpoints()) {
|
|
2757
|
+
if (child.hasAttributeServer(sub.cluster, sub.attribute)) {
|
|
2758
|
+
this.log.debug(`Subscribing to child endpoint ${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
|
|
2759
|
+
await child.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
|
|
2760
|
+
this.log.debug(`Bridged child endpoint ${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${isValidObject(value) ? debugStringify(value) : value}${db}`);
|
|
2761
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
2762
|
+
this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, child.number, child.id, sub.cluster, sub.attribute, value);
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2770
|
+
*
|
|
2771
|
+
* @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
|
|
2772
|
+
* @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
|
|
2773
|
+
*/
|
|
2774
|
+
sanitizeFabricInformations(fabricInfo) {
|
|
2775
|
+
return fabricInfo.map((info) => {
|
|
2776
|
+
return {
|
|
2777
|
+
fabricIndex: info.fabricIndex,
|
|
2778
|
+
fabricId: info.fabricId.toString(),
|
|
2779
|
+
nodeId: info.nodeId.toString(),
|
|
2780
|
+
rootNodeId: info.rootNodeId.toString(),
|
|
2781
|
+
rootVendorId: info.rootVendorId,
|
|
2782
|
+
rootVendorName: this.getVendorIdName(info.rootVendorId),
|
|
2783
|
+
label: info.label,
|
|
2784
|
+
};
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
/**
|
|
2788
|
+
* Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2789
|
+
*
|
|
2790
|
+
* @param {SessionsBehavior.Session[]} sessions - The array of session information objects.
|
|
2791
|
+
* @returns {SanitizedSession[]} An array of sanitized session information objects.
|
|
2792
|
+
*/
|
|
2793
|
+
sanitizeSessionInformation(sessions) {
|
|
2794
|
+
return sessions
|
|
2795
|
+
.filter((session) => session.isPeerActive)
|
|
2796
|
+
.map((session) => {
|
|
2797
|
+
return {
|
|
2798
|
+
name: session.name,
|
|
2799
|
+
nodeId: session.nodeId.toString(),
|
|
2800
|
+
peerNodeId: session.peerNodeId.toString(),
|
|
2801
|
+
fabric: session.fabric
|
|
2802
|
+
? {
|
|
2803
|
+
fabricIndex: session.fabric.fabricIndex,
|
|
2804
|
+
fabricId: session.fabric.fabricId.toString(),
|
|
2805
|
+
nodeId: session.fabric.nodeId.toString(),
|
|
2806
|
+
rootNodeId: session.fabric.rootNodeId.toString(),
|
|
2807
|
+
rootVendorId: session.fabric.rootVendorId,
|
|
2808
|
+
rootVendorName: this.getVendorIdName(session.fabric.rootVendorId),
|
|
2809
|
+
label: session.fabric.label,
|
|
2810
|
+
}
|
|
2811
|
+
: undefined,
|
|
2812
|
+
isPeerActive: session.isPeerActive,
|
|
2813
|
+
lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
|
|
2814
|
+
lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
|
|
2815
|
+
numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
|
|
2816
|
+
};
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Sets the reachability of the specified aggregator node bridged devices and trigger.
|
|
2821
|
+
*
|
|
2822
|
+
* @param {Endpoint<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
|
|
2823
|
+
* @param {boolean} reachable - A boolean indicating the reachability status to set.
|
|
2824
|
+
*/
|
|
2825
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2826
|
+
async setAggregatorReachability(aggregatorNode, reachable) {
|
|
2827
|
+
/*
|
|
2828
|
+
for (const child of aggregatorNode.parts) {
|
|
2829
|
+
this.log.debug(`Setting reachability of ${(child as unknown as MatterbridgeEndpoint)?.deviceName} to ${reachable}`);
|
|
2830
|
+
await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
|
|
2831
|
+
child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
|
|
2832
|
+
}
|
|
2833
|
+
*/
|
|
2834
|
+
}
|
|
2835
|
+
getVendorIdName = (vendorId) => {
|
|
2836
|
+
if (!vendorId)
|
|
2837
|
+
return '';
|
|
2838
|
+
let vendorName = '(Unknown vendorId)';
|
|
2839
|
+
switch (vendorId) {
|
|
2840
|
+
case 4937:
|
|
2841
|
+
vendorName = '(AppleHome)';
|
|
2842
|
+
break;
|
|
2843
|
+
case 4996:
|
|
2844
|
+
vendorName = '(AppleKeyChain)';
|
|
2845
|
+
break;
|
|
2846
|
+
case 4362:
|
|
2847
|
+
vendorName = '(SmartThings)';
|
|
2848
|
+
break;
|
|
2849
|
+
case 4939:
|
|
2850
|
+
vendorName = '(HomeAssistant)';
|
|
2851
|
+
break;
|
|
2852
|
+
case 24582:
|
|
2853
|
+
vendorName = '(GoogleHome)';
|
|
2854
|
+
break;
|
|
2855
|
+
case 4631:
|
|
2856
|
+
vendorName = '(Alexa)';
|
|
2857
|
+
break;
|
|
2858
|
+
case 4701:
|
|
2859
|
+
vendorName = '(Tuya)';
|
|
2860
|
+
break;
|
|
2861
|
+
case 4718:
|
|
2862
|
+
vendorName = '(Xiaomi)';
|
|
2863
|
+
break;
|
|
2864
|
+
case 4742:
|
|
2865
|
+
vendorName = '(eWeLink)';
|
|
2866
|
+
break;
|
|
2867
|
+
case 5264:
|
|
2868
|
+
vendorName = '(Shelly)';
|
|
2869
|
+
break;
|
|
2870
|
+
case 0x1488:
|
|
2871
|
+
vendorName = '(ShortcutLabsFlic)';
|
|
2872
|
+
break;
|
|
2873
|
+
case 65521: // 0xFFF1
|
|
2874
|
+
vendorName = '(MatterTest)';
|
|
2875
|
+
break;
|
|
2876
|
+
}
|
|
2877
|
+
return vendorName;
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
//# sourceMappingURL=matterbridge.js.map
|