matterbridge 3.0.0-edge.8 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -9
- package/README-DEV.md +4 -0
- package/README-DOCKER.md +21 -12
- package/README-SERVICE.md +27 -21
- package/README.md +80 -2
- package/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +41 -3
- package/dist/cli.js.map +1 -0
- package/dist/cluster/export.d.ts +2 -0
- package/dist/cluster/export.d.ts.map +1 -0
- package/dist/cluster/export.js +2 -0
- package/dist/cluster/export.js.map +1 -0
- package/dist/defaultConfigSchema.d.ts +27 -0
- package/dist/defaultConfigSchema.d.ts.map +1 -0
- package/dist/defaultConfigSchema.js +23 -0
- package/dist/defaultConfigSchema.js.map +1 -0
- package/dist/deviceManager.d.ts +114 -0
- package/dist/deviceManager.d.ts.map +1 -0
- package/dist/deviceManager.js +94 -1
- package/dist/deviceManager.js.map +1 -0
- package/dist/frontend.d.ts +222 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +443 -35
- package/dist/frontend.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -0
- package/dist/logger/export.d.ts +2 -0
- package/dist/logger/export.d.ts.map +1 -0
- package/dist/logger/export.js +1 -0
- package/dist/logger/export.js.map +1 -0
- package/dist/matter/behaviors.d.ts +2 -0
- package/dist/matter/behaviors.d.ts.map +1 -0
- package/dist/matter/behaviors.js +2 -0
- package/dist/matter/behaviors.js.map +1 -0
- package/dist/matter/clusters.d.ts +2 -0
- package/dist/matter/clusters.d.ts.map +1 -0
- package/dist/matter/clusters.js +2 -0
- package/dist/matter/clusters.js.map +1 -0
- package/dist/matter/devices.d.ts +2 -0
- package/dist/matter/devices.d.ts.map +1 -0
- package/dist/matter/devices.js +2 -0
- package/dist/matter/devices.js.map +1 -0
- package/dist/matter/endpoints.d.ts +2 -0
- package/dist/matter/endpoints.d.ts.map +1 -0
- package/dist/matter/endpoints.js +2 -0
- package/dist/matter/endpoints.js.map +1 -0
- package/dist/matter/export.d.ts +5 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +2 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/types.d.ts +3 -0
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/types.js +2 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matterbridge.d.ts +431 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +800 -79
- package/dist/matterbridge.js.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts +40 -0
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
- package/dist/matterbridgeAccessoryPlatform.js +34 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
- package/dist/matterbridgeBehaviors.d.ts +1514 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +33 -1
- package/dist/matterbridgeBehaviors.js.map +1 -0
- package/dist/matterbridgeDeviceTypes.d.ts +494 -0
- package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
- package/dist/matterbridgeDeviceTypes.js +431 -12
- package/dist/matterbridgeDeviceTypes.js.map +1 -0
- package/dist/matterbridgeDynamicPlatform.d.ts +40 -0
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
- package/dist/matterbridgeDynamicPlatform.js +34 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -0
- package/dist/matterbridgeEndpoint.d.ts +943 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +806 -7
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2706 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +156 -9
- package/dist/matterbridgeEndpointHelpers.js.map +1 -0
- package/dist/matterbridgePlatform.d.ts +294 -0
- package/dist/matterbridgePlatform.d.ts.map +1 -0
- package/dist/matterbridgePlatform.js +227 -9
- package/dist/matterbridgePlatform.js.map +1 -0
- package/dist/matterbridgeTypes.d.ts +187 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +24 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/pluginManager.d.ts +271 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +262 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/shelly.d.ts +92 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +146 -6
- package/dist/shelly.js.map +1 -0
- package/dist/storage/export.d.ts +2 -0
- package/dist/storage/export.d.ts.map +1 -0
- package/dist/storage/export.js +1 -0
- package/dist/storage/export.js.map +1 -0
- package/dist/update.d.ts +32 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +70 -0
- package/dist/update.js.map +1 -0
- package/dist/utils/colorUtils.d.ts +61 -0
- package/dist/utils/colorUtils.d.ts.map +1 -0
- package/dist/utils/colorUtils.js +205 -2
- package/dist/utils/colorUtils.js.map +1 -0
- package/dist/utils/copyDirectory.d.ts +32 -0
- package/dist/utils/copyDirectory.d.ts.map +1 -0
- package/dist/utils/copyDirectory.js +37 -1
- package/dist/utils/copyDirectory.js.map +1 -0
- package/dist/utils/createZip.d.ts +38 -0
- package/dist/utils/createZip.d.ts.map +1 -0
- package/dist/utils/createZip.js +42 -2
- package/dist/utils/createZip.js.map +1 -0
- package/dist/utils/deepCopy.d.ts +31 -0
- package/dist/utils/deepCopy.d.ts.map +1 -0
- package/dist/utils/deepCopy.js +40 -0
- package/dist/utils/deepCopy.js.map +1 -0
- package/dist/utils/deepEqual.d.ts +53 -0
- package/dist/utils/deepEqual.d.ts.map +1 -0
- package/dist/utils/deepEqual.js +65 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +10 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +1 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/utils/isvalid.d.ts +87 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +86 -0
- package/dist/utils/isvalid.js.map +1 -0
- package/dist/utils/network.d.ts +69 -0
- package/dist/utils/network.d.ts.map +1 -0
- package/dist/utils/network.js +76 -5
- package/dist/utils/network.js.map +1 -0
- package/dist/utils/parameter.d.ts +58 -0
- package/dist/utils/parameter.d.ts.map +1 -0
- package/dist/utils/parameter.js +92 -7
- package/dist/utils/parameter.js.map +1 -0
- package/dist/utils/wait.d.ts +43 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +50 -5
- package/dist/utils/wait.js.map +1 -0
- package/frontend/build/asset-manifest.json +6 -6
- package/frontend/build/bmc-button.svg +22 -0
- package/frontend/build/discord.svg +5 -0
- package/frontend/build/index.html +1 -1
- package/frontend/build/static/css/{main.ea7910e9.css → main.944b63c3.css} +2 -2
- package/frontend/build/static/css/main.944b63c3.css.map +1 -0
- package/frontend/build/static/js/{main.e11d6bb4.js → main.1d983660.js} +12 -12
- package/frontend/build/static/js/{main.e11d6bb4.js.map → main.1d983660.js.map} +1 -1
- package/npm-shrinkwrap.json +300 -362
- package/package.json +5 -4
- package/tsconfig.jest.json +8 -0
- package/README-EDGE.md +0 -74
- package/frontend/build/static/css/main.ea7910e9.css.map +0 -1
- /package/frontend/build/static/js/{main.e11d6bb4.js.LICENSE.txt → main.1d983660.js.LICENSE.txt} +0 -0
package/dist/matterbridge.js
CHANGED
|
@@ -1,25 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the class Matterbridge.
|
|
3
|
+
*
|
|
4
|
+
* @file matterbridge.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @date 2023-12-29
|
|
7
|
+
* @version 1.5.2
|
|
8
|
+
*
|
|
9
|
+
* Copyright 2023, 2024, 2025 Luca Liguori.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License. *
|
|
22
|
+
*/
|
|
23
|
+
// Node.js modules
|
|
1
24
|
import os from 'node:os';
|
|
2
25
|
import path from 'node:path';
|
|
3
26
|
import { promises as fs } from 'node:fs';
|
|
4
27
|
import EventEmitter from 'node:events';
|
|
5
28
|
import { inspect } from 'node:util';
|
|
29
|
+
// AnsiLogger module
|
|
6
30
|
import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from './logger/export.js';
|
|
31
|
+
// NodeStorage module
|
|
7
32
|
import { NodeStorageManager } from './storage/export.js';
|
|
8
|
-
|
|
33
|
+
// Matterbridge
|
|
34
|
+
import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout, waiter } from './utils/export.js';
|
|
9
35
|
import { logInterfaces, getGlobalNodeModules } from './utils/network.js';
|
|
10
36
|
import { PluginManager } from './pluginManager.js';
|
|
11
37
|
import { DeviceManager } from './deviceManager.js';
|
|
12
38
|
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
13
39
|
import { bridge } from './matterbridgeDeviceTypes.js';
|
|
14
40
|
import { Frontend } from './frontend.js';
|
|
41
|
+
// @matter
|
|
15
42
|
import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode } from '@matter/main';
|
|
16
43
|
import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol';
|
|
17
44
|
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
18
45
|
import { BasicInformationServer } from '@matter/main/behaviors/basic-information';
|
|
19
46
|
import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information';
|
|
47
|
+
// Default colors
|
|
20
48
|
const plg = '\u001B[38;5;33m';
|
|
21
49
|
const dev = '\u001B[38;5;79m';
|
|
22
50
|
const typ = '\u001B[38;5;207m';
|
|
51
|
+
/**
|
|
52
|
+
* Represents the Matterbridge application.
|
|
53
|
+
*/
|
|
23
54
|
export class Matterbridge extends EventEmitter {
|
|
24
55
|
systemInformation = {
|
|
25
56
|
interfaceName: '',
|
|
@@ -51,6 +82,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
51
82
|
globalModulesDirectory: '',
|
|
52
83
|
matterbridgeVersion: '',
|
|
53
84
|
matterbridgeLatestVersion: '',
|
|
85
|
+
matterbridgeDevVersion: '',
|
|
86
|
+
matterbridgeSerialNumber: '',
|
|
54
87
|
matterbridgeQrPairingCode: undefined,
|
|
55
88
|
matterbridgeManualPairingCode: undefined,
|
|
56
89
|
matterbridgeFabricInformations: [],
|
|
@@ -64,7 +97,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
64
97
|
shellySysUpdate: false,
|
|
65
98
|
shellyMainUpdate: false,
|
|
66
99
|
profile: getParameter('profile'),
|
|
67
|
-
loggerLevel: "info"
|
|
100
|
+
loggerLevel: "info" /* LogLevel.INFO */,
|
|
68
101
|
fileLogger: false,
|
|
69
102
|
matterLoggerLevel: MatterLogLevel.INFO,
|
|
70
103
|
matterFileLogger: false,
|
|
@@ -85,6 +118,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
85
118
|
globalModulesDirectory = '';
|
|
86
119
|
matterbridgeVersion = '';
|
|
87
120
|
matterbridgeLatestVersion = '';
|
|
121
|
+
matterbridgeDevVersion = '';
|
|
88
122
|
matterbridgeQrPairingCode = undefined;
|
|
89
123
|
matterbridgeManualPairingCode = undefined;
|
|
90
124
|
matterbridgeFabricInformations = undefined;
|
|
@@ -102,9 +136,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
102
136
|
plugins;
|
|
103
137
|
devices;
|
|
104
138
|
frontend = new Frontend(this);
|
|
139
|
+
// Matterbridge storage
|
|
105
140
|
nodeStorage;
|
|
106
141
|
nodeContext;
|
|
107
142
|
nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
|
|
143
|
+
// Cleanup
|
|
108
144
|
hasCleanupStarted = false;
|
|
109
145
|
initialized = false;
|
|
110
146
|
execRunningCount = 0;
|
|
@@ -117,18 +153,21 @@ export class Matterbridge extends EventEmitter {
|
|
|
117
153
|
sigtermHandler;
|
|
118
154
|
exceptionHandler;
|
|
119
155
|
rejectionHandler;
|
|
156
|
+
// Matter environment
|
|
120
157
|
environment = Environment.default;
|
|
158
|
+
// Matter storage
|
|
121
159
|
matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
|
|
122
160
|
matterStorageService;
|
|
123
161
|
matterStorageManager;
|
|
124
162
|
matterbridgeContext;
|
|
125
163
|
controllerContext;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
164
|
+
// Matter parameters
|
|
165
|
+
mdnsInterface; // matter server node mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
|
|
166
|
+
ipv4address; // matter server node listeningAddressIpv4
|
|
167
|
+
ipv6address; // matter server node listeningAddressIpv6
|
|
168
|
+
port; // first server node port
|
|
169
|
+
passcode; // first server node passcode
|
|
170
|
+
discriminator; // first server node discriminator
|
|
132
171
|
serverNode;
|
|
133
172
|
aggregatorNode;
|
|
134
173
|
aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
|
|
@@ -136,21 +175,50 @@ export class Matterbridge extends EventEmitter {
|
|
|
136
175
|
aggregatorProductId = getIntParameter('productId') ?? 0x8000;
|
|
137
176
|
aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
|
|
138
177
|
static instance;
|
|
178
|
+
// We load asyncronously so is private
|
|
139
179
|
constructor() {
|
|
140
180
|
super();
|
|
141
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Emits an event of the specified type with the provided arguments.
|
|
184
|
+
*
|
|
185
|
+
* @template K - The type of the event.
|
|
186
|
+
* @param {K} eventName - The name of the event to emit.
|
|
187
|
+
* @param {...MatterbridgeEvent[K]} args - The arguments to pass to the event listeners.
|
|
188
|
+
* @returns {boolean} - Returns true if the event had listeners, false otherwise.
|
|
189
|
+
*/
|
|
142
190
|
emit(eventName, ...args) {
|
|
143
191
|
return super.emit(eventName, ...args);
|
|
144
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Registers an event listener for the specified event type.
|
|
195
|
+
*
|
|
196
|
+
* @template K - The type of the event.
|
|
197
|
+
* @param {K} eventName - The name of the event to listen for.
|
|
198
|
+
* @param {(...args: MatterbridgeEvent[K]) => void} listener - The callback function to invoke when the event is emitted.
|
|
199
|
+
* @returns {this} - Returns the instance of the Matterbridge class.
|
|
200
|
+
*/
|
|
145
201
|
on(eventName, listener) {
|
|
146
202
|
return super.on(eventName, listener);
|
|
147
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Retrieves the list of Matterbridge devices.
|
|
206
|
+
* @returns {MatterbridgeEndpoint[]} An array of MatterbridgeDevice objects.
|
|
207
|
+
*/
|
|
148
208
|
getDevices() {
|
|
149
209
|
return this.devices.array();
|
|
150
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Retrieves the list of registered plugins.
|
|
213
|
+
* @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects.
|
|
214
|
+
*/
|
|
151
215
|
getPlugins() {
|
|
152
216
|
return this.plugins.array();
|
|
153
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Set the logger logLevel for the Matterbridge classes.
|
|
220
|
+
* @param {LogLevel} logLevel The logger logLevel to set.
|
|
221
|
+
*/
|
|
154
222
|
async setLogLevel(logLevel) {
|
|
155
223
|
if (this.log)
|
|
156
224
|
this.log.logLevel = logLevel;
|
|
@@ -164,19 +232,31 @@ export class Matterbridge extends EventEmitter {
|
|
|
164
232
|
for (const plugin of this.plugins) {
|
|
165
233
|
if (!plugin.platform || !plugin.platform.log || !plugin.platform.config)
|
|
166
234
|
continue;
|
|
167
|
-
plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" : this.log.logLevel;
|
|
168
|
-
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" : this.log.logLevel);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
|
|
236
|
+
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
|
|
237
|
+
}
|
|
238
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
239
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
240
|
+
if (this.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
241
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
242
|
+
if (this.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
243
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
175
244
|
AnsiLogger.setGlobalCallback(this.frontend.wssSendMessage.bind(this.frontend), callbackLogLevel);
|
|
176
245
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
177
246
|
}
|
|
247
|
+
/** ***********************************************************************************************************************************/
|
|
248
|
+
/** loadInstance() and cleanup() methods */
|
|
249
|
+
/** ***********************************************************************************************************************************/
|
|
250
|
+
/**
|
|
251
|
+
* Loads an instance of the Matterbridge class.
|
|
252
|
+
* If an instance already exists, return that instance.
|
|
253
|
+
*
|
|
254
|
+
* @param initialize - Whether to initialize the Matterbridge instance after loading.
|
|
255
|
+
* @returns The loaded Matterbridge instance.
|
|
256
|
+
*/
|
|
178
257
|
static async loadInstance(initialize = false) {
|
|
179
258
|
if (!Matterbridge.instance) {
|
|
259
|
+
// eslint-disable-next-line no-console
|
|
180
260
|
if (hasParameter('debug'))
|
|
181
261
|
console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
|
|
182
262
|
Matterbridge.instance = new Matterbridge();
|
|
@@ -185,8 +265,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
185
265
|
}
|
|
186
266
|
return Matterbridge.instance;
|
|
187
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Call cleanup().
|
|
270
|
+
* @deprecated This method is deprecated and is only used for jest tests.
|
|
271
|
+
*
|
|
272
|
+
*/
|
|
188
273
|
async destroyInstance() {
|
|
189
274
|
this.log.info(`Destroy instance...`);
|
|
275
|
+
// Save server nodes to close
|
|
190
276
|
const servers = [];
|
|
191
277
|
if (this.bridgeMode === 'bridge') {
|
|
192
278
|
if (this.serverNode)
|
|
@@ -198,55 +284,81 @@ export class Matterbridge extends EventEmitter {
|
|
|
198
284
|
servers.push(plugin.serverNode);
|
|
199
285
|
}
|
|
200
286
|
}
|
|
287
|
+
// Cleanup
|
|
201
288
|
await this.cleanup('destroying instance...', false);
|
|
289
|
+
// Close servers mdns service
|
|
202
290
|
this.log.info(`Dispose ${servers.length} MdnsService...`);
|
|
203
291
|
for (const server of servers) {
|
|
204
292
|
await server.env.get(MdnsService)[Symbol.asyncDispose]();
|
|
205
293
|
this.log.info(`Closed ${server.id} MdnsService`);
|
|
206
294
|
}
|
|
295
|
+
// Wait for the cleanup to finish
|
|
207
296
|
await new Promise((resolve) => {
|
|
208
297
|
setTimeout(resolve, 1000);
|
|
209
298
|
});
|
|
210
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Initializes the Matterbridge application.
|
|
302
|
+
*
|
|
303
|
+
* @remarks
|
|
304
|
+
* This method performs the necessary setup and initialization steps for the Matterbridge application.
|
|
305
|
+
* It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
|
|
306
|
+
* node version, registers signal handlers, initializes storage, and parses the command line.
|
|
307
|
+
*
|
|
308
|
+
* @returns A Promise that resolves when the initialization is complete.
|
|
309
|
+
*/
|
|
211
310
|
async initialize() {
|
|
311
|
+
// Set the restart mode
|
|
212
312
|
if (hasParameter('service'))
|
|
213
313
|
this.restartMode = 'service';
|
|
214
314
|
if (hasParameter('docker'))
|
|
215
315
|
this.restartMode = 'docker';
|
|
316
|
+
// Set the matterbridge directory
|
|
216
317
|
this.homeDirectory = getParameter('homedir') ?? os.homedir();
|
|
217
318
|
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
319
|
+
// Setup the matter environment
|
|
218
320
|
this.environment.vars.set('log.level', MatterLogLevel.INFO);
|
|
219
321
|
this.environment.vars.set('log.format', MatterLogFormat.ANSI);
|
|
220
322
|
this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
221
323
|
this.environment.vars.set('runtime.signals', false);
|
|
222
324
|
this.environment.vars.set('runtime.exitcode', false);
|
|
223
|
-
|
|
325
|
+
// Create the matterbridge logger
|
|
326
|
+
this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
327
|
+
// Register process handlers
|
|
224
328
|
this.registerProcessHandlers();
|
|
329
|
+
// Initialize nodeStorage and nodeContext
|
|
225
330
|
try {
|
|
226
331
|
this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
|
|
227
332
|
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
|
|
228
333
|
this.log.debug('Creating node storage context for matterbridge');
|
|
229
334
|
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
335
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
336
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
337
|
const keys = (await this.nodeStorage?.storage.keys());
|
|
231
338
|
for (const key of keys) {
|
|
232
339
|
this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
341
|
await this.nodeStorage?.storage.get(key);
|
|
234
342
|
}
|
|
235
343
|
const storages = await this.nodeStorage.getStorageNames();
|
|
236
344
|
for (const storage of storages) {
|
|
237
345
|
this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
|
|
238
346
|
const nodeContext = await this.nodeStorage?.createStorage(storage);
|
|
347
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
348
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
239
349
|
const keys = (await nodeContext?.storage.keys());
|
|
240
350
|
keys.forEach(async (key) => {
|
|
241
351
|
this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
|
|
242
352
|
await nodeContext?.get(key);
|
|
243
353
|
});
|
|
244
354
|
}
|
|
355
|
+
// Creating a backup of the node storage since it is not corrupted
|
|
245
356
|
this.log.debug('Creating node storage backup...');
|
|
246
357
|
await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'));
|
|
247
358
|
this.log.debug('Created node storage backup');
|
|
248
359
|
}
|
|
249
360
|
catch (error) {
|
|
361
|
+
// Restoring the backup of the node storage since it is corrupted
|
|
250
362
|
this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
|
|
251
363
|
if (hasParameter('norestore')) {
|
|
252
364
|
this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
|
|
@@ -261,41 +373,46 @@ export class Matterbridge extends EventEmitter {
|
|
|
261
373
|
this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
|
|
262
374
|
throw new Error('Fatal error creating node storage manager and context for matterbridge');
|
|
263
375
|
}
|
|
376
|
+
// Set the first port to use for the commissioning server (will be incremented in childbridge mode)
|
|
264
377
|
this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
|
|
378
|
+
// Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
|
|
265
379
|
this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
|
|
380
|
+
// Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
|
|
266
381
|
this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
|
|
267
382
|
this.log.debug(`Initializing server node for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
|
|
383
|
+
// Set matterbridge logger level (context: matterbridgeLogLevel)
|
|
268
384
|
if (hasParameter('logger')) {
|
|
269
385
|
const level = getParameter('logger');
|
|
270
386
|
if (level === 'debug') {
|
|
271
|
-
this.log.logLevel = "debug"
|
|
387
|
+
this.log.logLevel = "debug" /* LogLevel.DEBUG */;
|
|
272
388
|
}
|
|
273
389
|
else if (level === 'info') {
|
|
274
|
-
this.log.logLevel = "info"
|
|
390
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
275
391
|
}
|
|
276
392
|
else if (level === 'notice') {
|
|
277
|
-
this.log.logLevel = "notice"
|
|
393
|
+
this.log.logLevel = "notice" /* LogLevel.NOTICE */;
|
|
278
394
|
}
|
|
279
395
|
else if (level === 'warn') {
|
|
280
|
-
this.log.logLevel = "warn"
|
|
396
|
+
this.log.logLevel = "warn" /* LogLevel.WARN */;
|
|
281
397
|
}
|
|
282
398
|
else if (level === 'error') {
|
|
283
|
-
this.log.logLevel = "error"
|
|
399
|
+
this.log.logLevel = "error" /* LogLevel.ERROR */;
|
|
284
400
|
}
|
|
285
401
|
else if (level === 'fatal') {
|
|
286
|
-
this.log.logLevel = "fatal"
|
|
402
|
+
this.log.logLevel = "fatal" /* LogLevel.FATAL */;
|
|
287
403
|
}
|
|
288
404
|
else {
|
|
289
405
|
this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
|
|
290
|
-
this.log.logLevel = "info"
|
|
406
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
291
407
|
}
|
|
292
408
|
}
|
|
293
409
|
else {
|
|
294
|
-
this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" : "info");
|
|
410
|
+
this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */);
|
|
295
411
|
}
|
|
296
412
|
this.frontend.logLevel = this.log.logLevel;
|
|
297
413
|
MatterbridgeEndpoint.logLevel = this.log.logLevel;
|
|
298
414
|
this.matterbridgeInformation.loggerLevel = this.log.logLevel;
|
|
415
|
+
// Create the file logger for matterbridge (context: matterbridgeFileLog)
|
|
299
416
|
if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
|
|
300
417
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true);
|
|
301
418
|
this.matterbridgeInformation.fileLogger = true;
|
|
@@ -304,6 +421,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
304
421
|
this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`);
|
|
305
422
|
if (this.profile !== undefined)
|
|
306
423
|
this.log.debug(`Matterbridge profile: ${this.profile}.`);
|
|
424
|
+
// Set matter.js logger level, format and logger (context: matterLogLevel)
|
|
307
425
|
if (hasParameter('matterlogger')) {
|
|
308
426
|
const level = getParameter('matterlogger');
|
|
309
427
|
if (level === 'debug') {
|
|
@@ -335,6 +453,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
335
453
|
Logger.format = MatterLogFormat.ANSI;
|
|
336
454
|
Logger.setLogger('default', this.createMatterLogger());
|
|
337
455
|
this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
|
|
456
|
+
// Create the file logger for matter.js (context: matterFileLog)
|
|
338
457
|
if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
|
|
339
458
|
this.matterbridgeInformation.matterFileLogger = true;
|
|
340
459
|
Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
|
|
@@ -343,6 +462,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
343
462
|
});
|
|
344
463
|
}
|
|
345
464
|
this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
|
|
465
|
+
// Log network interfaces
|
|
346
466
|
const networkInterfaces = os.networkInterfaces();
|
|
347
467
|
const availableAddresses = Object.entries(networkInterfaces);
|
|
348
468
|
const availableInterfaces = Object.keys(networkInterfaces);
|
|
@@ -354,6 +474,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
354
474
|
});
|
|
355
475
|
}
|
|
356
476
|
}
|
|
477
|
+
// Set the interface to use for matter server node mdnsInterface
|
|
357
478
|
if (hasParameter('mdnsinterface')) {
|
|
358
479
|
this.mdnsInterface = getParameter('mdnsinterface');
|
|
359
480
|
}
|
|
@@ -362,6 +483,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
362
483
|
if (this.mdnsInterface === '')
|
|
363
484
|
this.mdnsInterface = undefined;
|
|
364
485
|
}
|
|
486
|
+
// Validate mdnsInterface
|
|
365
487
|
if (this.mdnsInterface) {
|
|
366
488
|
if (!availableInterfaces.includes(this.mdnsInterface)) {
|
|
367
489
|
this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
|
|
@@ -373,6 +495,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
373
495
|
}
|
|
374
496
|
if (this.mdnsInterface)
|
|
375
497
|
this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
|
|
498
|
+
// Set the listeningAddressIpv4 for the matter commissioning server
|
|
376
499
|
if (hasParameter('ipv4address')) {
|
|
377
500
|
this.ipv4address = getParameter('ipv4address');
|
|
378
501
|
}
|
|
@@ -381,6 +504,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
381
504
|
if (this.ipv4address === '')
|
|
382
505
|
this.ipv4address = undefined;
|
|
383
506
|
}
|
|
507
|
+
// Validate ipv4address
|
|
384
508
|
if (this.ipv4address) {
|
|
385
509
|
let isValid = false;
|
|
386
510
|
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
@@ -395,6 +519,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
395
519
|
this.ipv4address = undefined;
|
|
396
520
|
}
|
|
397
521
|
}
|
|
522
|
+
// Set the listeningAddressIpv6 for the matter commissioning server
|
|
398
523
|
if (hasParameter('ipv6address')) {
|
|
399
524
|
this.ipv6address = getParameter('ipv6address');
|
|
400
525
|
}
|
|
@@ -403,6 +528,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
403
528
|
if (this.ipv6address === '')
|
|
404
529
|
this.ipv6address = undefined;
|
|
405
530
|
}
|
|
531
|
+
// Validate ipv6address
|
|
406
532
|
if (this.ipv6address) {
|
|
407
533
|
let isValid = false;
|
|
408
534
|
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
@@ -422,14 +548,19 @@ export class Matterbridge extends EventEmitter {
|
|
|
422
548
|
this.ipv6address = undefined;
|
|
423
549
|
}
|
|
424
550
|
}
|
|
551
|
+
// Initialize PluginManager
|
|
425
552
|
this.plugins = new PluginManager(this);
|
|
426
553
|
await this.plugins.loadFromStorage();
|
|
427
554
|
this.plugins.logLevel = this.log.logLevel;
|
|
555
|
+
// Initialize DeviceManager
|
|
428
556
|
this.devices = new DeviceManager(this, this.nodeContext);
|
|
429
557
|
this.devices.logLevel = this.log.logLevel;
|
|
558
|
+
// Get the plugins from node storage and create the plugins node storage contexts
|
|
430
559
|
for (const plugin of this.plugins) {
|
|
431
560
|
const packageJson = await this.plugins.parse(plugin);
|
|
432
561
|
if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) {
|
|
562
|
+
// Try to reinstall the plugin from npm (for Docker pull and external plugins)
|
|
563
|
+
// We don't do this when the add and other parameters are set because we shut down the process after adding the plugin
|
|
433
564
|
this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
|
|
434
565
|
try {
|
|
435
566
|
await this.spawnCommand('npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']);
|
|
@@ -451,6 +582,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
451
582
|
await plugin.nodeContext.set('description', plugin.description);
|
|
452
583
|
await plugin.nodeContext.set('author', plugin.author);
|
|
453
584
|
}
|
|
585
|
+
// Log system info and create .matterbridge directory
|
|
454
586
|
await this.logNodeAndSystemInfo();
|
|
455
587
|
this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
|
|
456
588
|
`${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
|
|
@@ -458,6 +590,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
458
590
|
`${hasParameter('controller') ? 'mode controller ' : ''}` +
|
|
459
591
|
`${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
|
|
460
592
|
`running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
|
|
593
|
+
// Check node version and throw error
|
|
461
594
|
const minNodeVersion = 18;
|
|
462
595
|
const nodeVersion = process.versions.node;
|
|
463
596
|
const versionMajor = parseInt(nodeVersion.split('.')[0]);
|
|
@@ -465,9 +598,15 @@ export class Matterbridge extends EventEmitter {
|
|
|
465
598
|
this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
466
599
|
throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
467
600
|
}
|
|
601
|
+
// Parse command line
|
|
468
602
|
await this.parseCommandLine();
|
|
469
603
|
this.initialized = true;
|
|
470
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Parses the command line arguments and performs the corresponding actions.
|
|
607
|
+
* @private
|
|
608
|
+
* @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
|
|
609
|
+
*/
|
|
471
610
|
async parseCommandLine() {
|
|
472
611
|
if (hasParameter('help')) {
|
|
473
612
|
this.log.info(`\nUsage: matterbridge [options]\n
|
|
@@ -491,6 +630,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
491
630
|
- nosudo: force not to use sudo to install or update packages if the internal logic fails
|
|
492
631
|
- norestore: force not to automatically restore the matterbridge node storage and the matter storage from backup if it is corrupted
|
|
493
632
|
- ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
|
|
633
|
+
- vendorId: override the default vendorId 0xfff1
|
|
634
|
+
- vendorName: override the default vendorName "Matterbridge"
|
|
635
|
+
- productId: override the default productId 0x8000
|
|
636
|
+
- productName: override the default productName "Matterbridge aggregator"
|
|
637
|
+
- service: enable the service mode (used in the systemctl configuration file)
|
|
638
|
+
- docker: enable the docker mode (used in the Dockerfile to build the docker image)
|
|
639
|
+
- homedir: override the home directory (default: os.homedir())
|
|
494
640
|
- add [plugin path]: register the plugin from the given absolute or relative path
|
|
495
641
|
- add [plugin name]: register the globally installed plugin with the given name
|
|
496
642
|
- remove [plugin path]: remove the plugin from the given absolute or relative path
|
|
@@ -579,6 +725,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
579
725
|
this.shutdown = true;
|
|
580
726
|
return;
|
|
581
727
|
}
|
|
728
|
+
// Start the matter storage and create the matterbridge context
|
|
582
729
|
try {
|
|
583
730
|
await this.startMatterStorage();
|
|
584
731
|
}
|
|
@@ -586,12 +733,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
586
733
|
this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
587
734
|
throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
588
735
|
}
|
|
736
|
+
// Clear the matterbridge context if the reset parameter is set
|
|
589
737
|
if (hasParameter('reset') && getParameter('reset') === undefined) {
|
|
590
738
|
this.initialized = true;
|
|
591
739
|
await this.shutdownProcessAndReset();
|
|
592
740
|
this.shutdown = true;
|
|
593
741
|
return;
|
|
594
742
|
}
|
|
743
|
+
// Clear matterbridge plugin context if the reset parameter is set
|
|
595
744
|
if (hasParameter('reset') && getParameter('reset') !== undefined) {
|
|
596
745
|
this.log.debug(`Reset plugin ${getParameter('reset')}`);
|
|
597
746
|
const plugin = this.plugins.get(getParameter('reset'));
|
|
@@ -616,30 +765,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
616
765
|
this.shutdown = true;
|
|
617
766
|
return;
|
|
618
767
|
}
|
|
768
|
+
// Initialize frontend
|
|
619
769
|
if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
|
|
620
770
|
await this.frontend.start(getIntParameter('frontend'));
|
|
771
|
+
// Check in 30 seconds the latest versions
|
|
621
772
|
this.checkUpdateTimeout = setTimeout(async () => {
|
|
622
773
|
const { checkUpdates } = await import('./update.js');
|
|
623
774
|
checkUpdates(this);
|
|
624
775
|
}, 30 * 1000).unref();
|
|
776
|
+
// Check each 24 hours the latest versions
|
|
625
777
|
this.checkUpdateInterval = setInterval(async () => {
|
|
626
778
|
const { checkUpdates } = await import('./update.js');
|
|
627
779
|
checkUpdates(this);
|
|
628
|
-
},
|
|
780
|
+
}, 12 * 60 * 60 * 1000).unref();
|
|
781
|
+
// Start the matterbridge in mode test
|
|
629
782
|
if (hasParameter('test')) {
|
|
630
783
|
this.bridgeMode = 'bridge';
|
|
631
784
|
MatterbridgeEndpoint.bridgeMode = 'bridge';
|
|
632
785
|
return;
|
|
633
786
|
}
|
|
787
|
+
// Start the matterbridge in mode controller
|
|
634
788
|
if (hasParameter('controller')) {
|
|
635
789
|
this.bridgeMode = 'controller';
|
|
636
790
|
await this.startController();
|
|
637
791
|
return;
|
|
638
792
|
}
|
|
793
|
+
// Check if the bridge mode is set and start matterbridge in bridge mode if not set
|
|
639
794
|
if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
|
|
640
795
|
this.log.info('Setting default matterbridge start mode to bridge');
|
|
641
796
|
await this.nodeContext?.set('bridgeMode', 'bridge');
|
|
642
797
|
}
|
|
798
|
+
// Start matterbridge in bridge mode
|
|
643
799
|
if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
|
|
644
800
|
this.bridgeMode = 'bridge';
|
|
645
801
|
MatterbridgeEndpoint.bridgeMode = 'bridge';
|
|
@@ -647,6 +803,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
647
803
|
await this.startBridge();
|
|
648
804
|
return;
|
|
649
805
|
}
|
|
806
|
+
// Start matterbridge in childbridge mode
|
|
650
807
|
if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
|
|
651
808
|
this.bridgeMode = 'childbridge';
|
|
652
809
|
MatterbridgeEndpoint.bridgeMode = 'childbridge';
|
|
@@ -655,10 +812,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
655
812
|
return;
|
|
656
813
|
}
|
|
657
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Asynchronously loads and starts the registered plugins.
|
|
817
|
+
*
|
|
818
|
+
* This method is responsible for initializing and staarting all enabled plugins.
|
|
819
|
+
* It ensures that each plugin is properly loaded and started before the bridge starts.
|
|
820
|
+
*
|
|
821
|
+
* @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
|
|
822
|
+
*/
|
|
658
823
|
async startPlugins() {
|
|
824
|
+
// Check, load and start the plugins
|
|
659
825
|
for (const plugin of this.plugins) {
|
|
660
826
|
plugin.configJson = await this.plugins.loadConfig(plugin);
|
|
661
827
|
plugin.schemaJson = await this.plugins.loadSchema(plugin);
|
|
828
|
+
// Check if the plugin is available
|
|
662
829
|
if (!(await this.plugins.resolve(plugin.path))) {
|
|
663
830
|
this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
|
|
664
831
|
plugin.enabled = false;
|
|
@@ -678,20 +845,28 @@ export class Matterbridge extends EventEmitter {
|
|
|
678
845
|
plugin.addedDevices = undefined;
|
|
679
846
|
plugin.qrPairingCode = undefined;
|
|
680
847
|
plugin.manualPairingCode = undefined;
|
|
681
|
-
this.plugins.load(plugin, true, 'Matterbridge is starting');
|
|
848
|
+
this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
|
|
682
849
|
}
|
|
683
850
|
this.frontend.wssSendRefreshRequired('plugins');
|
|
684
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
|
|
854
|
+
* When either of these signals are received, the cleanup method is called with an appropriate message.
|
|
855
|
+
*/
|
|
685
856
|
registerProcessHandlers() {
|
|
686
857
|
this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
|
|
687
858
|
process.removeAllListeners('uncaughtException');
|
|
688
859
|
process.removeAllListeners('unhandledRejection');
|
|
689
860
|
this.exceptionHandler = async (error) => {
|
|
690
|
-
|
|
861
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
862
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
863
|
+
this.log.error(`Unhandled Exception detected: ${errorMessage}\nstack: ${errorInspect}}`);
|
|
691
864
|
};
|
|
692
865
|
process.on('uncaughtException', this.exceptionHandler);
|
|
693
866
|
this.rejectionHandler = async (reason, promise) => {
|
|
694
|
-
|
|
867
|
+
const errorMessage = reason instanceof Error ? reason.message : reason;
|
|
868
|
+
const errorInspect = inspect(reason, { depth: 10 });
|
|
869
|
+
this.log.error(`Unhandled Rejection detected: ${promise}\nreason: ${errorMessage}\nstack: ${errorInspect}`);
|
|
695
870
|
};
|
|
696
871
|
process.on('unhandledRejection', this.rejectionHandler);
|
|
697
872
|
this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
|
|
@@ -704,6 +879,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
704
879
|
};
|
|
705
880
|
process.on('SIGTERM', this.sigtermHandler);
|
|
706
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
|
|
884
|
+
*/
|
|
707
885
|
deregisterProcesslHandlers() {
|
|
708
886
|
this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
|
|
709
887
|
if (this.exceptionHandler)
|
|
@@ -720,12 +898,17 @@ export class Matterbridge extends EventEmitter {
|
|
|
720
898
|
process.off('SIGTERM', this.sigtermHandler);
|
|
721
899
|
this.sigtermHandler = undefined;
|
|
722
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* Logs the node and system information.
|
|
903
|
+
*/
|
|
723
904
|
async logNodeAndSystemInfo() {
|
|
905
|
+
// IP address information
|
|
724
906
|
const networkInterfaces = os.networkInterfaces();
|
|
725
907
|
this.systemInformation.interfaceName = '';
|
|
726
908
|
this.systemInformation.ipv4Address = '';
|
|
727
909
|
this.systemInformation.ipv6Address = '';
|
|
728
910
|
for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
|
|
911
|
+
// this.log.debug(`Checking interface: '${interfaceName}' for '${this.mdnsInterface}'`);
|
|
729
912
|
if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
|
|
730
913
|
continue;
|
|
731
914
|
if (!interfaceDetails) {
|
|
@@ -751,19 +934,22 @@ export class Matterbridge extends EventEmitter {
|
|
|
751
934
|
break;
|
|
752
935
|
}
|
|
753
936
|
}
|
|
937
|
+
// Node information
|
|
754
938
|
this.systemInformation.nodeVersion = process.versions.node;
|
|
755
939
|
const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
|
|
756
940
|
const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
|
|
757
941
|
const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
|
|
942
|
+
// Host system information
|
|
758
943
|
this.systemInformation.hostname = os.hostname();
|
|
759
944
|
this.systemInformation.user = os.userInfo().username;
|
|
760
|
-
this.systemInformation.osType = os.type();
|
|
761
|
-
this.systemInformation.osRelease = os.release();
|
|
762
|
-
this.systemInformation.osPlatform = os.platform();
|
|
763
|
-
this.systemInformation.osArch = os.arch();
|
|
764
|
-
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
765
|
-
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
766
|
-
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours';
|
|
945
|
+
this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
|
|
946
|
+
this.systemInformation.osRelease = os.release(); // Kernel version
|
|
947
|
+
this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
|
|
948
|
+
this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
|
|
949
|
+
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
950
|
+
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
951
|
+
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
|
|
952
|
+
// Log the system information
|
|
767
953
|
this.log.debug('Host System Information:');
|
|
768
954
|
this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
|
|
769
955
|
this.log.debug(`- User: ${this.systemInformation.user}`);
|
|
@@ -779,16 +965,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
779
965
|
this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
|
|
780
966
|
this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
|
|
781
967
|
this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
|
|
968
|
+
// Home directory
|
|
782
969
|
this.homeDirectory = getParameter('homedir') ?? os.homedir();
|
|
783
970
|
this.matterbridgeInformation.homeDirectory = this.homeDirectory;
|
|
784
971
|
this.log.debug(`Home Directory: ${this.homeDirectory}`);
|
|
972
|
+
// Package root directory
|
|
785
973
|
const { fileURLToPath } = await import('node:url');
|
|
786
974
|
const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
787
975
|
this.rootDirectory = path.resolve(currentFileDirectory, '../');
|
|
788
976
|
this.matterbridgeInformation.rootDirectory = this.rootDirectory;
|
|
789
977
|
this.log.debug(`Root Directory: ${this.rootDirectory}`);
|
|
978
|
+
// Global node_modules directory
|
|
790
979
|
if (this.nodeContext)
|
|
791
980
|
this.globalModulesDirectory = this.matterbridgeInformation.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
981
|
+
// First run of Matterbridge so the node storage is empty
|
|
792
982
|
if (this.globalModulesDirectory === '') {
|
|
793
983
|
try {
|
|
794
984
|
this.execRunningCount++;
|
|
@@ -804,6 +994,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
804
994
|
}
|
|
805
995
|
else
|
|
806
996
|
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
997
|
+
/* removed cause is too expensive for the shelly board and not really needed. Why should it change the globalModulesDirectory?
|
|
998
|
+
else {
|
|
999
|
+
this.getGlobalNodeModules()
|
|
1000
|
+
.then(async (globalModulesDirectory) => {
|
|
1001
|
+
this.globalModulesDirectory = globalModulesDirectory;
|
|
1002
|
+
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
1003
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
1004
|
+
await this.nodeContext?.set<string>('globalModulesDirectory', this.globalModulesDirectory);
|
|
1005
|
+
})
|
|
1006
|
+
.catch((error) => {
|
|
1007
|
+
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
1008
|
+
});
|
|
1009
|
+
}*/
|
|
1010
|
+
// Create the data directory .matterbridge in the home directory
|
|
807
1011
|
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
808
1012
|
this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
|
|
809
1013
|
try {
|
|
@@ -827,6 +1031,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
827
1031
|
}
|
|
828
1032
|
}
|
|
829
1033
|
this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
1034
|
+
// Create the plugin directory Matterbridge in the home directory
|
|
830
1035
|
this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
|
|
831
1036
|
this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
|
|
832
1037
|
try {
|
|
@@ -850,6 +1055,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
850
1055
|
}
|
|
851
1056
|
}
|
|
852
1057
|
this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
1058
|
+
// Create the matter cert directory in the home directory
|
|
853
1059
|
this.matterbridgeCertDirectory = path.join(this.homeDirectory, '.mattercert');
|
|
854
1060
|
this.matterbridgeInformation.matterbridgeCertDirectory = this.matterbridgeCertDirectory;
|
|
855
1061
|
try {
|
|
@@ -873,50 +1079,68 @@ export class Matterbridge extends EventEmitter {
|
|
|
873
1079
|
}
|
|
874
1080
|
}
|
|
875
1081
|
this.log.debug(`Matterbridge Matter Cert Directory: ${this.matterbridgeCertDirectory}`);
|
|
1082
|
+
// Matterbridge version
|
|
876
1083
|
const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
|
|
877
1084
|
this.matterbridgeVersion = this.matterbridgeLatestVersion = packageJson.version;
|
|
878
1085
|
this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeVersion;
|
|
879
1086
|
this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
|
|
1087
|
+
// Matterbridge latest version
|
|
880
1088
|
if (this.nodeContext)
|
|
881
1089
|
this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
|
|
882
1090
|
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
1091
|
+
// this.getMatterbridgeLatestVersion();
|
|
1092
|
+
// Current working directory
|
|
883
1093
|
const currentDir = process.cwd();
|
|
884
1094
|
this.log.debug(`Current Working Directory: ${currentDir}`);
|
|
1095
|
+
// Command line arguments (excluding 'node' and the script name)
|
|
885
1096
|
const cmdArgs = process.argv.slice(2).join(' ');
|
|
886
1097
|
this.log.debug(`Command Line Arguments: ${cmdArgs}`);
|
|
887
1098
|
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
|
|
1101
|
+
*
|
|
1102
|
+
* @returns {Function} The MatterLogger function.
|
|
1103
|
+
*/
|
|
888
1104
|
createMatterLogger() {
|
|
889
|
-
const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4
|
|
1105
|
+
const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
|
|
890
1106
|
return (_level, formattedLog) => {
|
|
891
1107
|
const logger = formattedLog.slice(44, 44 + 20).trim();
|
|
892
1108
|
const message = formattedLog.slice(65);
|
|
893
1109
|
matterLogger.logName = logger;
|
|
894
1110
|
switch (_level) {
|
|
895
1111
|
case MatterLogLevel.DEBUG:
|
|
896
|
-
matterLogger.log("debug"
|
|
1112
|
+
matterLogger.log("debug" /* LogLevel.DEBUG */, message);
|
|
897
1113
|
break;
|
|
898
1114
|
case MatterLogLevel.INFO:
|
|
899
|
-
matterLogger.log("info"
|
|
1115
|
+
matterLogger.log("info" /* LogLevel.INFO */, message);
|
|
900
1116
|
break;
|
|
901
1117
|
case MatterLogLevel.NOTICE:
|
|
902
|
-
matterLogger.log("notice"
|
|
1118
|
+
matterLogger.log("notice" /* LogLevel.NOTICE */, message);
|
|
903
1119
|
break;
|
|
904
1120
|
case MatterLogLevel.WARN:
|
|
905
|
-
matterLogger.log("warn"
|
|
1121
|
+
matterLogger.log("warn" /* LogLevel.WARN */, message);
|
|
906
1122
|
break;
|
|
907
1123
|
case MatterLogLevel.ERROR:
|
|
908
|
-
matterLogger.log("error"
|
|
1124
|
+
matterLogger.log("error" /* LogLevel.ERROR */, message);
|
|
909
1125
|
break;
|
|
910
1126
|
case MatterLogLevel.FATAL:
|
|
911
|
-
matterLogger.log("fatal"
|
|
1127
|
+
matterLogger.log("fatal" /* LogLevel.FATAL */, message);
|
|
912
1128
|
break;
|
|
913
1129
|
default:
|
|
914
|
-
matterLogger.log("debug"
|
|
1130
|
+
matterLogger.log("debug" /* LogLevel.DEBUG */, message);
|
|
915
1131
|
break;
|
|
916
1132
|
}
|
|
917
1133
|
};
|
|
918
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Creates a Matter File Logger.
|
|
1137
|
+
*
|
|
1138
|
+
* @param {string} filePath - The path to the log file.
|
|
1139
|
+
* @param {boolean} [unlink=false] - Whether to unlink the log file before creating a new one.
|
|
1140
|
+
* @returns {Function} - A function that logs formatted messages to the log file.
|
|
1141
|
+
*/
|
|
919
1142
|
async createMatterFileLogger(filePath, unlink = false) {
|
|
1143
|
+
// 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
|
|
920
1144
|
let fileSize = 0;
|
|
921
1145
|
if (unlink) {
|
|
922
1146
|
try {
|
|
@@ -965,12 +1189,21 @@ export class Matterbridge extends EventEmitter {
|
|
|
965
1189
|
}
|
|
966
1190
|
};
|
|
967
1191
|
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Restarts the process by exiting the current instance and loading a new instance.
|
|
1194
|
+
*/
|
|
968
1195
|
async restartProcess() {
|
|
969
1196
|
await this.cleanup('restarting...', true);
|
|
970
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Shut down the process by exiting the current process.
|
|
1200
|
+
*/
|
|
971
1201
|
async shutdownProcess() {
|
|
972
1202
|
await this.cleanup('shutting down...', false);
|
|
973
1203
|
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Update matterbridge and and shut down the process.
|
|
1206
|
+
*/
|
|
974
1207
|
async updateProcess() {
|
|
975
1208
|
this.log.info('Updating matterbridge...');
|
|
976
1209
|
try {
|
|
@@ -983,51 +1216,72 @@ export class Matterbridge extends EventEmitter {
|
|
|
983
1216
|
this.frontend.wssSendRestartRequired();
|
|
984
1217
|
await this.cleanup('updating...', false);
|
|
985
1218
|
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Unregister all devices and shut down the process.
|
|
1221
|
+
*/
|
|
986
1222
|
async unregisterAndShutdownProcess() {
|
|
987
1223
|
this.log.info('Unregistering all devices and shutting down...');
|
|
988
1224
|
for (const plugin of this.plugins) {
|
|
989
1225
|
await this.removeAllBridgedEndpoints(plugin.name, 250);
|
|
990
1226
|
}
|
|
991
1227
|
this.log.debug('Waiting for the MessageExchange to finish...');
|
|
992
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1228
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
|
|
993
1229
|
this.log.debug('Cleaning up and shutting down...');
|
|
994
1230
|
await this.cleanup('unregistered all devices and shutting down...', false);
|
|
995
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Reset commissioning and shut down the process.
|
|
1234
|
+
*/
|
|
996
1235
|
async shutdownProcessAndReset() {
|
|
997
1236
|
await this.cleanup('shutting down with reset...', false);
|
|
998
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Factory reset and shut down the process.
|
|
1240
|
+
*/
|
|
999
1241
|
async shutdownProcessAndFactoryReset() {
|
|
1000
1242
|
await this.cleanup('shutting down with factory reset...', false);
|
|
1001
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Cleans up the Matterbridge instance.
|
|
1246
|
+
* @param message - The cleanup message.
|
|
1247
|
+
* @param restart - Indicates whether to restart the instance after cleanup. Default is `false`.
|
|
1248
|
+
* @returns A promise that resolves when the cleanup is completed.
|
|
1249
|
+
*/
|
|
1002
1250
|
async cleanup(message, restart = false) {
|
|
1003
1251
|
if (this.initialized && !this.hasCleanupStarted) {
|
|
1004
1252
|
this.hasCleanupStarted = true;
|
|
1005
1253
|
this.log.info(message);
|
|
1254
|
+
// Clear the start matter interval
|
|
1006
1255
|
if (this.startMatterInterval) {
|
|
1007
1256
|
clearInterval(this.startMatterInterval);
|
|
1008
1257
|
this.startMatterInterval = undefined;
|
|
1009
1258
|
this.log.debug('Start matter interval cleared');
|
|
1010
1259
|
}
|
|
1260
|
+
// Clear the check update timeout
|
|
1011
1261
|
if (this.checkUpdateTimeout) {
|
|
1012
1262
|
clearInterval(this.checkUpdateTimeout);
|
|
1013
1263
|
this.checkUpdateTimeout = undefined;
|
|
1014
1264
|
this.log.debug('Check update timeout cleared');
|
|
1015
1265
|
}
|
|
1266
|
+
// Clear the check update interval
|
|
1016
1267
|
if (this.checkUpdateInterval) {
|
|
1017
1268
|
clearInterval(this.checkUpdateInterval);
|
|
1018
1269
|
this.checkUpdateInterval = undefined;
|
|
1019
1270
|
this.log.debug('Check update interval cleared');
|
|
1020
1271
|
}
|
|
1272
|
+
// Clear the configure timeout
|
|
1021
1273
|
if (this.configureTimeout) {
|
|
1022
1274
|
clearTimeout(this.configureTimeout);
|
|
1023
1275
|
this.configureTimeout = undefined;
|
|
1024
1276
|
this.log.debug('Matterbridge configure timeout cleared');
|
|
1025
1277
|
}
|
|
1278
|
+
// Clear the reachability timeout
|
|
1026
1279
|
if (this.reachabilityTimeout) {
|
|
1027
1280
|
clearTimeout(this.reachabilityTimeout);
|
|
1028
1281
|
this.reachabilityTimeout = undefined;
|
|
1029
1282
|
this.log.debug('Matterbridge reachability timeout cleared');
|
|
1030
1283
|
}
|
|
1284
|
+
// Calling the shutdown method of each plugin and clear the plugins reachability timeout
|
|
1031
1285
|
for (const plugin of this.plugins) {
|
|
1032
1286
|
if (!plugin.enabled || plugin.error)
|
|
1033
1287
|
continue;
|
|
@@ -1038,9 +1292,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1038
1292
|
this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
|
|
1039
1293
|
}
|
|
1040
1294
|
}
|
|
1295
|
+
// Stopping matter server nodes
|
|
1041
1296
|
this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
|
|
1042
1297
|
this.log.debug('Waiting for the MessageExchange to finish...');
|
|
1043
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1298
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
|
|
1044
1299
|
if (this.bridgeMode === 'bridge') {
|
|
1045
1300
|
if (this.serverNode) {
|
|
1046
1301
|
await this.stopServerNode(this.serverNode);
|
|
@@ -1056,6 +1311,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1056
1311
|
}
|
|
1057
1312
|
}
|
|
1058
1313
|
this.log.notice('Stopped matter server nodes');
|
|
1314
|
+
// Matter commisioning reset
|
|
1059
1315
|
if (message === 'shutting down with reset...') {
|
|
1060
1316
|
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1061
1317
|
await this.matterStorageManager?.createContext('events')?.clearAll();
|
|
@@ -1065,17 +1321,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
1065
1321
|
await this.matterbridgeContext?.clearAll();
|
|
1066
1322
|
this.log.info('Matter storage reset done! Remove the bridge from the controller.');
|
|
1067
1323
|
}
|
|
1324
|
+
// Stop matter storage
|
|
1068
1325
|
await this.stopMatterStorage();
|
|
1326
|
+
// Stop the frontend
|
|
1069
1327
|
await this.frontend.stop();
|
|
1328
|
+
// Remove the matterfilelogger
|
|
1070
1329
|
try {
|
|
1071
1330
|
Logger.removeLogger('matterfilelogger');
|
|
1331
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1072
1332
|
}
|
|
1073
1333
|
catch (error) {
|
|
1334
|
+
// this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
|
|
1074
1335
|
}
|
|
1336
|
+
// Serialize registeredDevices
|
|
1075
1337
|
if (this.nodeStorage && this.nodeContext) {
|
|
1338
|
+
/*
|
|
1339
|
+
TODO: Implement serialization of registered devices in edge mode
|
|
1340
|
+
this.log.info('Saving registered devices...');
|
|
1341
|
+
const serializedRegisteredDevices: SerializedMatterbridgeEndpoint[] = [];
|
|
1342
|
+
this.devices.forEach(async (device) => {
|
|
1343
|
+
const serializedMatterbridgeDevice = MatterbridgeEndpoint.serialize(device);
|
|
1344
|
+
// this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
|
|
1345
|
+
if (serializedMatterbridgeDevice) serializedRegisteredDevices.push(serializedMatterbridgeDevice);
|
|
1346
|
+
});
|
|
1347
|
+
await this.nodeContext.set<SerializedMatterbridgeEndpoint[]>('devices', serializedRegisteredDevices);
|
|
1348
|
+
this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
|
|
1349
|
+
*/
|
|
1350
|
+
// Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
|
|
1076
1351
|
this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
|
|
1077
1352
|
await this.nodeContext.close();
|
|
1078
1353
|
this.nodeContext = undefined;
|
|
1354
|
+
// Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
|
|
1079
1355
|
for (const plugin of this.plugins) {
|
|
1080
1356
|
if (plugin.nodeContext) {
|
|
1081
1357
|
this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
|
|
@@ -1092,8 +1368,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1092
1368
|
}
|
|
1093
1369
|
this.plugins.clear();
|
|
1094
1370
|
this.devices.clear();
|
|
1371
|
+
// Factory reset
|
|
1095
1372
|
if (message === 'shutting down with factory reset...') {
|
|
1096
1373
|
try {
|
|
1374
|
+
// Delete old matter storage file and backup
|
|
1097
1375
|
const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
|
|
1098
1376
|
this.log.info(`Unlinking old matter storage file: ${file}`);
|
|
1099
1377
|
await fs.unlink(file);
|
|
@@ -1107,6 +1385,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1107
1385
|
}
|
|
1108
1386
|
}
|
|
1109
1387
|
try {
|
|
1388
|
+
// Delete matter node storage directory with its subdirectories and backup
|
|
1110
1389
|
const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
|
|
1111
1390
|
this.log.info(`Removing matter node storage directory: ${dir}`);
|
|
1112
1391
|
await fs.rm(dir, { recursive: true });
|
|
@@ -1120,6 +1399,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1120
1399
|
}
|
|
1121
1400
|
}
|
|
1122
1401
|
try {
|
|
1402
|
+
// Delete node storage directory with its subdirectories and backup
|
|
1123
1403
|
const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
|
|
1124
1404
|
this.log.info(`Removing storage directory: ${dir}`);
|
|
1125
1405
|
await fs.rm(dir, { recursive: true });
|
|
@@ -1134,12 +1414,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1134
1414
|
}
|
|
1135
1415
|
this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
|
|
1136
1416
|
}
|
|
1417
|
+
// Deregisters the process handlers
|
|
1137
1418
|
this.deregisterProcesslHandlers();
|
|
1138
1419
|
if (restart) {
|
|
1139
1420
|
if (message === 'updating...') {
|
|
1140
1421
|
this.log.info('Cleanup completed. Updating...');
|
|
1141
1422
|
Matterbridge.instance = undefined;
|
|
1142
|
-
this.emit('update');
|
|
1423
|
+
this.emit('update'); // Restart the process but the update has been done before. TODO move all updates to the cli
|
|
1143
1424
|
}
|
|
1144
1425
|
else if (message === 'restarting...') {
|
|
1145
1426
|
this.log.info('Cleanup completed. Restarting...');
|
|
@@ -1159,30 +1440,53 @@ export class Matterbridge extends EventEmitter {
|
|
|
1159
1440
|
this.log.debug('Cleanup already started...');
|
|
1160
1441
|
}
|
|
1161
1442
|
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Creates and configures the server node for an accessory plugin for a given device.
|
|
1445
|
+
*
|
|
1446
|
+
* @param {RegisteredPlugin} plugin - The plugin to configure.
|
|
1447
|
+
* @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
|
|
1448
|
+
* @param {boolean} [start=false] - Whether to start the server node after adding the device.
|
|
1449
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
|
|
1450
|
+
*/
|
|
1162
1451
|
async createAccessoryPlugin(plugin, device, start = false) {
|
|
1163
1452
|
if (!plugin.locked && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
|
|
1164
1453
|
plugin.locked = true;
|
|
1165
1454
|
plugin.device = device;
|
|
1166
1455
|
plugin.storageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
|
|
1167
1456
|
plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
1457
|
+
plugin.serialNumber = await plugin.storageContext.get('serialNumber', '');
|
|
1168
1458
|
this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node`);
|
|
1169
1459
|
await plugin.serverNode.add(device);
|
|
1170
1460
|
if (start)
|
|
1171
1461
|
await this.startServerNode(plugin.serverNode);
|
|
1172
1462
|
}
|
|
1173
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Creates and configures the server node for a dynamic plugin.
|
|
1466
|
+
*
|
|
1467
|
+
* @param {RegisteredPlugin} plugin - The plugin to configure.
|
|
1468
|
+
* @param {boolean} [start=false] - Whether to start the server node after adding the aggregator node.
|
|
1469
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the dynamic plugin is created and configured.
|
|
1470
|
+
*/
|
|
1174
1471
|
async createDynamicPlugin(plugin, start = false) {
|
|
1175
1472
|
if (!plugin.locked) {
|
|
1176
1473
|
plugin.locked = true;
|
|
1177
|
-
plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId,
|
|
1474
|
+
plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
|
|
1178
1475
|
plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
1179
1476
|
plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
|
|
1477
|
+
plugin.serialNumber = await plugin.storageContext.get('serialNumber', '');
|
|
1180
1478
|
await plugin.serverNode.add(plugin.aggregatorNode);
|
|
1181
1479
|
if (start)
|
|
1182
1480
|
await this.startServerNode(plugin.serverNode);
|
|
1183
1481
|
}
|
|
1184
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Starts the Matterbridge in bridge mode.
|
|
1485
|
+
* @private
|
|
1486
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1487
|
+
*/
|
|
1185
1488
|
async startBridge() {
|
|
1489
|
+
// Plugins are configured by a timer when matter server is started and plugin.configured is set to true
|
|
1186
1490
|
if (!this.matterStorageManager)
|
|
1187
1491
|
throw new Error('No storage manager initialized');
|
|
1188
1492
|
if (!this.matterbridgeContext)
|
|
@@ -1220,7 +1524,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
1220
1524
|
clearInterval(this.startMatterInterval);
|
|
1221
1525
|
this.startMatterInterval = undefined;
|
|
1222
1526
|
this.log.debug('Cleared startMatterInterval interval for Matterbridge');
|
|
1527
|
+
// Start the Matter server node
|
|
1223
1528
|
this.startServerNode(this.serverNode);
|
|
1529
|
+
// Configure the plugins
|
|
1224
1530
|
this.configureTimeout = setTimeout(async () => {
|
|
1225
1531
|
for (const plugin of this.plugins) {
|
|
1226
1532
|
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
@@ -1238,6 +1544,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1238
1544
|
}
|
|
1239
1545
|
this.frontend.wssSendRefreshRequired('plugins');
|
|
1240
1546
|
}, 30 * 1000);
|
|
1547
|
+
// Setting reachability to true
|
|
1241
1548
|
this.reachabilityTimeout = setTimeout(() => {
|
|
1242
1549
|
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
1243
1550
|
if (this.aggregatorNode)
|
|
@@ -1246,16 +1553,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
1246
1553
|
}, 60 * 1000);
|
|
1247
1554
|
}, 1000);
|
|
1248
1555
|
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Starts the Matterbridge in childbridge mode.
|
|
1558
|
+
* @private
|
|
1559
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1560
|
+
*/
|
|
1249
1561
|
async startChildbridge() {
|
|
1250
1562
|
if (!this.matterStorageManager)
|
|
1251
1563
|
throw new Error('No storage manager initialized');
|
|
1252
|
-
for (const plugin of this.plugins) {
|
|
1253
|
-
if (!plugin.enabled)
|
|
1254
|
-
continue;
|
|
1255
|
-
if (plugin.type === 'DynamicPlatform') {
|
|
1256
|
-
await this.createDynamicPlugin(plugin);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
1564
|
await this.startPlugins();
|
|
1260
1565
|
this.log.debug('Starting start matter interval in childbridge mode...');
|
|
1261
1566
|
let failCount = 0;
|
|
@@ -1290,6 +1595,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1290
1595
|
clearInterval(this.startMatterInterval);
|
|
1291
1596
|
this.startMatterInterval = undefined;
|
|
1292
1597
|
this.log.debug('Cleared startMatterInterval interval in childbridge mode');
|
|
1598
|
+
// Configure the plugins
|
|
1293
1599
|
this.configureTimeout = setTimeout(async () => {
|
|
1294
1600
|
for (const plugin of this.plugins) {
|
|
1295
1601
|
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
@@ -1326,7 +1632,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
1326
1632
|
this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1327
1633
|
continue;
|
|
1328
1634
|
}
|
|
1635
|
+
// Start the Matter server node
|
|
1329
1636
|
this.startServerNode(plugin.serverNode);
|
|
1637
|
+
// Setting reachability to true
|
|
1330
1638
|
plugin.reachabilityTimeout = setTimeout(() => {
|
|
1331
1639
|
this.log.info(`Setting reachability to true for ${plg}${plugin.name}${nf} type ${plugin.type} server node ${plugin.serverNode !== undefined} aggregator node ${plugin.aggregatorNode !== undefined} device ${plugin.device !== undefined}`);
|
|
1332
1640
|
if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
|
|
@@ -1336,6 +1644,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
1336
1644
|
}
|
|
1337
1645
|
}, 1000);
|
|
1338
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Starts the Matterbridge controller.
|
|
1649
|
+
* @private
|
|
1650
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1651
|
+
*/
|
|
1339
1652
|
async startController() {
|
|
1340
1653
|
if (!this.matterStorageManager) {
|
|
1341
1654
|
this.log.error('No storage manager initialized');
|
|
@@ -1350,30 +1663,255 @@ export class Matterbridge extends EventEmitter {
|
|
|
1350
1663
|
return;
|
|
1351
1664
|
}
|
|
1352
1665
|
this.log.debug('Starting matterbridge in mode', this.bridgeMode);
|
|
1666
|
+
/*
|
|
1667
|
+
this.matterServer = await this.createMatterServer(this.storageManager);
|
|
1668
|
+
this.log.info('Creating matter commissioning controller');
|
|
1669
|
+
this.commissioningController = new CommissioningController({
|
|
1670
|
+
autoConnect: false,
|
|
1671
|
+
});
|
|
1672
|
+
this.log.info('Adding matter commissioning controller to matter server');
|
|
1673
|
+
await this.matterServer.addCommissioningController(this.commissioningController);
|
|
1674
|
+
|
|
1675
|
+
this.log.info('Starting matter server');
|
|
1676
|
+
await this.matterServer.start();
|
|
1677
|
+
this.log.info('Matter server started');
|
|
1678
|
+
const commissioningOptions: ControllerCommissioningFlowOptions = {
|
|
1679
|
+
regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
|
|
1680
|
+
regulatoryCountryCode: 'XX',
|
|
1681
|
+
};
|
|
1682
|
+
const commissioningController = new CommissioningController({
|
|
1683
|
+
environment: {
|
|
1684
|
+
environment,
|
|
1685
|
+
id: uniqueId,
|
|
1686
|
+
},
|
|
1687
|
+
autoConnect: false, // Do not auto connect to the commissioned nodes
|
|
1688
|
+
adminFabricLabel,
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
if (hasParameter('pairingcode')) {
|
|
1692
|
+
this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
|
|
1693
|
+
const pairingCode = getParameter('pairingcode');
|
|
1694
|
+
const ip = this.controllerContext.has('ip') ? this.controllerContext.get<string>('ip') : undefined;
|
|
1695
|
+
const port = this.controllerContext.has('port') ? this.controllerContext.get<number>('port') : undefined;
|
|
1696
|
+
|
|
1697
|
+
let longDiscriminator, setupPin, shortDiscriminator;
|
|
1698
|
+
if (pairingCode !== undefined) {
|
|
1699
|
+
const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
|
|
1700
|
+
shortDiscriminator = pairingCodeCodec.shortDiscriminator;
|
|
1701
|
+
longDiscriminator = undefined;
|
|
1702
|
+
setupPin = pairingCodeCodec.passcode;
|
|
1703
|
+
this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
|
|
1704
|
+
} else {
|
|
1705
|
+
longDiscriminator = await this.controllerContext.get('longDiscriminator', 3840);
|
|
1706
|
+
if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
|
|
1707
|
+
setupPin = this.controllerContext.get('pin', 20202021);
|
|
1708
|
+
}
|
|
1709
|
+
if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
|
|
1710
|
+
throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const options = {
|
|
1714
|
+
commissioning: commissioningOptions,
|
|
1715
|
+
discovery: {
|
|
1716
|
+
knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
|
|
1717
|
+
identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
|
|
1718
|
+
},
|
|
1719
|
+
passcode: setupPin,
|
|
1720
|
+
} as NodeCommissioningOptions;
|
|
1721
|
+
this.log.info('Commissioning with options:', options);
|
|
1722
|
+
const nodeId = await this.commissioningController.commissionNode(options);
|
|
1723
|
+
this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
|
|
1724
|
+
this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
|
|
1725
|
+
} // (hasParameter('pairingcode'))
|
|
1726
|
+
|
|
1727
|
+
if (hasParameter('unpairall')) {
|
|
1728
|
+
this.log.info('***Commissioning controller unpairing all nodes...');
|
|
1729
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1730
|
+
for (const nodeId of nodeIds) {
|
|
1731
|
+
this.log.info('***Commissioning controller unpairing node:', nodeId);
|
|
1732
|
+
await this.commissioningController.removeNode(nodeId);
|
|
1733
|
+
}
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (hasParameter('discover')) {
|
|
1738
|
+
// const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
|
|
1739
|
+
// console.log(discover);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (!this.commissioningController.isCommissioned()) {
|
|
1743
|
+
this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1748
|
+
this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
|
|
1749
|
+
for (const nodeId of nodeIds) {
|
|
1750
|
+
this.log.info(`***Connecting to commissioned node: ${nodeId}`);
|
|
1751
|
+
|
|
1752
|
+
const node = await this.commissioningController.connectNode(nodeId, {
|
|
1753
|
+
autoSubscribe: false,
|
|
1754
|
+
attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
|
|
1755
|
+
this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
|
|
1756
|
+
eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
|
|
1757
|
+
this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
|
|
1758
|
+
stateInformationCallback: (peerNodeId, info) => {
|
|
1759
|
+
switch (info) {
|
|
1760
|
+
case NodeStateInformation.Connected:
|
|
1761
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
|
|
1762
|
+
break;
|
|
1763
|
+
case NodeStateInformation.Disconnected:
|
|
1764
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
|
|
1765
|
+
break;
|
|
1766
|
+
case NodeStateInformation.Reconnecting:
|
|
1767
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
|
|
1768
|
+
break;
|
|
1769
|
+
case NodeStateInformation.WaitingForDeviceDiscovery:
|
|
1770
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
|
|
1771
|
+
break;
|
|
1772
|
+
case NodeStateInformation.StructureChanged:
|
|
1773
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
|
|
1774
|
+
break;
|
|
1775
|
+
case NodeStateInformation.Decommissioned:
|
|
1776
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
|
|
1777
|
+
break;
|
|
1778
|
+
default:
|
|
1779
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
node.logStructure();
|
|
1786
|
+
|
|
1787
|
+
// Get the interaction client
|
|
1788
|
+
this.log.info('Getting the interaction client');
|
|
1789
|
+
const interactionClient = await node.getInteractionClient();
|
|
1790
|
+
let cluster;
|
|
1791
|
+
let attributes;
|
|
1792
|
+
|
|
1793
|
+
// Log BasicInformationCluster
|
|
1794
|
+
cluster = BasicInformationCluster;
|
|
1795
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1796
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1797
|
+
});
|
|
1798
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1799
|
+
attributes.forEach((attribute) => {
|
|
1800
|
+
this.log.info(
|
|
1801
|
+
`- 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}`,
|
|
1802
|
+
);
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
// Log PowerSourceCluster
|
|
1806
|
+
cluster = PowerSourceCluster;
|
|
1807
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1808
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1809
|
+
});
|
|
1810
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1811
|
+
attributes.forEach((attribute) => {
|
|
1812
|
+
this.log.info(
|
|
1813
|
+
`- 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}`,
|
|
1814
|
+
);
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
// Log ThreadNetworkDiagnostics
|
|
1818
|
+
cluster = ThreadNetworkDiagnosticsCluster;
|
|
1819
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1820
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1821
|
+
});
|
|
1822
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1823
|
+
attributes.forEach((attribute) => {
|
|
1824
|
+
this.log.info(
|
|
1825
|
+
`- 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}`,
|
|
1826
|
+
);
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
// Log SwitchCluster
|
|
1830
|
+
cluster = SwitchCluster;
|
|
1831
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1832
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1833
|
+
});
|
|
1834
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1835
|
+
attributes.forEach((attribute) => {
|
|
1836
|
+
this.log.info(
|
|
1837
|
+
`- 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}`,
|
|
1838
|
+
);
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
this.log.info('Subscribing to all attributes and events');
|
|
1842
|
+
await node.subscribeAllAttributesAndEvents({
|
|
1843
|
+
ignoreInitialTriggers: false,
|
|
1844
|
+
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
|
|
1845
|
+
this.log.info(
|
|
1846
|
+
`***${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}`,
|
|
1847
|
+
),
|
|
1848
|
+
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
|
|
1849
|
+
this.log.info(
|
|
1850
|
+
`***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
|
|
1851
|
+
);
|
|
1852
|
+
},
|
|
1853
|
+
});
|
|
1854
|
+
this.log.info('Subscribed to all attributes and events');
|
|
1855
|
+
}
|
|
1856
|
+
*/
|
|
1353
1857
|
}
|
|
1858
|
+
/** ***********************************************************************************************************************************/
|
|
1859
|
+
/** Matter.js methods */
|
|
1860
|
+
/** ***********************************************************************************************************************************/
|
|
1861
|
+
/**
|
|
1862
|
+
* Starts the matter storage process with name Matterbridge.
|
|
1863
|
+
* @returns {Promise<void>} - A promise that resolves when the storage process is started.
|
|
1864
|
+
*/
|
|
1354
1865
|
async startMatterStorage() {
|
|
1866
|
+
// Setup Matter storage
|
|
1355
1867
|
this.log.info(`Starting matter node storage...`);
|
|
1356
1868
|
this.matterStorageService = this.environment.get(StorageService);
|
|
1357
1869
|
this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
|
|
1358
1870
|
this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
|
|
1359
1871
|
this.log.info('Matter node storage manager "Matterbridge" created');
|
|
1360
1872
|
this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
|
|
1873
|
+
this.matterbridgeInformation.matterbridgeSerialNumber = await this.matterbridgeContext.get('serialNumber', '');
|
|
1361
1874
|
this.log.info('Matter node storage started');
|
|
1875
|
+
// Backup matter storage since it is created/opened correctly
|
|
1362
1876
|
await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
|
|
1363
1877
|
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Makes a backup copy of the specified matter storage directory.
|
|
1880
|
+
*
|
|
1881
|
+
* @param storageName - The name of the storage directory to be backed up.
|
|
1882
|
+
* @param backupName - The name of the backup directory to be created.
|
|
1883
|
+
* @returns {Promise<void>} A promise that resolves when the has been done.
|
|
1884
|
+
*/
|
|
1364
1885
|
async backupMatterStorage(storageName, backupName) {
|
|
1365
1886
|
this.log.info('Creating matter node storage backup...');
|
|
1366
1887
|
await copyDirectory(storageName, backupName);
|
|
1367
1888
|
this.log.info('Created matter node storage backup');
|
|
1368
1889
|
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Stops the matter storage.
|
|
1892
|
+
* @returns {Promise<void>} A promise that resolves when the storage is stopped.
|
|
1893
|
+
*/
|
|
1369
1894
|
async stopMatterStorage() {
|
|
1370
1895
|
this.log.info('Closing matter node storage...');
|
|
1371
|
-
this.matterStorageManager?.close();
|
|
1896
|
+
await this.matterStorageManager?.close();
|
|
1372
1897
|
this.matterStorageService = undefined;
|
|
1373
1898
|
this.matterStorageManager = undefined;
|
|
1374
1899
|
this.matterbridgeContext = undefined;
|
|
1375
1900
|
this.log.info('Matter node storage closed');
|
|
1376
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Creates a server node storage context.
|
|
1904
|
+
*
|
|
1905
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1906
|
+
* @param {string} deviceName - The name of the device.
|
|
1907
|
+
* @param {DeviceTypeId} deviceType - The device type of the device.
|
|
1908
|
+
* @param {number} vendorId - The vendor ID.
|
|
1909
|
+
* @param {string} vendorName - The vendor name.
|
|
1910
|
+
* @param {number} productId - The product ID.
|
|
1911
|
+
* @param {string} productName - The product name.
|
|
1912
|
+
* @param {string} [serialNumber] - The serial number of the device (optional).
|
|
1913
|
+
* @returns {Promise<StorageContext>} The storage context for the commissioning server.
|
|
1914
|
+
*/
|
|
1377
1915
|
async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
|
|
1378
1916
|
const { randomBytes } = await import('node:crypto');
|
|
1379
1917
|
if (!this.matterStorageService)
|
|
@@ -1407,6 +1945,15 @@ export class Matterbridge extends EventEmitter {
|
|
|
1407
1945
|
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
1408
1946
|
return storageContext;
|
|
1409
1947
|
}
|
|
1948
|
+
/**
|
|
1949
|
+
* Creates a server node.
|
|
1950
|
+
*
|
|
1951
|
+
* @param {StorageContext} storageContext - The storage context for the server node.
|
|
1952
|
+
* @param {number} [port=5540] - The port number for the server node. Defaults to 5540.
|
|
1953
|
+
* @param {number} [passcode=20242025] - The passcode for the server node. Defaults to 20242025.
|
|
1954
|
+
* @param {number} [discriminator=3850] - The discriminator for the server node. Defaults to 3850.
|
|
1955
|
+
* @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
|
|
1956
|
+
*/
|
|
1410
1957
|
async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
|
|
1411
1958
|
const storeId = await storageContext.get('storeId');
|
|
1412
1959
|
this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
|
|
@@ -1416,21 +1963,33 @@ export class Matterbridge extends EventEmitter {
|
|
|
1416
1963
|
this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
1417
1964
|
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
1418
1965
|
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
1966
|
+
/**
|
|
1967
|
+
* Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
|
|
1968
|
+
*/
|
|
1419
1969
|
const serverNode = await ServerNode.create({
|
|
1970
|
+
// Required: Give the Node a unique ID which is used to store the state of this node
|
|
1420
1971
|
id: storeId,
|
|
1972
|
+
// Provide Network relevant configuration like the port
|
|
1973
|
+
// Optional when operating only one device on a host, Default port is 5540
|
|
1421
1974
|
network: {
|
|
1422
1975
|
listeningAddressIpv4: this.ipv4address,
|
|
1423
1976
|
listeningAddressIpv6: this.ipv6address,
|
|
1424
1977
|
port,
|
|
1425
1978
|
},
|
|
1979
|
+
// Provide Commissioning relevant settings
|
|
1980
|
+
// Optional for development/testing purposes
|
|
1426
1981
|
commissioning: {
|
|
1427
1982
|
passcode,
|
|
1428
1983
|
discriminator,
|
|
1429
1984
|
},
|
|
1985
|
+
// Provide Node announcement settings
|
|
1986
|
+
// Optional: If Ommitted some development defaults are used
|
|
1430
1987
|
productDescription: {
|
|
1431
1988
|
name: await storageContext.get('deviceName'),
|
|
1432
1989
|
deviceType: DeviceTypeId(await storageContext.get('deviceType')),
|
|
1433
1990
|
},
|
|
1991
|
+
// Provide defaults for the BasicInformation cluster on the Root endpoint
|
|
1992
|
+
// Optional: If Omitted some development defaults are used
|
|
1434
1993
|
basicInformation: {
|
|
1435
1994
|
vendorId: VendorId(await storageContext.get('vendorId')),
|
|
1436
1995
|
vendorName: await storageContext.get('vendorName'),
|
|
@@ -1448,12 +2007,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1448
2007
|
},
|
|
1449
2008
|
});
|
|
1450
2009
|
const sanitizeFabrics = (fabrics, resetSessions = false) => {
|
|
2010
|
+
// New type of fabric information: Record<FabricIndex, ExposedFabricInformation>
|
|
1451
2011
|
const sanitizedFabrics = this.sanitizeFabricInformations(Array.from(Object.values(fabrics)));
|
|
1452
2012
|
this.log.info(`Fabrics: ${debugStringify(sanitizedFabrics)}`);
|
|
1453
2013
|
if (this.bridgeMode === 'bridge') {
|
|
1454
2014
|
this.matterbridgeFabricInformations = sanitizedFabrics;
|
|
1455
2015
|
if (resetSessions)
|
|
1456
|
-
this.matterbridgeSessionInformations = undefined;
|
|
2016
|
+
this.matterbridgeSessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
|
|
1457
2017
|
this.matterbridgePaired = true;
|
|
1458
2018
|
}
|
|
1459
2019
|
if (this.bridgeMode === 'childbridge') {
|
|
@@ -1461,13 +2021,19 @@ export class Matterbridge extends EventEmitter {
|
|
|
1461
2021
|
if (plugin) {
|
|
1462
2022
|
plugin.fabricInformations = sanitizedFabrics;
|
|
1463
2023
|
if (resetSessions)
|
|
1464
|
-
plugin.sessionInformations = undefined;
|
|
2024
|
+
plugin.sessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
|
|
1465
2025
|
plugin.paired = true;
|
|
1466
2026
|
}
|
|
1467
2027
|
}
|
|
1468
2028
|
};
|
|
2029
|
+
/**
|
|
2030
|
+
* This event is triggered when the device is initially commissioned successfully.
|
|
2031
|
+
* This means: It is added to the first fabric.
|
|
2032
|
+
*/
|
|
1469
2033
|
serverNode.lifecycle.commissioned.on(() => this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`));
|
|
2034
|
+
/** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
|
|
1470
2035
|
serverNode.lifecycle.decommissioned.on(() => this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`));
|
|
2036
|
+
/** This event is triggered when the device went online. This means that it is discoverable in the network. */
|
|
1471
2037
|
serverNode.lifecycle.online.on(async () => {
|
|
1472
2038
|
this.log.notice(`Server node for ${storeId} is online`);
|
|
1473
2039
|
if (!serverNode.lifecycle.isCommissioned) {
|
|
@@ -1507,6 +2073,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
1507
2073
|
}
|
|
1508
2074
|
}
|
|
1509
2075
|
setTimeout(() => {
|
|
2076
|
+
if (serverNode.lifecycle.isCommissioned)
|
|
2077
|
+
return;
|
|
1510
2078
|
if (this.bridgeMode === 'bridge') {
|
|
1511
2079
|
this.matterbridgeQrPairingCode = undefined;
|
|
1512
2080
|
this.matterbridgeManualPairingCode = undefined;
|
|
@@ -1516,10 +2084,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1516
2084
|
if (plugin) {
|
|
1517
2085
|
plugin.qrPairingCode = undefined;
|
|
1518
2086
|
plugin.manualPairingCode = undefined;
|
|
1519
|
-
this.frontend.wssSendRefreshRequired('plugins');
|
|
1520
2087
|
}
|
|
1521
2088
|
}
|
|
2089
|
+
this.frontend.wssSendRefreshRequired('plugins');
|
|
1522
2090
|
this.frontend.wssSendRefreshRequired('settings');
|
|
2091
|
+
this.frontend.wssSendRefreshRequired('fabrics');
|
|
2092
|
+
this.frontend.wssSendRefreshRequired('sessions');
|
|
1523
2093
|
this.frontend.wssSendSnackbarMessage(`Advertising on server node for ${storeId} stopped. Restart to commission.`, 0);
|
|
1524
2094
|
this.log.notice(`Advertising on server node for ${storeId} stopped. Restart to commission.`);
|
|
1525
2095
|
}, 15 * 60 * 1000).unref();
|
|
@@ -1532,6 +2102,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1532
2102
|
this.frontend.wssSendRefreshRequired('settings');
|
|
1533
2103
|
this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
|
|
1534
2104
|
});
|
|
2105
|
+
/** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
|
|
1535
2106
|
serverNode.lifecycle.offline.on(() => {
|
|
1536
2107
|
this.log.notice(`Server node for ${storeId} is offline`);
|
|
1537
2108
|
if (this.bridgeMode === 'bridge') {
|
|
@@ -1555,6 +2126,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1555
2126
|
this.frontend.wssSendRefreshRequired('settings');
|
|
1556
2127
|
this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
|
|
1557
2128
|
});
|
|
2129
|
+
/**
|
|
2130
|
+
* This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
|
|
2131
|
+
* information is needed.
|
|
2132
|
+
*/
|
|
1558
2133
|
serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
|
|
1559
2134
|
let action = '';
|
|
1560
2135
|
switch (fabricAction) {
|
|
@@ -1588,16 +2163,24 @@ export class Matterbridge extends EventEmitter {
|
|
|
1588
2163
|
}
|
|
1589
2164
|
}
|
|
1590
2165
|
};
|
|
2166
|
+
/**
|
|
2167
|
+
* This event is triggered when an operative new session was opened by a Controller.
|
|
2168
|
+
* It is not triggered for the initial commissioning process, just afterwards for real connections.
|
|
2169
|
+
*/
|
|
1591
2170
|
serverNode.events.sessions.opened.on((session) => {
|
|
1592
2171
|
this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1593
2172
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
1594
2173
|
this.frontend.wssSendRefreshRequired('sessions');
|
|
1595
2174
|
});
|
|
2175
|
+
/**
|
|
2176
|
+
* This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
|
|
2177
|
+
*/
|
|
1596
2178
|
serverNode.events.sessions.closed.on((session) => {
|
|
1597
2179
|
this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1598
2180
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
1599
2181
|
this.frontend.wssSendRefreshRequired('sessions');
|
|
1600
2182
|
});
|
|
2183
|
+
/** This event is triggered when a subscription gets added or removed on an operative session. */
|
|
1601
2184
|
serverNode.events.sessions.subscriptionsChanged.on((session) => {
|
|
1602
2185
|
this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1603
2186
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
@@ -1606,24 +2189,42 @@ export class Matterbridge extends EventEmitter {
|
|
|
1606
2189
|
this.log.info(`Created server node for ${storeId}`);
|
|
1607
2190
|
return serverNode;
|
|
1608
2191
|
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Starts the specified server node.
|
|
2194
|
+
*
|
|
2195
|
+
* @param {ServerNode} [matterServerNode] - The server node to start.
|
|
2196
|
+
* @returns {Promise<void>} A promise that resolves when the server node has started.
|
|
2197
|
+
*/
|
|
1609
2198
|
async startServerNode(matterServerNode) {
|
|
1610
2199
|
if (!matterServerNode)
|
|
1611
2200
|
return;
|
|
1612
2201
|
this.log.notice(`Starting ${matterServerNode.id} server node`);
|
|
1613
2202
|
await matterServerNode.start();
|
|
1614
2203
|
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Stops the specified server node.
|
|
2206
|
+
*
|
|
2207
|
+
* @param {ServerNode} matterServerNode - The server node to stop.
|
|
2208
|
+
* @returns {Promise<void>} A promise that resolves when the server node has stopped.
|
|
2209
|
+
*/
|
|
1615
2210
|
async stopServerNode(matterServerNode) {
|
|
1616
2211
|
if (!matterServerNode)
|
|
1617
2212
|
return;
|
|
1618
2213
|
this.log.notice(`Closing ${matterServerNode.id} server node`);
|
|
1619
2214
|
try {
|
|
1620
|
-
await withTimeout(matterServerNode.close(), 30000);
|
|
2215
|
+
await withTimeout(matterServerNode.close(), 30000); // 30 seconds timeout to allow slow devices to close gracefully
|
|
1621
2216
|
this.log.info(`Closed ${matterServerNode.id} server node`);
|
|
1622
2217
|
}
|
|
1623
2218
|
catch (error) {
|
|
1624
2219
|
this.log.error(`Failed to close ${matterServerNode.id} server node: ${error instanceof Error ? error.message : error}`);
|
|
1625
2220
|
}
|
|
1626
2221
|
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Advertises the specified server node.
|
|
2224
|
+
*
|
|
2225
|
+
* @param {ServerNode} [matterServerNode] - The server node to advertise.
|
|
2226
|
+
* @returns {Promise<{ qrPairingCode: string, manualPairingCode: string } | undefined>} A promise that resolves to the pairing codes if the server node is advertised, or undefined if not.
|
|
2227
|
+
*/
|
|
1627
2228
|
async advertiseServerNode(matterServerNode) {
|
|
1628
2229
|
if (matterServerNode) {
|
|
1629
2230
|
await matterServerNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
|
|
@@ -1632,65 +2233,95 @@ export class Matterbridge extends EventEmitter {
|
|
|
1632
2233
|
return { qrPairingCode, manualPairingCode };
|
|
1633
2234
|
}
|
|
1634
2235
|
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Stop advertise the specified server node.
|
|
2238
|
+
*
|
|
2239
|
+
* @param {ServerNode} [matterServerNode] - The server node to advertise.
|
|
2240
|
+
* @returns {Promise<void>} A promise that resolves when the server node has stopped advertising.
|
|
2241
|
+
*/
|
|
1635
2242
|
async stopAdvertiseServerNode(matterServerNode) {
|
|
1636
2243
|
if (matterServerNode && matterServerNode.lifecycle.isOnline) {
|
|
1637
2244
|
await matterServerNode.env.get(DeviceCommissioner)?.endCommissioning();
|
|
1638
2245
|
this.log.notice(`Stopped advertising for ${matterServerNode.id}`);
|
|
1639
2246
|
}
|
|
1640
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Creates an aggregator node with the specified storage context.
|
|
2250
|
+
*
|
|
2251
|
+
* @param {StorageContext} storageContext - The storage context for the aggregator node.
|
|
2252
|
+
* @returns {Promise<EndpointNode<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
|
|
2253
|
+
*/
|
|
1641
2254
|
async createAggregatorNode(storageContext) {
|
|
1642
2255
|
this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator `);
|
|
1643
2256
|
const aggregatorNode = new EndpointNode(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
|
|
1644
2257
|
return aggregatorNode;
|
|
1645
2258
|
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Adds a MatterbridgeEndpoint to the specified plugin.
|
|
2261
|
+
*
|
|
2262
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2263
|
+
* @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
|
|
2264
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been added.
|
|
2265
|
+
*/
|
|
1646
2266
|
async addBridgedEndpoint(pluginName, device) {
|
|
2267
|
+
// Check if the plugin is registered
|
|
1647
2268
|
const plugin = this.plugins.get(pluginName);
|
|
1648
2269
|
if (!plugin) {
|
|
1649
2270
|
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
|
|
1650
2271
|
return;
|
|
1651
2272
|
}
|
|
1652
2273
|
if (this.bridgeMode === 'bridge') {
|
|
2274
|
+
// Register and add the device to the matterbridge aggregator node
|
|
1653
2275
|
this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
|
|
1654
|
-
if (!this.aggregatorNode)
|
|
2276
|
+
if (!this.aggregatorNode) {
|
|
1655
2277
|
this.log.error('Aggregator node not found for Matterbridge');
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
1656
2280
|
try {
|
|
1657
|
-
await this.aggregatorNode
|
|
2281
|
+
await this.aggregatorNode.add(device);
|
|
1658
2282
|
}
|
|
1659
2283
|
catch (error) {
|
|
1660
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
|
|
2284
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2285
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2286
|
+
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for plugin ${plg}${pluginName}${er}: ${errorMessage}\nstack: ${errorInspect}`);
|
|
1664
2287
|
return;
|
|
1665
2288
|
}
|
|
1666
2289
|
}
|
|
1667
2290
|
else if (this.bridgeMode === 'childbridge') {
|
|
2291
|
+
// Register and add the device to the plugin server node
|
|
1668
2292
|
if (plugin.type === 'AccessoryPlatform') {
|
|
1669
2293
|
try {
|
|
1670
2294
|
this.log.debug(`Creating endpoint ${dev}${device.deviceName}${db} for AccessoryPlatform plugin ${plg}${plugin.name}${db} server node`);
|
|
2295
|
+
if (plugin.serverNode) {
|
|
2296
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} has already added a device. Only one device is allowed per AccessoryPlatform plugin.`);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
1671
2299
|
await this.createAccessoryPlugin(plugin, device);
|
|
1672
2300
|
}
|
|
1673
2301
|
catch (error) {
|
|
1674
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
this.log.error(`Error creating endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for AccessoryPlatform plugin ${plg}${pluginName}${er} server node: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
|
|
2302
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2303
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2304
|
+
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}`);
|
|
1678
2305
|
return;
|
|
1679
2306
|
}
|
|
1680
2307
|
}
|
|
2308
|
+
// Register and add the device to the plugin aggregator node
|
|
1681
2309
|
if (plugin.type === 'DynamicPlatform') {
|
|
1682
|
-
plugin.locked = true;
|
|
1683
|
-
this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
|
|
1684
|
-
if (!plugin.aggregatorNode)
|
|
1685
|
-
this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${db}`);
|
|
1686
2310
|
try {
|
|
1687
|
-
|
|
2311
|
+
this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
|
|
2312
|
+
await this.createDynamicPlugin(plugin);
|
|
2313
|
+
// Fast plugins can add another device before the server node is created
|
|
2314
|
+
await waiter(`createDynamicPlugin(${plugin.name})`, () => plugin.serverNode?.hasParts === true);
|
|
2315
|
+
if (!plugin.aggregatorNode) {
|
|
2316
|
+
this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
await plugin.aggregatorNode.add(device);
|
|
1688
2320
|
}
|
|
1689
2321
|
catch (error) {
|
|
1690
|
-
const errorMessage = error instanceof Error ? error.message :
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) for DynamicPlatform plugin ${plg}${pluginName}${er} aggregator node: ${error} ${errorMessage} ${errorStack} ${errorDebug}`);
|
|
2322
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
2323
|
+
const errorInspect = inspect(error, { depth: 10 });
|
|
2324
|
+
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}`);
|
|
1694
2325
|
return;
|
|
1695
2326
|
}
|
|
1696
2327
|
}
|
|
@@ -1699,17 +2330,28 @@ export class Matterbridge extends EventEmitter {
|
|
|
1699
2330
|
plugin.registeredDevices++;
|
|
1700
2331
|
if (plugin.addedDevices !== undefined)
|
|
1701
2332
|
plugin.addedDevices++;
|
|
2333
|
+
// Add the device to the DeviceManager
|
|
1702
2334
|
this.devices.set(device);
|
|
2335
|
+
// Subscribe to the reachable$Changed event
|
|
1703
2336
|
await this.subscribeAttributeChanged(plugin, device);
|
|
1704
2337
|
this.log.info(`Added and registered bridged endpoint (${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) for plugin ${plg}${pluginName}${nf}`);
|
|
1705
2338
|
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Removes a MatterbridgeEndpoint from the specified plugin.
|
|
2341
|
+
*
|
|
2342
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2343
|
+
* @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
|
|
2344
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been removed.
|
|
2345
|
+
*/
|
|
1706
2346
|
async removeBridgedEndpoint(pluginName, device) {
|
|
1707
2347
|
this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
|
|
2348
|
+
// Check if the plugin is registered
|
|
1708
2349
|
const plugin = this.plugins.get(pluginName);
|
|
1709
2350
|
if (!plugin) {
|
|
1710
2351
|
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
1711
2352
|
return;
|
|
1712
2353
|
}
|
|
2354
|
+
// Register and add the device to the matterbridge aggregator node
|
|
1713
2355
|
if (this.bridgeMode === 'bridge') {
|
|
1714
2356
|
if (!this.aggregatorNode) {
|
|
1715
2357
|
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
|
|
@@ -1724,6 +2366,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1724
2366
|
}
|
|
1725
2367
|
else if (this.bridgeMode === 'childbridge') {
|
|
1726
2368
|
if (plugin.type === 'AccessoryPlatform') {
|
|
2369
|
+
// Nothing to do here since the server node has no aggregator node but only the device itself
|
|
1727
2370
|
}
|
|
1728
2371
|
else if (plugin.type === 'DynamicPlatform') {
|
|
1729
2372
|
if (!plugin.aggregatorNode) {
|
|
@@ -1738,8 +2381,21 @@ export class Matterbridge extends EventEmitter {
|
|
|
1738
2381
|
if (plugin.addedDevices !== undefined)
|
|
1739
2382
|
plugin.addedDevices--;
|
|
1740
2383
|
}
|
|
2384
|
+
// Remove the device from the DeviceManager
|
|
1741
2385
|
this.devices.remove(device);
|
|
1742
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Removes all bridged endpoints from the specified plugin.
|
|
2389
|
+
*
|
|
2390
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2391
|
+
* @param {number} [delay=0] - The delay in milliseconds between removing each bridged endpoint (default: 0).
|
|
2392
|
+
* @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
|
|
2393
|
+
*
|
|
2394
|
+
* @remarks
|
|
2395
|
+
* This method iterates through all devices in the DeviceManager and removes each bridged endpoint associated with the specified plugin.
|
|
2396
|
+
* It also applies a delay between each removal if specified.
|
|
2397
|
+
* The delay is useful to allow the controllers to receive a single subscription for each device removed.
|
|
2398
|
+
*/
|
|
1743
2399
|
async removeAllBridgedEndpoints(pluginName, delay = 0) {
|
|
1744
2400
|
this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}`);
|
|
1745
2401
|
for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
|
|
@@ -1750,9 +2406,25 @@ export class Matterbridge extends EventEmitter {
|
|
|
1750
2406
|
if (delay > 0)
|
|
1751
2407
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1752
2408
|
}
|
|
2409
|
+
/**
|
|
2410
|
+
* Subscribes to the attribute change event for the given device and plugin.
|
|
2411
|
+
* Specifically, it listens for changes in the 'reachable' attribute of the
|
|
2412
|
+
* BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
|
|
2413
|
+
*
|
|
2414
|
+
* @param {RegisteredPlugin} plugin - The plugin associated with the device.
|
|
2415
|
+
* @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
|
|
2416
|
+
* @returns {Promise<void>} A promise that resolves when the subscription is set up.
|
|
2417
|
+
*/
|
|
1753
2418
|
async subscribeAttributeChanged(plugin, device) {
|
|
1754
2419
|
this.log.info(`Subscribing attributes for endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) plugin ${plg}${plugin.name}${nf}`);
|
|
1755
2420
|
if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && plugin.serverNode) {
|
|
2421
|
+
/*
|
|
2422
|
+
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) subscribed to reachable$Changed`);
|
|
2423
|
+
setTimeout(async () => {
|
|
2424
|
+
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) changed to reachable false`);
|
|
2425
|
+
await plugin.serverNode?.setStateOf(BasicInformationServer, { reachable: false });
|
|
2426
|
+
}, 60000).unref();
|
|
2427
|
+
*/
|
|
1756
2428
|
plugin.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
|
|
1757
2429
|
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) is ${reachable ? 'reachable' : 'unreachable'}`);
|
|
1758
2430
|
this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, 'BasicInformationServer', 'reachable', reachable);
|
|
@@ -1765,6 +2437,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1765
2437
|
});
|
|
1766
2438
|
}
|
|
1767
2439
|
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2442
|
+
*
|
|
2443
|
+
* @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
|
|
2444
|
+
* @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
|
|
2445
|
+
*/
|
|
1768
2446
|
sanitizeFabricInformations(fabricInfo) {
|
|
1769
2447
|
return fabricInfo.map((info) => {
|
|
1770
2448
|
return {
|
|
@@ -1778,6 +2456,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1778
2456
|
};
|
|
1779
2457
|
});
|
|
1780
2458
|
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2461
|
+
*
|
|
2462
|
+
* @param {SessionInformation[]} sessionInfo - The array of session information objects.
|
|
2463
|
+
* @returns {SanitizedSessionInformation[]} An array of sanitized session information objects.
|
|
2464
|
+
*/
|
|
1781
2465
|
sanitizeSessionInformation(sessionInfo) {
|
|
1782
2466
|
return sessionInfo
|
|
1783
2467
|
.filter((session) => session.isPeerActive)
|
|
@@ -1805,7 +2489,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
1805
2489
|
};
|
|
1806
2490
|
});
|
|
1807
2491
|
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Sets the reachability of the specified aggregator node bridged devices and trigger.
|
|
2494
|
+
* @param {EndpointNode<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
|
|
2495
|
+
* @param {boolean} reachable - A boolean indicating the reachability status to set.
|
|
2496
|
+
*/
|
|
2497
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1808
2498
|
async setAggregatorReachability(aggregatorNode, reachable) {
|
|
2499
|
+
/*
|
|
2500
|
+
for (const child of aggregatorNode.parts) {
|
|
2501
|
+
this.log.debug(`Setting reachability of ${(child as unknown as MatterbridgeEndpoint)?.deviceName} to ${reachable}`);
|
|
2502
|
+
await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
|
|
2503
|
+
child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
|
|
2504
|
+
}
|
|
2505
|
+
*/
|
|
1809
2506
|
}
|
|
1810
2507
|
getVendorIdName = (vendorId) => {
|
|
1811
2508
|
if (!vendorId)
|
|
@@ -1848,14 +2545,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
1848
2545
|
}
|
|
1849
2546
|
return vendorName;
|
|
1850
2547
|
};
|
|
2548
|
+
/**
|
|
2549
|
+
* Spawns a child process with the given command and arguments.
|
|
2550
|
+
* @param {string} command - The command to execute.
|
|
2551
|
+
* @param {string[]} args - The arguments to pass to the command (default: []).
|
|
2552
|
+
* @returns {Promise<boolean>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
|
|
2553
|
+
*/
|
|
1851
2554
|
async spawnCommand(command, args = []) {
|
|
1852
2555
|
const { spawn } = await import('node:child_process');
|
|
2556
|
+
/*
|
|
2557
|
+
npm > npm.cmd on windows
|
|
2558
|
+
cmd.exe ['dir'] on windows
|
|
2559
|
+
await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
|
|
2560
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
2561
|
+
this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
2562
|
+
});
|
|
2563
|
+
|
|
2564
|
+
spawn - [14:27:21.125] [Matterbridge:spawn]: changed 38 packages in 4s
|
|
2565
|
+
spawn - [14:27:21.125] [Matterbridge:spawn]: 10 packages are looking for funding run `npm fund` for details
|
|
2566
|
+
debug - [14:27:21.131] [Matterbridge]: Child process exited with code 0 and signal null
|
|
2567
|
+
debug - [14:27:21.131] [Matterbridge]: Child process stdio streams have closed with code 0
|
|
2568
|
+
*/
|
|
1853
2569
|
const cmdLine = command + ' ' + args.join(' ');
|
|
1854
2570
|
if (process.platform === 'win32' && command === 'npm') {
|
|
2571
|
+
// Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
|
|
1855
2572
|
const argstring = 'npm ' + args.join(' ');
|
|
1856
2573
|
args.splice(0, args.length, '/c', argstring);
|
|
1857
2574
|
command = 'cmd.exe';
|
|
1858
2575
|
}
|
|
2576
|
+
// Decide when using sudo on linux
|
|
2577
|
+
// When you need sudo: Spawn stderr: npm error Error: EACCES: permission denied
|
|
2578
|
+
// When you don't need sudo: Failed to start child process "npm install -g matterbridge-eve-door": spawn sudo ENOENT
|
|
1859
2579
|
if (hasParameter('sudo') || (process.platform === 'linux' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo'))) {
|
|
1860
2580
|
args.unshift(command);
|
|
1861
2581
|
command = 'sudo';
|
|
@@ -1914,3 +2634,4 @@ export class Matterbridge extends EventEmitter {
|
|
|
1914
2634
|
});
|
|
1915
2635
|
}
|
|
1916
2636
|
}
|
|
2637
|
+
//# sourceMappingURL=matterbridge.js.map
|