matterbridge 3.0.2-dev-20250514-0b26f0a → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +29 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +37 -2
- 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 +241 -0
- package/dist/frontend.d.ts.map +1 -0
- package/dist/frontend.js +334 -15
- package/dist/frontend.js.map +1 -0
- package/dist/helpers.d.ts +46 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +49 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +36 -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 +435 -0
- package/dist/matterbridge.d.ts.map +1 -0
- package/dist/matterbridge.js +746 -47
- 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 +1188 -0
- package/dist/matterbridgeBehaviors.d.ts.map +1 -0
- package/dist/matterbridgeBehaviors.js +53 -4
- 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 +965 -0
- package/dist/matterbridgeEndpoint.d.ts.map +1 -0
- package/dist/matterbridgeEndpoint.js +807 -11
- package/dist/matterbridgeEndpoint.js.map +1 -0
- package/dist/matterbridgeEndpointHelpers.d.ts +2728 -0
- package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
- package/dist/matterbridgeEndpointHelpers.js +147 -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 +225 -7
- 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 +273 -0
- package/dist/pluginManager.d.ts.map +1 -0
- package/dist/pluginManager.js +264 -3
- package/dist/pluginManager.js.map +1 -0
- package/dist/roboticVacuumCleaner.d.ts +43 -0
- package/dist/roboticVacuumCleaner.d.ts.map +1 -0
- package/dist/roboticVacuumCleaner.js +39 -3
- package/dist/roboticVacuumCleaner.js.map +1 -0
- package/dist/shelly.d.ts +153 -0
- package/dist/shelly.d.ts.map +1 -0
- package/dist/shelly.js +155 -7
- package/dist/shelly.js.map +1 -0
- package/dist/storage/export.d.ts +2 -0
- package/dist/storage/export.d.ts.map +1 -0
- package/dist/storage/export.js +1 -0
- package/dist/storage/export.js.map +1 -0
- package/dist/update.d.ts +58 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +53 -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/commandLine.d.ts +58 -0
- package/dist/utils/commandLine.d.ts.map +1 -0
- package/dist/utils/commandLine.js +53 -0
- package/dist/utils/commandLine.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 +38 -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 +71 -1
- package/dist/utils/deepEqual.js.map +1 -0
- package/dist/utils/export.d.ts +11 -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/hex.d.ts +48 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/hex.js +57 -0
- package/dist/utils/hex.js.map +1 -0
- package/dist/utils/isvalid.d.ts +102 -0
- package/dist/utils/isvalid.d.ts.map +1 -0
- package/dist/utils/isvalid.js +100 -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/wait.d.ts +51 -0
- package/dist/utils/wait.d.ts.map +1 -0
- package/dist/utils/wait.js +53 -5
- package/dist/utils/wait.js.map +1 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/matterbridge.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
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.3
|
|
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';
|
|
33
|
+
// Matterbridge
|
|
8
34
|
import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout, waiter, isValidString, parseVersionString, isValidNumber } from './utils/export.js';
|
|
9
35
|
import { logInterfaces, getGlobalNodeModules } from './utils/network.js';
|
|
10
36
|
import { dev, plg, typ } from './matterbridgeTypes.js';
|
|
@@ -14,11 +40,15 @@ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
|
14
40
|
import { bridge } from './matterbridgeDeviceTypes.js';
|
|
15
41
|
import { Frontend } from './frontend.js';
|
|
16
42
|
import { addVirtualDevices } from './helpers.js';
|
|
43
|
+
// @matter
|
|
17
44
|
import { DeviceTypeId, Endpoint, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode, UINT32_MAX, UINT16_MAX, } from '@matter/main';
|
|
18
45
|
import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol';
|
|
19
46
|
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
20
47
|
import { BasicInformationServer } from '@matter/main/behaviors/basic-information';
|
|
21
48
|
import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information';
|
|
49
|
+
/**
|
|
50
|
+
* Represents the Matterbridge application.
|
|
51
|
+
*/
|
|
22
52
|
export class Matterbridge extends EventEmitter {
|
|
23
53
|
systemInformation = {
|
|
24
54
|
interfaceName: '',
|
|
@@ -65,7 +95,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
65
95
|
shellySysUpdate: false,
|
|
66
96
|
shellyMainUpdate: false,
|
|
67
97
|
profile: getParameter('profile'),
|
|
68
|
-
loggerLevel: "info"
|
|
98
|
+
loggerLevel: "info" /* LogLevel.INFO */,
|
|
69
99
|
fileLogger: false,
|
|
70
100
|
matterLoggerLevel: MatterLogLevel.INFO,
|
|
71
101
|
matterFileLogger: false,
|
|
@@ -104,9 +134,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
104
134
|
plugins;
|
|
105
135
|
devices;
|
|
106
136
|
frontend = new Frontend(this);
|
|
137
|
+
// Matterbridge storage
|
|
107
138
|
nodeStorage;
|
|
108
139
|
nodeContext;
|
|
109
140
|
nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
|
|
141
|
+
// Cleanup
|
|
110
142
|
hasCleanupStarted = false;
|
|
111
143
|
initialized = false;
|
|
112
144
|
execRunningCount = 0;
|
|
@@ -119,19 +151,22 @@ export class Matterbridge extends EventEmitter {
|
|
|
119
151
|
sigtermHandler;
|
|
120
152
|
exceptionHandler;
|
|
121
153
|
rejectionHandler;
|
|
154
|
+
// Matter environment
|
|
122
155
|
environment = Environment.default;
|
|
156
|
+
// Matter storage
|
|
123
157
|
matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
|
|
124
158
|
matterStorageService;
|
|
125
159
|
matterStorageManager;
|
|
126
160
|
matterbridgeContext;
|
|
127
161
|
controllerContext;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
162
|
+
// Matter parameters
|
|
163
|
+
mdnsInterface; // matter server node mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
|
|
164
|
+
ipv4address; // matter server node listeningAddressIpv4
|
|
165
|
+
ipv6address; // matter server node listeningAddressIpv6
|
|
166
|
+
port; // first server node port
|
|
167
|
+
passcode; // first server node passcode
|
|
168
|
+
discriminator; // first server node discriminator
|
|
169
|
+
certification; // device certification
|
|
135
170
|
serverNode;
|
|
136
171
|
aggregatorNode;
|
|
137
172
|
aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
|
|
@@ -139,21 +174,50 @@ export class Matterbridge extends EventEmitter {
|
|
|
139
174
|
aggregatorProductId = getIntParameter('productId') ?? 0x8000;
|
|
140
175
|
aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
|
|
141
176
|
static instance;
|
|
177
|
+
// We load asyncronously so is private
|
|
142
178
|
constructor() {
|
|
143
179
|
super();
|
|
144
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Emits an event of the specified type with the provided arguments.
|
|
183
|
+
*
|
|
184
|
+
* @template K - The type of the event.
|
|
185
|
+
* @param {K} eventName - The name of the event to emit.
|
|
186
|
+
* @param {...MatterbridgeEvent[K]} args - The arguments to pass to the event listeners.
|
|
187
|
+
* @returns {boolean} - Returns true if the event had listeners, false otherwise.
|
|
188
|
+
*/
|
|
145
189
|
emit(eventName, ...args) {
|
|
146
190
|
return super.emit(eventName, ...args);
|
|
147
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Registers an event listener for the specified event type.
|
|
194
|
+
*
|
|
195
|
+
* @template K - The type of the event.
|
|
196
|
+
* @param {K} eventName - The name of the event to listen for.
|
|
197
|
+
* @param {(...args: MatterbridgeEvent[K]) => void} listener - The callback function to invoke when the event is emitted.
|
|
198
|
+
* @returns {this} - Returns the instance of the Matterbridge class.
|
|
199
|
+
*/
|
|
148
200
|
on(eventName, listener) {
|
|
149
201
|
return super.on(eventName, listener);
|
|
150
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Retrieves the list of Matterbridge devices.
|
|
205
|
+
* @returns {MatterbridgeEndpoint[]} An array of MatterbridgeDevice objects.
|
|
206
|
+
*/
|
|
151
207
|
getDevices() {
|
|
152
208
|
return this.devices.array();
|
|
153
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Retrieves the list of registered plugins.
|
|
212
|
+
* @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects.
|
|
213
|
+
*/
|
|
154
214
|
getPlugins() {
|
|
155
215
|
return this.plugins.array();
|
|
156
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Set the logger logLevel for the Matterbridge classes.
|
|
219
|
+
* @param {LogLevel} logLevel The logger logLevel to set.
|
|
220
|
+
*/
|
|
157
221
|
async setLogLevel(logLevel) {
|
|
158
222
|
if (this.log)
|
|
159
223
|
this.log.logLevel = logLevel;
|
|
@@ -167,19 +231,31 @@ export class Matterbridge extends EventEmitter {
|
|
|
167
231
|
for (const plugin of this.plugins) {
|
|
168
232
|
if (!plugin.platform || !plugin.platform.log || !plugin.platform.config)
|
|
169
233
|
continue;
|
|
170
|
-
plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" : this.log.logLevel;
|
|
171
|
-
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" : this.log.logLevel);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
234
|
+
plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
|
|
235
|
+
await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
|
|
236
|
+
}
|
|
237
|
+
// Set the global logger callback for the WebSocketServer to the common minimum logLevel
|
|
238
|
+
let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
|
|
239
|
+
if (this.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
|
|
240
|
+
callbackLogLevel = "info" /* LogLevel.INFO */;
|
|
241
|
+
if (this.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
|
|
242
|
+
callbackLogLevel = "debug" /* LogLevel.DEBUG */;
|
|
178
243
|
AnsiLogger.setGlobalCallback(this.frontend.wssSendMessage.bind(this.frontend), callbackLogLevel);
|
|
179
244
|
this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
|
|
180
245
|
}
|
|
246
|
+
/** ***********************************************************************************************************************************/
|
|
247
|
+
/** loadInstance() and cleanup() methods */
|
|
248
|
+
/** ***********************************************************************************************************************************/
|
|
249
|
+
/**
|
|
250
|
+
* Loads an instance of the Matterbridge class.
|
|
251
|
+
* If an instance already exists, return that instance.
|
|
252
|
+
*
|
|
253
|
+
* @param initialize - Whether to initialize the Matterbridge instance after loading.
|
|
254
|
+
* @returns The loaded Matterbridge instance.
|
|
255
|
+
*/
|
|
181
256
|
static async loadInstance(initialize = false) {
|
|
182
257
|
if (!Matterbridge.instance) {
|
|
258
|
+
// eslint-disable-next-line no-console
|
|
183
259
|
if (hasParameter('debug'))
|
|
184
260
|
console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
|
|
185
261
|
Matterbridge.instance = new Matterbridge();
|
|
@@ -188,8 +264,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
188
264
|
}
|
|
189
265
|
return Matterbridge.instance;
|
|
190
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Call cleanup().
|
|
269
|
+
* @deprecated This method is deprecated and is only used for jest tests.
|
|
270
|
+
*
|
|
271
|
+
*/
|
|
191
272
|
async destroyInstance() {
|
|
192
273
|
this.log.info(`Destroy instance...`);
|
|
274
|
+
// Save server nodes to close
|
|
193
275
|
const servers = [];
|
|
194
276
|
if (this.bridgeMode === 'bridge') {
|
|
195
277
|
if (this.serverNode)
|
|
@@ -201,55 +283,81 @@ export class Matterbridge extends EventEmitter {
|
|
|
201
283
|
servers.push(plugin.serverNode);
|
|
202
284
|
}
|
|
203
285
|
}
|
|
286
|
+
// Cleanup
|
|
204
287
|
await this.cleanup('destroying instance...', false);
|
|
288
|
+
// Close servers mdns service
|
|
205
289
|
this.log.info(`Dispose ${servers.length} MdnsService...`);
|
|
206
290
|
for (const server of servers) {
|
|
207
291
|
await server.env.get(MdnsService)[Symbol.asyncDispose]();
|
|
208
292
|
this.log.info(`Closed ${server.id} MdnsService`);
|
|
209
293
|
}
|
|
294
|
+
// Wait for the cleanup to finish
|
|
210
295
|
await new Promise((resolve) => {
|
|
211
296
|
setTimeout(resolve, 1000);
|
|
212
297
|
});
|
|
213
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Initializes the Matterbridge application.
|
|
301
|
+
*
|
|
302
|
+
* @remarks
|
|
303
|
+
* This method performs the necessary setup and initialization steps for the Matterbridge application.
|
|
304
|
+
* It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
|
|
305
|
+
* node version, registers signal handlers, initializes storage, and parses the command line.
|
|
306
|
+
*
|
|
307
|
+
* @returns A Promise that resolves when the initialization is complete.
|
|
308
|
+
*/
|
|
214
309
|
async initialize() {
|
|
310
|
+
// Set the restart mode
|
|
215
311
|
if (hasParameter('service'))
|
|
216
312
|
this.restartMode = 'service';
|
|
217
313
|
if (hasParameter('docker'))
|
|
218
314
|
this.restartMode = 'docker';
|
|
315
|
+
// Set the matterbridge directory
|
|
219
316
|
this.homeDirectory = getParameter('homedir') ?? os.homedir();
|
|
220
317
|
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
318
|
+
// Setup the matter environment
|
|
221
319
|
this.environment.vars.set('log.level', MatterLogLevel.INFO);
|
|
222
320
|
this.environment.vars.set('log.format', MatterLogFormat.ANSI);
|
|
223
321
|
this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
224
322
|
this.environment.vars.set('runtime.signals', false);
|
|
225
323
|
this.environment.vars.set('runtime.exitcode', false);
|
|
226
|
-
|
|
324
|
+
// Create the matterbridge logger
|
|
325
|
+
this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
|
|
326
|
+
// Register process handlers
|
|
227
327
|
this.registerProcessHandlers();
|
|
328
|
+
// Initialize nodeStorage and nodeContext
|
|
228
329
|
try {
|
|
229
330
|
this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
|
|
230
331
|
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
|
|
231
332
|
this.log.debug('Creating node storage context for matterbridge');
|
|
232
333
|
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
334
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
336
|
const keys = (await this.nodeStorage?.storage.keys());
|
|
234
337
|
for (const key of keys) {
|
|
235
338
|
this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
|
|
339
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
236
340
|
await this.nodeStorage?.storage.get(key);
|
|
237
341
|
}
|
|
238
342
|
const storages = await this.nodeStorage.getStorageNames();
|
|
239
343
|
for (const storage of storages) {
|
|
240
344
|
this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
|
|
241
345
|
const nodeContext = await this.nodeStorage?.createStorage(storage);
|
|
346
|
+
// TODO: Remove this code when node-persist-manager is updated
|
|
347
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
242
348
|
const keys = (await nodeContext?.storage.keys());
|
|
243
349
|
keys.forEach(async (key) => {
|
|
244
350
|
this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
|
|
245
351
|
await nodeContext?.get(key);
|
|
246
352
|
});
|
|
247
353
|
}
|
|
354
|
+
// Creating a backup of the node storage since it is not corrupted
|
|
248
355
|
this.log.debug('Creating node storage backup...');
|
|
249
356
|
await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'));
|
|
250
357
|
this.log.debug('Created node storage backup');
|
|
251
358
|
}
|
|
252
359
|
catch (error) {
|
|
360
|
+
// Restoring the backup of the node storage since it is corrupted
|
|
253
361
|
this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
|
|
254
362
|
if (hasParameter('norestore')) {
|
|
255
363
|
this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
|
|
@@ -264,24 +372,32 @@ export class Matterbridge extends EventEmitter {
|
|
|
264
372
|
this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
|
|
265
373
|
throw new Error('Fatal error creating node storage manager and context for matterbridge');
|
|
266
374
|
}
|
|
375
|
+
// Set the first port to use for the commissioning server (will be incremented in childbridge mode)
|
|
267
376
|
this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
|
|
377
|
+
// Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
|
|
268
378
|
this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
|
|
379
|
+
// Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
|
|
269
380
|
this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
|
|
381
|
+
// Certificate management
|
|
270
382
|
const pairingFilePath = path.join(this.homeDirectory, '.mattercert', 'pairing.json');
|
|
271
383
|
try {
|
|
272
384
|
await fs.access(pairingFilePath, fs.constants.R_OK);
|
|
273
385
|
const pairingFileContent = await fs.readFile(pairingFilePath, 'utf8');
|
|
274
386
|
const pairingFileJson = JSON.parse(pairingFileContent);
|
|
387
|
+
// Override the passcode and discriminator if they are present in the pairing file
|
|
275
388
|
if (pairingFileJson.passcode && pairingFileJson.discriminator) {
|
|
276
389
|
this.passcode = pairingFileJson.passcode;
|
|
277
390
|
this.discriminator = pairingFileJson.discriminator;
|
|
278
391
|
this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf}`);
|
|
279
392
|
}
|
|
393
|
+
// Set the certification if it is present in the pairing file
|
|
280
394
|
if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
|
|
281
395
|
const hexStringToUint8Array = (hexString) => {
|
|
282
396
|
const matches = hexString.match(/.{1,2}/g);
|
|
283
397
|
return matches ? new Uint8Array(matches.map((byte) => parseInt(byte, 16))) : new Uint8Array();
|
|
284
398
|
};
|
|
399
|
+
// const hexString = Buffer.from('Test string', 'utf-8').toString('hex');
|
|
400
|
+
// console.log(hexString, Buffer.from(hexStringToUint8Array(hexString)).toString('utf-8'));
|
|
285
401
|
this.certification = {
|
|
286
402
|
privateKey: hexStringToUint8Array(pairingFileJson.privateKey),
|
|
287
403
|
certificate: hexStringToUint8Array(pairingFileJson.certificate),
|
|
@@ -294,41 +410,44 @@ export class Matterbridge extends EventEmitter {
|
|
|
294
410
|
catch (error) {
|
|
295
411
|
this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`);
|
|
296
412
|
}
|
|
413
|
+
// Store the passcode, discriminator and port in the node context
|
|
297
414
|
await this.nodeContext.set('matterport', this.port);
|
|
298
415
|
await this.nodeContext.set('matterpasscode', this.passcode);
|
|
299
416
|
await this.nodeContext.set('matterdiscriminator', this.discriminator);
|
|
300
417
|
this.log.debug(`Initializing server node for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
|
|
418
|
+
// Set matterbridge logger level (context: matterbridgeLogLevel)
|
|
301
419
|
if (hasParameter('logger')) {
|
|
302
420
|
const level = getParameter('logger');
|
|
303
421
|
if (level === 'debug') {
|
|
304
|
-
this.log.logLevel = "debug"
|
|
422
|
+
this.log.logLevel = "debug" /* LogLevel.DEBUG */;
|
|
305
423
|
}
|
|
306
424
|
else if (level === 'info') {
|
|
307
|
-
this.log.logLevel = "info"
|
|
425
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
308
426
|
}
|
|
309
427
|
else if (level === 'notice') {
|
|
310
|
-
this.log.logLevel = "notice"
|
|
428
|
+
this.log.logLevel = "notice" /* LogLevel.NOTICE */;
|
|
311
429
|
}
|
|
312
430
|
else if (level === 'warn') {
|
|
313
|
-
this.log.logLevel = "warn"
|
|
431
|
+
this.log.logLevel = "warn" /* LogLevel.WARN */;
|
|
314
432
|
}
|
|
315
433
|
else if (level === 'error') {
|
|
316
|
-
this.log.logLevel = "error"
|
|
434
|
+
this.log.logLevel = "error" /* LogLevel.ERROR */;
|
|
317
435
|
}
|
|
318
436
|
else if (level === 'fatal') {
|
|
319
|
-
this.log.logLevel = "fatal"
|
|
437
|
+
this.log.logLevel = "fatal" /* LogLevel.FATAL */;
|
|
320
438
|
}
|
|
321
439
|
else {
|
|
322
440
|
this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
|
|
323
|
-
this.log.logLevel = "info"
|
|
441
|
+
this.log.logLevel = "info" /* LogLevel.INFO */;
|
|
324
442
|
}
|
|
325
443
|
}
|
|
326
444
|
else {
|
|
327
|
-
this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" : "info");
|
|
445
|
+
this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */);
|
|
328
446
|
}
|
|
329
447
|
this.frontend.logLevel = this.log.logLevel;
|
|
330
448
|
MatterbridgeEndpoint.logLevel = this.log.logLevel;
|
|
331
449
|
this.matterbridgeInformation.loggerLevel = this.log.logLevel;
|
|
450
|
+
// Create the file logger for matterbridge (context: matterbridgeFileLog)
|
|
332
451
|
if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
|
|
333
452
|
AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true);
|
|
334
453
|
this.matterbridgeInformation.fileLogger = true;
|
|
@@ -337,6 +456,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
337
456
|
this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`);
|
|
338
457
|
if (this.profile !== undefined)
|
|
339
458
|
this.log.debug(`Matterbridge profile: ${this.profile}.`);
|
|
459
|
+
// Set matter.js logger level, format and logger (context: matterLogLevel)
|
|
340
460
|
if (hasParameter('matterlogger')) {
|
|
341
461
|
const level = getParameter('matterlogger');
|
|
342
462
|
if (level === 'debug') {
|
|
@@ -368,6 +488,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
368
488
|
Logger.format = MatterLogFormat.ANSI;
|
|
369
489
|
Logger.setLogger('default', this.createMatterLogger());
|
|
370
490
|
this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
|
|
491
|
+
// Create the file logger for matter.js (context: matterFileLog)
|
|
371
492
|
if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
|
|
372
493
|
this.matterbridgeInformation.matterFileLogger = true;
|
|
373
494
|
Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
|
|
@@ -376,6 +497,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
376
497
|
});
|
|
377
498
|
}
|
|
378
499
|
this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
|
|
500
|
+
// Log network interfaces
|
|
379
501
|
const networkInterfaces = os.networkInterfaces();
|
|
380
502
|
const availableAddresses = Object.entries(networkInterfaces);
|
|
381
503
|
const availableInterfaces = Object.keys(networkInterfaces);
|
|
@@ -387,6 +509,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
387
509
|
});
|
|
388
510
|
}
|
|
389
511
|
}
|
|
512
|
+
// Set the interface to use for matter server node mdnsInterface
|
|
390
513
|
if (hasParameter('mdnsinterface')) {
|
|
391
514
|
this.mdnsInterface = getParameter('mdnsinterface');
|
|
392
515
|
}
|
|
@@ -395,6 +518,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
395
518
|
if (this.mdnsInterface === '')
|
|
396
519
|
this.mdnsInterface = undefined;
|
|
397
520
|
}
|
|
521
|
+
// Validate mdnsInterface
|
|
398
522
|
if (this.mdnsInterface) {
|
|
399
523
|
if (!availableInterfaces.includes(this.mdnsInterface)) {
|
|
400
524
|
this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
|
|
@@ -407,6 +531,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
407
531
|
}
|
|
408
532
|
if (this.mdnsInterface)
|
|
409
533
|
this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
|
|
534
|
+
// Set the listeningAddressIpv4 for the matter commissioning server
|
|
410
535
|
if (hasParameter('ipv4address')) {
|
|
411
536
|
this.ipv4address = getParameter('ipv4address');
|
|
412
537
|
}
|
|
@@ -415,6 +540,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
415
540
|
if (this.ipv4address === '')
|
|
416
541
|
this.ipv4address = undefined;
|
|
417
542
|
}
|
|
543
|
+
// Validate ipv4address
|
|
418
544
|
if (this.ipv4address) {
|
|
419
545
|
let isValid = false;
|
|
420
546
|
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
@@ -430,6 +556,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
430
556
|
await this.nodeContext.remove('matteripv4address');
|
|
431
557
|
}
|
|
432
558
|
}
|
|
559
|
+
// Set the listeningAddressIpv6 for the matter commissioning server
|
|
433
560
|
if (hasParameter('ipv6address')) {
|
|
434
561
|
this.ipv6address = getParameter('ipv6address');
|
|
435
562
|
}
|
|
@@ -438,6 +565,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
438
565
|
if (this.ipv6address === '')
|
|
439
566
|
this.ipv6address = undefined;
|
|
440
567
|
}
|
|
568
|
+
// Validate ipv6address
|
|
441
569
|
if (this.ipv6address) {
|
|
442
570
|
let isValid = false;
|
|
443
571
|
for (const [ifaceName, ifaces] of availableAddresses) {
|
|
@@ -458,14 +586,19 @@ export class Matterbridge extends EventEmitter {
|
|
|
458
586
|
await this.nodeContext.remove('matteripv6address');
|
|
459
587
|
}
|
|
460
588
|
}
|
|
589
|
+
// Initialize PluginManager
|
|
461
590
|
this.plugins = new PluginManager(this);
|
|
462
591
|
await this.plugins.loadFromStorage();
|
|
463
592
|
this.plugins.logLevel = this.log.logLevel;
|
|
593
|
+
// Initialize DeviceManager
|
|
464
594
|
this.devices = new DeviceManager(this, this.nodeContext);
|
|
465
595
|
this.devices.logLevel = this.log.logLevel;
|
|
596
|
+
// Get the plugins from node storage and create the plugins node storage contexts
|
|
466
597
|
for (const plugin of this.plugins) {
|
|
467
598
|
const packageJson = await this.plugins.parse(plugin);
|
|
468
599
|
if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) {
|
|
600
|
+
// Try to reinstall the plugin from npm (for Docker pull and external plugins)
|
|
601
|
+
// We don't do this when the add and other parameters are set because we shut down the process after adding the plugin
|
|
469
602
|
this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
|
|
470
603
|
try {
|
|
471
604
|
await this.spawnCommand('npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']);
|
|
@@ -487,6 +620,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
487
620
|
await plugin.nodeContext.set('description', plugin.description);
|
|
488
621
|
await plugin.nodeContext.set('author', plugin.author);
|
|
489
622
|
}
|
|
623
|
+
// Log system info and create .matterbridge directory
|
|
490
624
|
await this.logNodeAndSystemInfo();
|
|
491
625
|
this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
|
|
492
626
|
`${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
|
|
@@ -494,6 +628,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
494
628
|
`${hasParameter('controller') ? 'mode controller ' : ''}` +
|
|
495
629
|
`${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
|
|
496
630
|
`running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
|
|
631
|
+
// Check node version and throw error
|
|
497
632
|
const minNodeVersion = 18;
|
|
498
633
|
const nodeVersion = process.versions.node;
|
|
499
634
|
const versionMajor = parseInt(nodeVersion.split('.')[0]);
|
|
@@ -501,9 +636,15 @@ export class Matterbridge extends EventEmitter {
|
|
|
501
636
|
this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
502
637
|
throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
|
|
503
638
|
}
|
|
639
|
+
// Parse command line
|
|
504
640
|
await this.parseCommandLine();
|
|
505
641
|
this.initialized = true;
|
|
506
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Parses the command line arguments and performs the corresponding actions.
|
|
645
|
+
* @private
|
|
646
|
+
* @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
|
|
647
|
+
*/
|
|
507
648
|
async parseCommandLine() {
|
|
508
649
|
if (hasParameter('help')) {
|
|
509
650
|
this.log.info(`\nUsage: matterbridge [options]\n
|
|
@@ -622,6 +763,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
622
763
|
this.shutdown = true;
|
|
623
764
|
return;
|
|
624
765
|
}
|
|
766
|
+
// Start the matter storage and create the matterbridge context
|
|
625
767
|
try {
|
|
626
768
|
await this.startMatterStorage();
|
|
627
769
|
}
|
|
@@ -629,12 +771,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
629
771
|
this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
630
772
|
throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
|
|
631
773
|
}
|
|
774
|
+
// Clear the matterbridge context if the reset parameter is set
|
|
632
775
|
if (hasParameter('reset') && getParameter('reset') === undefined) {
|
|
633
776
|
this.initialized = true;
|
|
634
777
|
await this.shutdownProcessAndReset();
|
|
635
778
|
this.shutdown = true;
|
|
636
779
|
return;
|
|
637
780
|
}
|
|
781
|
+
// Clear matterbridge plugin context if the reset parameter is set
|
|
638
782
|
if (hasParameter('reset') && getParameter('reset') !== undefined) {
|
|
639
783
|
this.log.debug(`Reset plugin ${getParameter('reset')}`);
|
|
640
784
|
const plugin = this.plugins.get(getParameter('reset'));
|
|
@@ -659,30 +803,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
659
803
|
this.shutdown = true;
|
|
660
804
|
return;
|
|
661
805
|
}
|
|
806
|
+
// Initialize frontend
|
|
662
807
|
if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
|
|
663
808
|
await this.frontend.start(getIntParameter('frontend'));
|
|
809
|
+
// Check in 30 seconds the latest versions
|
|
664
810
|
this.checkUpdateTimeout = setTimeout(async () => {
|
|
665
811
|
const { checkUpdates } = await import('./update.js');
|
|
666
812
|
checkUpdates(this);
|
|
667
813
|
}, 30 * 1000).unref();
|
|
814
|
+
// Check each 24 hours the latest versions
|
|
668
815
|
this.checkUpdateInterval = setInterval(async () => {
|
|
669
816
|
const { checkUpdates } = await import('./update.js');
|
|
670
817
|
checkUpdates(this);
|
|
671
818
|
}, 12 * 60 * 60 * 1000).unref();
|
|
819
|
+
// Start the matterbridge in mode test
|
|
672
820
|
if (hasParameter('test')) {
|
|
673
821
|
this.bridgeMode = 'bridge';
|
|
674
822
|
MatterbridgeEndpoint.bridgeMode = 'bridge';
|
|
675
823
|
return;
|
|
676
824
|
}
|
|
825
|
+
// Start the matterbridge in mode controller
|
|
677
826
|
if (hasParameter('controller')) {
|
|
678
827
|
this.bridgeMode = 'controller';
|
|
679
828
|
await this.startController();
|
|
680
829
|
return;
|
|
681
830
|
}
|
|
831
|
+
// Check if the bridge mode is set and start matterbridge in bridge mode if not set
|
|
682
832
|
if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
|
|
683
833
|
this.log.info('Setting default matterbridge start mode to bridge');
|
|
684
834
|
await this.nodeContext?.set('bridgeMode', 'bridge');
|
|
685
835
|
}
|
|
836
|
+
// Start matterbridge in bridge mode
|
|
686
837
|
if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
|
|
687
838
|
this.bridgeMode = 'bridge';
|
|
688
839
|
MatterbridgeEndpoint.bridgeMode = 'bridge';
|
|
@@ -690,6 +841,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
690
841
|
await this.startBridge();
|
|
691
842
|
return;
|
|
692
843
|
}
|
|
844
|
+
// Start matterbridge in childbridge mode
|
|
693
845
|
if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
|
|
694
846
|
this.bridgeMode = 'childbridge';
|
|
695
847
|
MatterbridgeEndpoint.bridgeMode = 'childbridge';
|
|
@@ -698,10 +850,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
698
850
|
return;
|
|
699
851
|
}
|
|
700
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Asynchronously loads and starts the registered plugins.
|
|
855
|
+
*
|
|
856
|
+
* This method is responsible for initializing and staarting all enabled plugins.
|
|
857
|
+
* It ensures that each plugin is properly loaded and started before the bridge starts.
|
|
858
|
+
*
|
|
859
|
+
* @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
|
|
860
|
+
*/
|
|
701
861
|
async startPlugins() {
|
|
862
|
+
// Check, load and start the plugins
|
|
702
863
|
for (const plugin of this.plugins) {
|
|
703
864
|
plugin.configJson = await this.plugins.loadConfig(plugin);
|
|
704
865
|
plugin.schemaJson = await this.plugins.loadSchema(plugin);
|
|
866
|
+
// Check if the plugin is available
|
|
705
867
|
if (!(await this.plugins.resolve(plugin.path))) {
|
|
706
868
|
this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
|
|
707
869
|
plugin.enabled = false;
|
|
@@ -721,10 +883,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
721
883
|
plugin.addedDevices = undefined;
|
|
722
884
|
plugin.qrPairingCode = undefined;
|
|
723
885
|
plugin.manualPairingCode = undefined;
|
|
724
|
-
this.plugins.load(plugin, true, 'Matterbridge is starting');
|
|
886
|
+
this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
|
|
725
887
|
}
|
|
726
888
|
this.frontend.wssSendRefreshRequired('plugins');
|
|
727
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
|
|
892
|
+
* When either of these signals are received, the cleanup method is called with an appropriate message.
|
|
893
|
+
*/
|
|
728
894
|
registerProcessHandlers() {
|
|
729
895
|
this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
|
|
730
896
|
process.removeAllListeners('uncaughtException');
|
|
@@ -751,6 +917,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
751
917
|
};
|
|
752
918
|
process.on('SIGTERM', this.sigtermHandler);
|
|
753
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
|
|
922
|
+
*/
|
|
754
923
|
deregisterProcesslHandlers() {
|
|
755
924
|
this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
|
|
756
925
|
if (this.exceptionHandler)
|
|
@@ -767,12 +936,17 @@ export class Matterbridge extends EventEmitter {
|
|
|
767
936
|
process.off('SIGTERM', this.sigtermHandler);
|
|
768
937
|
this.sigtermHandler = undefined;
|
|
769
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* Logs the node and system information.
|
|
941
|
+
*/
|
|
770
942
|
async logNodeAndSystemInfo() {
|
|
943
|
+
// IP address information
|
|
771
944
|
const networkInterfaces = os.networkInterfaces();
|
|
772
945
|
this.systemInformation.interfaceName = '';
|
|
773
946
|
this.systemInformation.ipv4Address = '';
|
|
774
947
|
this.systemInformation.ipv6Address = '';
|
|
775
948
|
for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
|
|
949
|
+
// this.log.debug(`Checking interface: '${interfaceName}' for '${this.mdnsInterface}'`);
|
|
776
950
|
if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
|
|
777
951
|
continue;
|
|
778
952
|
if (!interfaceDetails) {
|
|
@@ -798,19 +972,22 @@ export class Matterbridge extends EventEmitter {
|
|
|
798
972
|
break;
|
|
799
973
|
}
|
|
800
974
|
}
|
|
975
|
+
// Node information
|
|
801
976
|
this.systemInformation.nodeVersion = process.versions.node;
|
|
802
977
|
const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
|
|
803
978
|
const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
|
|
804
979
|
const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
|
|
980
|
+
// Host system information
|
|
805
981
|
this.systemInformation.hostname = os.hostname();
|
|
806
982
|
this.systemInformation.user = os.userInfo().username;
|
|
807
|
-
this.systemInformation.osType = os.type();
|
|
808
|
-
this.systemInformation.osRelease = os.release();
|
|
809
|
-
this.systemInformation.osPlatform = os.platform();
|
|
810
|
-
this.systemInformation.osArch = os.arch();
|
|
811
|
-
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
812
|
-
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
813
|
-
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours';
|
|
983
|
+
this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
|
|
984
|
+
this.systemInformation.osRelease = os.release(); // Kernel version
|
|
985
|
+
this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
|
|
986
|
+
this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
|
|
987
|
+
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
988
|
+
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
989
|
+
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
|
|
990
|
+
// Log the system information
|
|
814
991
|
this.log.debug('Host System Information:');
|
|
815
992
|
this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
|
|
816
993
|
this.log.debug(`- User: ${this.systemInformation.user}`);
|
|
@@ -826,16 +1003,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
826
1003
|
this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
|
|
827
1004
|
this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
|
|
828
1005
|
this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
|
|
1006
|
+
// Home directory
|
|
829
1007
|
this.homeDirectory = getParameter('homedir') ?? os.homedir();
|
|
830
1008
|
this.matterbridgeInformation.homeDirectory = this.homeDirectory;
|
|
831
1009
|
this.log.debug(`Home Directory: ${this.homeDirectory}`);
|
|
1010
|
+
// Package root directory
|
|
832
1011
|
const { fileURLToPath } = await import('node:url');
|
|
833
1012
|
const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
834
1013
|
this.rootDirectory = path.resolve(currentFileDirectory, '../');
|
|
835
1014
|
this.matterbridgeInformation.rootDirectory = this.rootDirectory;
|
|
836
1015
|
this.log.debug(`Root Directory: ${this.rootDirectory}`);
|
|
1016
|
+
// Global node_modules directory
|
|
837
1017
|
if (this.nodeContext)
|
|
838
1018
|
this.globalModulesDirectory = this.matterbridgeInformation.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
1019
|
+
// First run of Matterbridge so the node storage is empty
|
|
839
1020
|
if (this.globalModulesDirectory === '') {
|
|
840
1021
|
try {
|
|
841
1022
|
this.execRunningCount++;
|
|
@@ -851,6 +1032,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
851
1032
|
}
|
|
852
1033
|
else
|
|
853
1034
|
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
1035
|
+
/* removed cause is too expensive for the shelly board and not really needed. Why should it change the globalModulesDirectory?
|
|
1036
|
+
else {
|
|
1037
|
+
this.getGlobalNodeModules()
|
|
1038
|
+
.then(async (globalModulesDirectory) => {
|
|
1039
|
+
this.globalModulesDirectory = globalModulesDirectory;
|
|
1040
|
+
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
1041
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
1042
|
+
await this.nodeContext?.set<string>('globalModulesDirectory', this.globalModulesDirectory);
|
|
1043
|
+
})
|
|
1044
|
+
.catch((error) => {
|
|
1045
|
+
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
1046
|
+
});
|
|
1047
|
+
}*/
|
|
1048
|
+
// Create the data directory .matterbridge in the home directory
|
|
854
1049
|
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
855
1050
|
this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
|
|
856
1051
|
try {
|
|
@@ -874,6 +1069,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
874
1069
|
}
|
|
875
1070
|
}
|
|
876
1071
|
this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
1072
|
+
// Create the plugin directory Matterbridge in the home directory
|
|
877
1073
|
this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
|
|
878
1074
|
this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
|
|
879
1075
|
try {
|
|
@@ -897,6 +1093,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
897
1093
|
}
|
|
898
1094
|
}
|
|
899
1095
|
this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
1096
|
+
// Create the matter cert directory in the home directory
|
|
900
1097
|
this.matterbridgeCertDirectory = path.join(this.homeDirectory, '.mattercert');
|
|
901
1098
|
this.matterbridgeInformation.matterbridgeCertDirectory = this.matterbridgeCertDirectory;
|
|
902
1099
|
try {
|
|
@@ -920,50 +1117,68 @@ export class Matterbridge extends EventEmitter {
|
|
|
920
1117
|
}
|
|
921
1118
|
}
|
|
922
1119
|
this.log.debug(`Matterbridge Matter Cert Directory: ${this.matterbridgeCertDirectory}`);
|
|
1120
|
+
// Matterbridge version
|
|
923
1121
|
const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
|
|
924
1122
|
this.matterbridgeVersion = this.matterbridgeLatestVersion = packageJson.version;
|
|
925
1123
|
this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeVersion;
|
|
926
1124
|
this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
|
|
1125
|
+
// Matterbridge latest version
|
|
927
1126
|
if (this.nodeContext)
|
|
928
1127
|
this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
|
|
929
1128
|
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
1129
|
+
// this.getMatterbridgeLatestVersion();
|
|
1130
|
+
// Current working directory
|
|
930
1131
|
const currentDir = process.cwd();
|
|
931
1132
|
this.log.debug(`Current Working Directory: ${currentDir}`);
|
|
1133
|
+
// Command line arguments (excluding 'node' and the script name)
|
|
932
1134
|
const cmdArgs = process.argv.slice(2).join(' ');
|
|
933
1135
|
this.log.debug(`Command Line Arguments: ${cmdArgs}`);
|
|
934
1136
|
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
|
|
1139
|
+
*
|
|
1140
|
+
* @returns {Function} The MatterLogger function.
|
|
1141
|
+
*/
|
|
935
1142
|
createMatterLogger() {
|
|
936
|
-
const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4
|
|
1143
|
+
const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
|
|
937
1144
|
return (_level, formattedLog) => {
|
|
938
1145
|
const logger = formattedLog.slice(44, 44 + 20).trim();
|
|
939
1146
|
const message = formattedLog.slice(65);
|
|
940
1147
|
matterLogger.logName = logger;
|
|
941
1148
|
switch (_level) {
|
|
942
1149
|
case MatterLogLevel.DEBUG:
|
|
943
|
-
matterLogger.log("debug"
|
|
1150
|
+
matterLogger.log("debug" /* LogLevel.DEBUG */, message);
|
|
944
1151
|
break;
|
|
945
1152
|
case MatterLogLevel.INFO:
|
|
946
|
-
matterLogger.log("info"
|
|
1153
|
+
matterLogger.log("info" /* LogLevel.INFO */, message);
|
|
947
1154
|
break;
|
|
948
1155
|
case MatterLogLevel.NOTICE:
|
|
949
|
-
matterLogger.log("notice"
|
|
1156
|
+
matterLogger.log("notice" /* LogLevel.NOTICE */, message);
|
|
950
1157
|
break;
|
|
951
1158
|
case MatterLogLevel.WARN:
|
|
952
|
-
matterLogger.log("warn"
|
|
1159
|
+
matterLogger.log("warn" /* LogLevel.WARN */, message);
|
|
953
1160
|
break;
|
|
954
1161
|
case MatterLogLevel.ERROR:
|
|
955
|
-
matterLogger.log("error"
|
|
1162
|
+
matterLogger.log("error" /* LogLevel.ERROR */, message);
|
|
956
1163
|
break;
|
|
957
1164
|
case MatterLogLevel.FATAL:
|
|
958
|
-
matterLogger.log("fatal"
|
|
1165
|
+
matterLogger.log("fatal" /* LogLevel.FATAL */, message);
|
|
959
1166
|
break;
|
|
960
1167
|
default:
|
|
961
|
-
matterLogger.log("debug"
|
|
1168
|
+
matterLogger.log("debug" /* LogLevel.DEBUG */, message);
|
|
962
1169
|
break;
|
|
963
1170
|
}
|
|
964
1171
|
};
|
|
965
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Creates a Matter File Logger.
|
|
1175
|
+
*
|
|
1176
|
+
* @param {string} filePath - The path to the log file.
|
|
1177
|
+
* @param {boolean} [unlink=false] - Whether to unlink the log file before creating a new one.
|
|
1178
|
+
* @returns {Function} - A function that logs formatted messages to the log file.
|
|
1179
|
+
*/
|
|
966
1180
|
async createMatterFileLogger(filePath, unlink = false) {
|
|
1181
|
+
// 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
|
|
967
1182
|
let fileSize = 0;
|
|
968
1183
|
if (unlink) {
|
|
969
1184
|
try {
|
|
@@ -1012,12 +1227,21 @@ export class Matterbridge extends EventEmitter {
|
|
|
1012
1227
|
}
|
|
1013
1228
|
};
|
|
1014
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Restarts the process by exiting the current instance and loading a new instance.
|
|
1232
|
+
*/
|
|
1015
1233
|
async restartProcess() {
|
|
1016
1234
|
await this.cleanup('restarting...', true);
|
|
1017
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Shut down the process by exiting the current process.
|
|
1238
|
+
*/
|
|
1018
1239
|
async shutdownProcess() {
|
|
1019
1240
|
await this.cleanup('shutting down...', false);
|
|
1020
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Update matterbridge and and shut down the process.
|
|
1244
|
+
*/
|
|
1021
1245
|
async updateProcess() {
|
|
1022
1246
|
this.log.info('Updating matterbridge...');
|
|
1023
1247
|
try {
|
|
@@ -1030,52 +1254,73 @@ export class Matterbridge extends EventEmitter {
|
|
|
1030
1254
|
this.frontend.wssSendRestartRequired();
|
|
1031
1255
|
await this.cleanup('updating...', false);
|
|
1032
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Unregister all devices and shut down the process.
|
|
1259
|
+
*/
|
|
1033
1260
|
async unregisterAndShutdownProcess() {
|
|
1034
1261
|
this.log.info('Unregistering all devices and shutting down...');
|
|
1035
1262
|
for (const plugin of this.plugins) {
|
|
1036
1263
|
await this.removeAllBridgedEndpoints(plugin.name, 250);
|
|
1037
1264
|
}
|
|
1038
1265
|
this.log.debug('Waiting for the MessageExchange to finish...');
|
|
1039
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1266
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
|
|
1040
1267
|
this.log.debug('Cleaning up and shutting down...');
|
|
1041
1268
|
await this.cleanup('unregistered all devices and shutting down...', false);
|
|
1042
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Reset commissioning and shut down the process.
|
|
1272
|
+
*/
|
|
1043
1273
|
async shutdownProcessAndReset() {
|
|
1044
1274
|
await this.cleanup('shutting down with reset...', false);
|
|
1045
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Factory reset and shut down the process.
|
|
1278
|
+
*/
|
|
1046
1279
|
async shutdownProcessAndFactoryReset() {
|
|
1047
1280
|
await this.cleanup('shutting down with factory reset...', false);
|
|
1048
1281
|
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Cleans up the Matterbridge instance.
|
|
1284
|
+
* @param message - The cleanup message.
|
|
1285
|
+
* @param restart - Indicates whether to restart the instance after cleanup. Default is `false`.
|
|
1286
|
+
* @returns A promise that resolves when the cleanup is completed.
|
|
1287
|
+
*/
|
|
1049
1288
|
async cleanup(message, restart = false) {
|
|
1050
1289
|
if (this.initialized && !this.hasCleanupStarted) {
|
|
1051
1290
|
this.emit('cleanup_started');
|
|
1052
1291
|
this.hasCleanupStarted = true;
|
|
1053
1292
|
this.log.info(message);
|
|
1293
|
+
// Clear the start matter interval
|
|
1054
1294
|
if (this.startMatterInterval) {
|
|
1055
1295
|
clearInterval(this.startMatterInterval);
|
|
1056
1296
|
this.startMatterInterval = undefined;
|
|
1057
1297
|
this.log.debug('Start matter interval cleared');
|
|
1058
1298
|
}
|
|
1299
|
+
// Clear the check update timeout
|
|
1059
1300
|
if (this.checkUpdateTimeout) {
|
|
1060
1301
|
clearInterval(this.checkUpdateTimeout);
|
|
1061
1302
|
this.checkUpdateTimeout = undefined;
|
|
1062
1303
|
this.log.debug('Check update timeout cleared');
|
|
1063
1304
|
}
|
|
1305
|
+
// Clear the check update interval
|
|
1064
1306
|
if (this.checkUpdateInterval) {
|
|
1065
1307
|
clearInterval(this.checkUpdateInterval);
|
|
1066
1308
|
this.checkUpdateInterval = undefined;
|
|
1067
1309
|
this.log.debug('Check update interval cleared');
|
|
1068
1310
|
}
|
|
1311
|
+
// Clear the configure timeout
|
|
1069
1312
|
if (this.configureTimeout) {
|
|
1070
1313
|
clearTimeout(this.configureTimeout);
|
|
1071
1314
|
this.configureTimeout = undefined;
|
|
1072
1315
|
this.log.debug('Matterbridge configure timeout cleared');
|
|
1073
1316
|
}
|
|
1317
|
+
// Clear the reachability timeout
|
|
1074
1318
|
if (this.reachabilityTimeout) {
|
|
1075
1319
|
clearTimeout(this.reachabilityTimeout);
|
|
1076
1320
|
this.reachabilityTimeout = undefined;
|
|
1077
1321
|
this.log.debug('Matterbridge reachability timeout cleared');
|
|
1078
1322
|
}
|
|
1323
|
+
// Calling the shutdown method of each plugin and clear the plugins reachability timeout
|
|
1079
1324
|
for (const plugin of this.plugins) {
|
|
1080
1325
|
if (!plugin.enabled || plugin.error)
|
|
1081
1326
|
continue;
|
|
@@ -1086,9 +1331,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1086
1331
|
this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
|
|
1087
1332
|
}
|
|
1088
1333
|
}
|
|
1334
|
+
// Stop matter server nodes
|
|
1089
1335
|
this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
|
|
1090
1336
|
this.log.debug('Waiting for the MessageExchange to finish...');
|
|
1091
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1337
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
|
|
1092
1338
|
if (this.bridgeMode === 'bridge') {
|
|
1093
1339
|
if (this.serverNode) {
|
|
1094
1340
|
await this.stopServerNode(this.serverNode);
|
|
@@ -1104,6 +1350,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1104
1350
|
}
|
|
1105
1351
|
}
|
|
1106
1352
|
this.log.notice('Stopped matter server nodes');
|
|
1353
|
+
// Matter commisioning reset
|
|
1107
1354
|
if (message === 'shutting down with reset...') {
|
|
1108
1355
|
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1109
1356
|
await this.matterStorageManager?.createContext('events')?.clearAll();
|
|
@@ -1113,17 +1360,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
1113
1360
|
await this.matterbridgeContext?.clearAll();
|
|
1114
1361
|
this.log.info('Matter storage reset done! Remove the bridge from the controller.');
|
|
1115
1362
|
}
|
|
1363
|
+
// Stop matter storage
|
|
1116
1364
|
await this.stopMatterStorage();
|
|
1365
|
+
// Stop the frontend
|
|
1117
1366
|
await this.frontend.stop();
|
|
1367
|
+
// Remove the matterfilelogger
|
|
1118
1368
|
try {
|
|
1119
1369
|
Logger.removeLogger('matterfilelogger');
|
|
1370
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1120
1371
|
}
|
|
1121
1372
|
catch (error) {
|
|
1373
|
+
// this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
|
|
1122
1374
|
}
|
|
1375
|
+
// Serialize registeredDevices
|
|
1123
1376
|
if (this.nodeStorage && this.nodeContext) {
|
|
1377
|
+
/*
|
|
1378
|
+
TODO: Implement serialization of registered devices in edge mode
|
|
1379
|
+
this.log.info('Saving registered devices...');
|
|
1380
|
+
const serializedRegisteredDevices: SerializedMatterbridgeEndpoint[] = [];
|
|
1381
|
+
this.devices.forEach(async (device) => {
|
|
1382
|
+
const serializedMatterbridgeDevice = MatterbridgeEndpoint.serialize(device);
|
|
1383
|
+
// this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
|
|
1384
|
+
if (serializedMatterbridgeDevice) serializedRegisteredDevices.push(serializedMatterbridgeDevice);
|
|
1385
|
+
});
|
|
1386
|
+
await this.nodeContext.set<SerializedMatterbridgeEndpoint[]>('devices', serializedRegisteredDevices);
|
|
1387
|
+
this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
|
|
1388
|
+
*/
|
|
1389
|
+
// Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
|
|
1124
1390
|
this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
|
|
1125
1391
|
await this.nodeContext.close();
|
|
1126
1392
|
this.nodeContext = undefined;
|
|
1393
|
+
// Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
|
|
1127
1394
|
for (const plugin of this.plugins) {
|
|
1128
1395
|
if (plugin.nodeContext) {
|
|
1129
1396
|
this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
|
|
@@ -1140,8 +1407,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1140
1407
|
}
|
|
1141
1408
|
this.plugins.clear();
|
|
1142
1409
|
this.devices.clear();
|
|
1410
|
+
// Factory reset
|
|
1143
1411
|
if (message === 'shutting down with factory reset...') {
|
|
1144
1412
|
try {
|
|
1413
|
+
// Delete old matter storage file and backup
|
|
1145
1414
|
const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
|
|
1146
1415
|
this.log.info(`Unlinking old matter storage file: ${file}`);
|
|
1147
1416
|
await fs.unlink(file);
|
|
@@ -1155,6 +1424,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1155
1424
|
}
|
|
1156
1425
|
}
|
|
1157
1426
|
try {
|
|
1427
|
+
// Delete matter node storage directory with its subdirectories and backup
|
|
1158
1428
|
const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
|
|
1159
1429
|
this.log.info(`Removing matter node storage directory: ${dir}`);
|
|
1160
1430
|
await fs.rm(dir, { recursive: true });
|
|
@@ -1168,6 +1438,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1168
1438
|
}
|
|
1169
1439
|
}
|
|
1170
1440
|
try {
|
|
1441
|
+
// Delete node storage directory with its subdirectories and backup
|
|
1171
1442
|
const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
|
|
1172
1443
|
this.log.info(`Removing storage directory: ${dir}`);
|
|
1173
1444
|
await fs.rm(dir, { recursive: true });
|
|
@@ -1182,12 +1453,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1182
1453
|
}
|
|
1183
1454
|
this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
|
|
1184
1455
|
}
|
|
1456
|
+
// Deregisters the process handlers
|
|
1185
1457
|
this.deregisterProcesslHandlers();
|
|
1186
1458
|
if (restart) {
|
|
1187
1459
|
if (message === 'updating...') {
|
|
1188
1460
|
this.log.info('Cleanup completed. Updating...');
|
|
1189
1461
|
Matterbridge.instance = undefined;
|
|
1190
|
-
this.emit('update');
|
|
1462
|
+
this.emit('update'); // Restart the process but the update has been done before. TODO move all updates to the cli
|
|
1191
1463
|
}
|
|
1192
1464
|
else if (message === 'restarting...') {
|
|
1193
1465
|
this.log.info('Cleanup completed. Restarting...');
|
|
@@ -1208,6 +1480,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
1208
1480
|
this.log.debug('Cleanup already started...');
|
|
1209
1481
|
}
|
|
1210
1482
|
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Creates and configures the server node for an accessory plugin for a given device.
|
|
1485
|
+
*
|
|
1486
|
+
* @param {RegisteredPlugin} plugin - The plugin to configure.
|
|
1487
|
+
* @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
|
|
1488
|
+
* @param {boolean} [start=false] - Whether to start the server node after adding the device.
|
|
1489
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
|
|
1490
|
+
*/
|
|
1211
1491
|
async createAccessoryPlugin(plugin, device, start = false) {
|
|
1212
1492
|
if (!plugin.locked && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
|
|
1213
1493
|
plugin.locked = true;
|
|
@@ -1221,6 +1501,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1221
1501
|
await this.startServerNode(plugin.serverNode);
|
|
1222
1502
|
}
|
|
1223
1503
|
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Creates and configures the server node for a dynamic plugin.
|
|
1506
|
+
*
|
|
1507
|
+
* @param {RegisteredPlugin} plugin - The plugin to configure.
|
|
1508
|
+
* @param {boolean} [start=false] - Whether to start the server node after adding the aggregator node.
|
|
1509
|
+
* @returns {Promise<void>} A promise that resolves when the server node for the dynamic plugin is created and configured.
|
|
1510
|
+
*/
|
|
1224
1511
|
async createDynamicPlugin(plugin, start = false) {
|
|
1225
1512
|
if (!plugin.locked) {
|
|
1226
1513
|
plugin.locked = true;
|
|
@@ -1233,7 +1520,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1233
1520
|
await this.startServerNode(plugin.serverNode);
|
|
1234
1521
|
}
|
|
1235
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Starts the Matterbridge in bridge mode.
|
|
1525
|
+
* @private
|
|
1526
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1527
|
+
*/
|
|
1236
1528
|
async startBridge() {
|
|
1529
|
+
// Plugins are configured by a timer when matter server is started and plugin.configured is set to true
|
|
1237
1530
|
if (!this.matterStorageManager)
|
|
1238
1531
|
throw new Error('No storage manager initialized');
|
|
1239
1532
|
if (!this.matterbridgeContext)
|
|
@@ -1272,7 +1565,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
1272
1565
|
clearInterval(this.startMatterInterval);
|
|
1273
1566
|
this.startMatterInterval = undefined;
|
|
1274
1567
|
this.log.debug('Cleared startMatterInterval interval for Matterbridge');
|
|
1568
|
+
// Start the Matter server node
|
|
1275
1569
|
this.startServerNode(this.serverNode);
|
|
1570
|
+
// Configure the plugins
|
|
1276
1571
|
this.configureTimeout = setTimeout(async () => {
|
|
1277
1572
|
for (const plugin of this.plugins) {
|
|
1278
1573
|
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
@@ -1290,6 +1585,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1290
1585
|
}
|
|
1291
1586
|
this.frontend.wssSendRefreshRequired('plugins');
|
|
1292
1587
|
}, 30 * 1000);
|
|
1588
|
+
// Setting reachability to true
|
|
1293
1589
|
this.reachabilityTimeout = setTimeout(() => {
|
|
1294
1590
|
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
1295
1591
|
if (this.aggregatorNode)
|
|
@@ -1298,6 +1594,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
1298
1594
|
}, 60 * 1000);
|
|
1299
1595
|
}, 1000);
|
|
1300
1596
|
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Starts the Matterbridge in childbridge mode.
|
|
1599
|
+
* @private
|
|
1600
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1601
|
+
*/
|
|
1301
1602
|
async startChildbridge() {
|
|
1302
1603
|
if (!this.matterStorageManager)
|
|
1303
1604
|
throw new Error('No storage manager initialized');
|
|
@@ -1335,6 +1636,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1335
1636
|
clearInterval(this.startMatterInterval);
|
|
1336
1637
|
this.startMatterInterval = undefined;
|
|
1337
1638
|
this.log.debug('Cleared startMatterInterval interval in childbridge mode');
|
|
1639
|
+
// Configure the plugins
|
|
1338
1640
|
this.configureTimeout = setTimeout(async () => {
|
|
1339
1641
|
for (const plugin of this.plugins) {
|
|
1340
1642
|
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
@@ -1371,7 +1673,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
1371
1673
|
this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1372
1674
|
continue;
|
|
1373
1675
|
}
|
|
1676
|
+
// Start the Matter server node
|
|
1374
1677
|
this.startServerNode(plugin.serverNode);
|
|
1678
|
+
// Setting reachability to true
|
|
1375
1679
|
plugin.reachabilityTimeout = setTimeout(() => {
|
|
1376
1680
|
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}`);
|
|
1377
1681
|
if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
|
|
@@ -1381,6 +1685,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
1381
1685
|
}
|
|
1382
1686
|
}, 1000);
|
|
1383
1687
|
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Starts the Matterbridge controller.
|
|
1690
|
+
* @private
|
|
1691
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1692
|
+
*/
|
|
1384
1693
|
async startController() {
|
|
1385
1694
|
if (!this.matterStorageManager) {
|
|
1386
1695
|
this.log.error('No storage manager initialized');
|
|
@@ -1395,8 +1704,207 @@ export class Matterbridge extends EventEmitter {
|
|
|
1395
1704
|
return;
|
|
1396
1705
|
}
|
|
1397
1706
|
this.log.debug('Starting matterbridge in mode', this.bridgeMode);
|
|
1707
|
+
/*
|
|
1708
|
+
this.matterServer = await this.createMatterServer(this.storageManager);
|
|
1709
|
+
this.log.info('Creating matter commissioning controller');
|
|
1710
|
+
this.commissioningController = new CommissioningController({
|
|
1711
|
+
autoConnect: false,
|
|
1712
|
+
});
|
|
1713
|
+
this.log.info('Adding matter commissioning controller to matter server');
|
|
1714
|
+
await this.matterServer.addCommissioningController(this.commissioningController);
|
|
1715
|
+
|
|
1716
|
+
this.log.info('Starting matter server');
|
|
1717
|
+
await this.matterServer.start();
|
|
1718
|
+
this.log.info('Matter server started');
|
|
1719
|
+
const commissioningOptions: ControllerCommissioningFlowOptions = {
|
|
1720
|
+
regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
|
|
1721
|
+
regulatoryCountryCode: 'XX',
|
|
1722
|
+
};
|
|
1723
|
+
const commissioningController = new CommissioningController({
|
|
1724
|
+
environment: {
|
|
1725
|
+
environment,
|
|
1726
|
+
id: uniqueId,
|
|
1727
|
+
},
|
|
1728
|
+
autoConnect: false, // Do not auto connect to the commissioned nodes
|
|
1729
|
+
adminFabricLabel,
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
if (hasParameter('pairingcode')) {
|
|
1733
|
+
this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
|
|
1734
|
+
const pairingCode = getParameter('pairingcode');
|
|
1735
|
+
const ip = this.controllerContext.has('ip') ? this.controllerContext.get<string>('ip') : undefined;
|
|
1736
|
+
const port = this.controllerContext.has('port') ? this.controllerContext.get<number>('port') : undefined;
|
|
1737
|
+
|
|
1738
|
+
let longDiscriminator, setupPin, shortDiscriminator;
|
|
1739
|
+
if (pairingCode !== undefined) {
|
|
1740
|
+
const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
|
|
1741
|
+
shortDiscriminator = pairingCodeCodec.shortDiscriminator;
|
|
1742
|
+
longDiscriminator = undefined;
|
|
1743
|
+
setupPin = pairingCodeCodec.passcode;
|
|
1744
|
+
this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
|
|
1745
|
+
} else {
|
|
1746
|
+
longDiscriminator = await this.controllerContext.get('longDiscriminator', 3840);
|
|
1747
|
+
if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
|
|
1748
|
+
setupPin = this.controllerContext.get('pin', 20202021);
|
|
1749
|
+
}
|
|
1750
|
+
if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
|
|
1751
|
+
throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const options = {
|
|
1755
|
+
commissioning: commissioningOptions,
|
|
1756
|
+
discovery: {
|
|
1757
|
+
knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
|
|
1758
|
+
identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
|
|
1759
|
+
},
|
|
1760
|
+
passcode: setupPin,
|
|
1761
|
+
} as NodeCommissioningOptions;
|
|
1762
|
+
this.log.info('Commissioning with options:', options);
|
|
1763
|
+
const nodeId = await this.commissioningController.commissionNode(options);
|
|
1764
|
+
this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
|
|
1765
|
+
this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
|
|
1766
|
+
} // (hasParameter('pairingcode'))
|
|
1767
|
+
|
|
1768
|
+
if (hasParameter('unpairall')) {
|
|
1769
|
+
this.log.info('***Commissioning controller unpairing all nodes...');
|
|
1770
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1771
|
+
for (const nodeId of nodeIds) {
|
|
1772
|
+
this.log.info('***Commissioning controller unpairing node:', nodeId);
|
|
1773
|
+
await this.commissioningController.removeNode(nodeId);
|
|
1774
|
+
}
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
if (hasParameter('discover')) {
|
|
1779
|
+
// const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
|
|
1780
|
+
// console.log(discover);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (!this.commissioningController.isCommissioned()) {
|
|
1784
|
+
this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const nodeIds = this.commissioningController.getCommissionedNodes();
|
|
1789
|
+
this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
|
|
1790
|
+
for (const nodeId of nodeIds) {
|
|
1791
|
+
this.log.info(`***Connecting to commissioned node: ${nodeId}`);
|
|
1792
|
+
|
|
1793
|
+
const node = await this.commissioningController.connectNode(nodeId, {
|
|
1794
|
+
autoSubscribe: false,
|
|
1795
|
+
attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
|
|
1796
|
+
this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
|
|
1797
|
+
eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
|
|
1798
|
+
this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
|
|
1799
|
+
stateInformationCallback: (peerNodeId, info) => {
|
|
1800
|
+
switch (info) {
|
|
1801
|
+
case NodeStateInformation.Connected:
|
|
1802
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
|
|
1803
|
+
break;
|
|
1804
|
+
case NodeStateInformation.Disconnected:
|
|
1805
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
|
|
1806
|
+
break;
|
|
1807
|
+
case NodeStateInformation.Reconnecting:
|
|
1808
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
|
|
1809
|
+
break;
|
|
1810
|
+
case NodeStateInformation.WaitingForDeviceDiscovery:
|
|
1811
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
|
|
1812
|
+
break;
|
|
1813
|
+
case NodeStateInformation.StructureChanged:
|
|
1814
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
|
|
1815
|
+
break;
|
|
1816
|
+
case NodeStateInformation.Decommissioned:
|
|
1817
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
|
|
1818
|
+
break;
|
|
1819
|
+
default:
|
|
1820
|
+
this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
node.logStructure();
|
|
1827
|
+
|
|
1828
|
+
// Get the interaction client
|
|
1829
|
+
this.log.info('Getting the interaction client');
|
|
1830
|
+
const interactionClient = await node.getInteractionClient();
|
|
1831
|
+
let cluster;
|
|
1832
|
+
let attributes;
|
|
1833
|
+
|
|
1834
|
+
// Log BasicInformationCluster
|
|
1835
|
+
cluster = BasicInformationCluster;
|
|
1836
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1837
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1838
|
+
});
|
|
1839
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1840
|
+
attributes.forEach((attribute) => {
|
|
1841
|
+
this.log.info(
|
|
1842
|
+
`- 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}`,
|
|
1843
|
+
);
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
// Log PowerSourceCluster
|
|
1847
|
+
cluster = PowerSourceCluster;
|
|
1848
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1849
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1850
|
+
});
|
|
1851
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1852
|
+
attributes.forEach((attribute) => {
|
|
1853
|
+
this.log.info(
|
|
1854
|
+
`- 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}`,
|
|
1855
|
+
);
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
// Log ThreadNetworkDiagnostics
|
|
1859
|
+
cluster = ThreadNetworkDiagnosticsCluster;
|
|
1860
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1861
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1862
|
+
});
|
|
1863
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1864
|
+
attributes.forEach((attribute) => {
|
|
1865
|
+
this.log.info(
|
|
1866
|
+
`- 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}`,
|
|
1867
|
+
);
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
// Log SwitchCluster
|
|
1871
|
+
cluster = SwitchCluster;
|
|
1872
|
+
attributes = await interactionClient.getMultipleAttributes({
|
|
1873
|
+
attributes: [{ clusterId: cluster.id }],
|
|
1874
|
+
});
|
|
1875
|
+
if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
|
|
1876
|
+
attributes.forEach((attribute) => {
|
|
1877
|
+
this.log.info(
|
|
1878
|
+
`- 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}`,
|
|
1879
|
+
);
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
this.log.info('Subscribing to all attributes and events');
|
|
1883
|
+
await node.subscribeAllAttributesAndEvents({
|
|
1884
|
+
ignoreInitialTriggers: false,
|
|
1885
|
+
attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
|
|
1886
|
+
this.log.info(
|
|
1887
|
+
`***${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}`,
|
|
1888
|
+
),
|
|
1889
|
+
eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
|
|
1890
|
+
this.log.info(
|
|
1891
|
+
`***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
|
|
1892
|
+
);
|
|
1893
|
+
},
|
|
1894
|
+
});
|
|
1895
|
+
this.log.info('Subscribed to all attributes and events');
|
|
1896
|
+
}
|
|
1897
|
+
*/
|
|
1398
1898
|
}
|
|
1899
|
+
/** ***********************************************************************************************************************************/
|
|
1900
|
+
/** Matter.js methods */
|
|
1901
|
+
/** ***********************************************************************************************************************************/
|
|
1902
|
+
/**
|
|
1903
|
+
* Starts the matter storage process with name Matterbridge.
|
|
1904
|
+
* @returns {Promise<void>} - A promise that resolves when the storage process is started.
|
|
1905
|
+
*/
|
|
1399
1906
|
async startMatterStorage() {
|
|
1907
|
+
// Setup Matter storage
|
|
1400
1908
|
this.log.info(`Starting matter node storage...`);
|
|
1401
1909
|
this.matterStorageService = this.environment.get(StorageService);
|
|
1402
1910
|
this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
|
|
@@ -1405,13 +1913,25 @@ export class Matterbridge extends EventEmitter {
|
|
|
1405
1913
|
this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
|
|
1406
1914
|
this.matterbridgeInformation.matterbridgeSerialNumber = await this.matterbridgeContext.get('serialNumber', '');
|
|
1407
1915
|
this.log.info('Matter node storage started');
|
|
1916
|
+
// Backup matter storage since it is created/opened correctly
|
|
1408
1917
|
await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
|
|
1409
1918
|
}
|
|
1919
|
+
/**
|
|
1920
|
+
* Makes a backup copy of the specified matter storage directory.
|
|
1921
|
+
*
|
|
1922
|
+
* @param storageName - The name of the storage directory to be backed up.
|
|
1923
|
+
* @param backupName - The name of the backup directory to be created.
|
|
1924
|
+
* @returns {Promise<void>} A promise that resolves when the has been done.
|
|
1925
|
+
*/
|
|
1410
1926
|
async backupMatterStorage(storageName, backupName) {
|
|
1411
1927
|
this.log.info('Creating matter node storage backup...');
|
|
1412
1928
|
await copyDirectory(storageName, backupName);
|
|
1413
1929
|
this.log.info('Created matter node storage backup');
|
|
1414
1930
|
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Stops the matter storage.
|
|
1933
|
+
* @returns {Promise<void>} A promise that resolves when the storage is stopped.
|
|
1934
|
+
*/
|
|
1415
1935
|
async stopMatterStorage() {
|
|
1416
1936
|
this.log.info('Closing matter node storage...');
|
|
1417
1937
|
await this.matterStorageManager?.close();
|
|
@@ -1420,6 +1940,19 @@ export class Matterbridge extends EventEmitter {
|
|
|
1420
1940
|
this.matterbridgeContext = undefined;
|
|
1421
1941
|
this.log.info('Matter node storage closed');
|
|
1422
1942
|
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Creates a server node storage context.
|
|
1945
|
+
*
|
|
1946
|
+
* @param {string} pluginName - The name of the plugin.
|
|
1947
|
+
* @param {string} deviceName - The name of the device.
|
|
1948
|
+
* @param {DeviceTypeId} deviceType - The device type of the device.
|
|
1949
|
+
* @param {number} vendorId - The vendor ID.
|
|
1950
|
+
* @param {string} vendorName - The vendor name.
|
|
1951
|
+
* @param {number} productId - The product ID.
|
|
1952
|
+
* @param {string} productName - The product name.
|
|
1953
|
+
* @param {string} [serialNumber] - The serial number of the device (optional).
|
|
1954
|
+
* @returns {Promise<StorageContext>} The storage context for the commissioning server.
|
|
1955
|
+
*/
|
|
1423
1956
|
async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
|
|
1424
1957
|
const { randomBytes } = await import('node:crypto');
|
|
1425
1958
|
if (!this.matterStorageService)
|
|
@@ -1453,6 +1986,15 @@ export class Matterbridge extends EventEmitter {
|
|
|
1453
1986
|
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
1454
1987
|
return storageContext;
|
|
1455
1988
|
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Creates a server node.
|
|
1991
|
+
*
|
|
1992
|
+
* @param {StorageContext} storageContext - The storage context for the server node.
|
|
1993
|
+
* @param {number} [port=5540] - The port number for the server node. Defaults to 5540.
|
|
1994
|
+
* @param {number} [passcode=20242025] - The passcode for the server node. Defaults to 20242025.
|
|
1995
|
+
* @param {number} [discriminator=3850] - The discriminator for the server node. Defaults to 3850.
|
|
1996
|
+
* @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
|
|
1997
|
+
*/
|
|
1456
1998
|
async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
|
|
1457
1999
|
const storeId = await storageContext.get('storeId');
|
|
1458
2000
|
this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
|
|
@@ -1462,24 +2004,37 @@ export class Matterbridge extends EventEmitter {
|
|
|
1462
2004
|
this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
1463
2005
|
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
1464
2006
|
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
2007
|
+
/**
|
|
2008
|
+
* Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
|
|
2009
|
+
*/
|
|
1465
2010
|
const serverNode = await ServerNode.create({
|
|
2011
|
+
// Required: Give the Node a unique ID which is used to store the state of this node
|
|
1466
2012
|
id: storeId,
|
|
2013
|
+
// Provide Network relevant configuration like the port
|
|
2014
|
+
// Optional when operating only one device on a host, Default port is 5540
|
|
1467
2015
|
network: {
|
|
1468
2016
|
listeningAddressIpv4: this.ipv4address,
|
|
1469
2017
|
listeningAddressIpv6: this.ipv6address,
|
|
1470
2018
|
port,
|
|
1471
2019
|
},
|
|
2020
|
+
// Provide the certificate for the device
|
|
1472
2021
|
operationalCredentials: {
|
|
1473
2022
|
certification: this.certification,
|
|
1474
2023
|
},
|
|
2024
|
+
// Provide Commissioning relevant settings
|
|
2025
|
+
// Optional for development/testing purposes
|
|
1475
2026
|
commissioning: {
|
|
1476
2027
|
passcode,
|
|
1477
2028
|
discriminator,
|
|
1478
2029
|
},
|
|
2030
|
+
// Provide Node announcement settings
|
|
2031
|
+
// Optional: If Ommitted some development defaults are used
|
|
1479
2032
|
productDescription: {
|
|
1480
2033
|
name: await storageContext.get('deviceName'),
|
|
1481
2034
|
deviceType: DeviceTypeId(await storageContext.get('deviceType')),
|
|
1482
2035
|
},
|
|
2036
|
+
// Provide defaults for the BasicInformation cluster on the Root endpoint
|
|
2037
|
+
// Optional: If Omitted some development defaults are used
|
|
1483
2038
|
basicInformation: {
|
|
1484
2039
|
vendorId: VendorId(await storageContext.get('vendorId')),
|
|
1485
2040
|
vendorName: await storageContext.get('vendorName'),
|
|
@@ -1497,12 +2052,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1497
2052
|
},
|
|
1498
2053
|
});
|
|
1499
2054
|
const sanitizeFabrics = (fabrics, resetSessions = false) => {
|
|
2055
|
+
// New type of fabric information: Record<FabricIndex, ExposedFabricInformation>
|
|
1500
2056
|
const sanitizedFabrics = this.sanitizeFabricInformations(Array.from(Object.values(fabrics)));
|
|
1501
2057
|
this.log.info(`Fabrics: ${debugStringify(sanitizedFabrics)}`);
|
|
1502
2058
|
if (this.bridgeMode === 'bridge') {
|
|
1503
2059
|
this.matterbridgeFabricInformations = sanitizedFabrics;
|
|
1504
2060
|
if (resetSessions)
|
|
1505
|
-
this.matterbridgeSessionInformations = undefined;
|
|
2061
|
+
this.matterbridgeSessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
|
|
1506
2062
|
this.matterbridgePaired = true;
|
|
1507
2063
|
}
|
|
1508
2064
|
if (this.bridgeMode === 'childbridge') {
|
|
@@ -1510,13 +2066,19 @@ export class Matterbridge extends EventEmitter {
|
|
|
1510
2066
|
if (plugin) {
|
|
1511
2067
|
plugin.fabricInformations = sanitizedFabrics;
|
|
1512
2068
|
if (resetSessions)
|
|
1513
|
-
plugin.sessionInformations = undefined;
|
|
2069
|
+
plugin.sessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
|
|
1514
2070
|
plugin.paired = true;
|
|
1515
2071
|
}
|
|
1516
2072
|
}
|
|
1517
2073
|
};
|
|
2074
|
+
/**
|
|
2075
|
+
* This event is triggered when the device is initially commissioned successfully.
|
|
2076
|
+
* This means: It is added to the first fabric.
|
|
2077
|
+
*/
|
|
1518
2078
|
serverNode.lifecycle.commissioned.on(() => this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`));
|
|
2079
|
+
/** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
|
|
1519
2080
|
serverNode.lifecycle.decommissioned.on(() => this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`));
|
|
2081
|
+
/** This event is triggered when the device went online. This means that it is discoverable in the network. */
|
|
1520
2082
|
serverNode.lifecycle.online.on(async () => {
|
|
1521
2083
|
this.log.notice(`Server node for ${storeId} is online`);
|
|
1522
2084
|
if (!serverNode.lifecycle.isCommissioned) {
|
|
@@ -1585,6 +2147,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1585
2147
|
this.frontend.wssSendRefreshRequired('settings');
|
|
1586
2148
|
this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
|
|
1587
2149
|
});
|
|
2150
|
+
/** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
|
|
1588
2151
|
serverNode.lifecycle.offline.on(() => {
|
|
1589
2152
|
this.log.notice(`Server node for ${storeId} is offline`);
|
|
1590
2153
|
if (this.bridgeMode === 'bridge') {
|
|
@@ -1608,6 +2171,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
1608
2171
|
this.frontend.wssSendRefreshRequired('settings');
|
|
1609
2172
|
this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
|
|
1610
2173
|
});
|
|
2174
|
+
/**
|
|
2175
|
+
* This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
|
|
2176
|
+
* information is needed.
|
|
2177
|
+
*/
|
|
1611
2178
|
serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
|
|
1612
2179
|
let action = '';
|
|
1613
2180
|
switch (fabricAction) {
|
|
@@ -1641,16 +2208,24 @@ export class Matterbridge extends EventEmitter {
|
|
|
1641
2208
|
}
|
|
1642
2209
|
}
|
|
1643
2210
|
};
|
|
2211
|
+
/**
|
|
2212
|
+
* This event is triggered when an operative new session was opened by a Controller.
|
|
2213
|
+
* It is not triggered for the initial commissioning process, just afterwards for real connections.
|
|
2214
|
+
*/
|
|
1644
2215
|
serverNode.events.sessions.opened.on((session) => {
|
|
1645
2216
|
this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1646
2217
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
1647
2218
|
this.frontend.wssSendRefreshRequired('sessions');
|
|
1648
2219
|
});
|
|
2220
|
+
/**
|
|
2221
|
+
* This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
|
|
2222
|
+
*/
|
|
1649
2223
|
serverNode.events.sessions.closed.on((session) => {
|
|
1650
2224
|
this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1651
2225
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
1652
2226
|
this.frontend.wssSendRefreshRequired('sessions');
|
|
1653
2227
|
});
|
|
2228
|
+
/** This event is triggered when a subscription gets added or removed on an operative session. */
|
|
1654
2229
|
serverNode.events.sessions.subscriptionsChanged.on((session) => {
|
|
1655
2230
|
this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
1656
2231
|
sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
|
|
@@ -1659,24 +2234,42 @@ export class Matterbridge extends EventEmitter {
|
|
|
1659
2234
|
this.log.info(`Created server node for ${storeId}`);
|
|
1660
2235
|
return serverNode;
|
|
1661
2236
|
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Starts the specified server node.
|
|
2239
|
+
*
|
|
2240
|
+
* @param {ServerNode} [matterServerNode] - The server node to start.
|
|
2241
|
+
* @returns {Promise<void>} A promise that resolves when the server node has started.
|
|
2242
|
+
*/
|
|
1662
2243
|
async startServerNode(matterServerNode) {
|
|
1663
2244
|
if (!matterServerNode)
|
|
1664
2245
|
return;
|
|
1665
2246
|
this.log.notice(`Starting ${matterServerNode.id} server node`);
|
|
1666
2247
|
await matterServerNode.start();
|
|
1667
2248
|
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Stops the specified server node.
|
|
2251
|
+
*
|
|
2252
|
+
* @param {ServerNode} matterServerNode - The server node to stop.
|
|
2253
|
+
* @returns {Promise<void>} A promise that resolves when the server node has stopped.
|
|
2254
|
+
*/
|
|
1668
2255
|
async stopServerNode(matterServerNode) {
|
|
1669
2256
|
if (!matterServerNode)
|
|
1670
2257
|
return;
|
|
1671
2258
|
this.log.notice(`Closing ${matterServerNode.id} server node`);
|
|
1672
2259
|
try {
|
|
1673
|
-
await withTimeout(matterServerNode.close(), 30000);
|
|
2260
|
+
await withTimeout(matterServerNode.close(), 30000); // 30 seconds timeout to allow slow devices to close gracefully
|
|
1674
2261
|
this.log.info(`Closed ${matterServerNode.id} server node`);
|
|
1675
2262
|
}
|
|
1676
2263
|
catch (error) {
|
|
1677
2264
|
this.log.error(`Failed to close ${matterServerNode.id} server node: ${error instanceof Error ? error.message : error}`);
|
|
1678
2265
|
}
|
|
1679
2266
|
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Advertises the specified server node.
|
|
2269
|
+
*
|
|
2270
|
+
* @param {ServerNode} [matterServerNode] - The server node to advertise.
|
|
2271
|
+
* @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.
|
|
2272
|
+
*/
|
|
1680
2273
|
async advertiseServerNode(matterServerNode) {
|
|
1681
2274
|
if (matterServerNode) {
|
|
1682
2275
|
await matterServerNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
|
|
@@ -1685,24 +2278,45 @@ export class Matterbridge extends EventEmitter {
|
|
|
1685
2278
|
return { qrPairingCode, manualPairingCode };
|
|
1686
2279
|
}
|
|
1687
2280
|
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Stop advertise the specified server node.
|
|
2283
|
+
*
|
|
2284
|
+
* @param {ServerNode} [matterServerNode] - The server node to advertise.
|
|
2285
|
+
* @returns {Promise<void>} A promise that resolves when the server node has stopped advertising.
|
|
2286
|
+
*/
|
|
1688
2287
|
async stopAdvertiseServerNode(matterServerNode) {
|
|
1689
2288
|
if (matterServerNode && matterServerNode.lifecycle.isOnline) {
|
|
1690
2289
|
await matterServerNode.env.get(DeviceCommissioner)?.endCommissioning();
|
|
1691
2290
|
this.log.notice(`Stopped advertising for ${matterServerNode.id}`);
|
|
1692
2291
|
}
|
|
1693
2292
|
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Creates an aggregator node with the specified storage context.
|
|
2295
|
+
*
|
|
2296
|
+
* @param {StorageContext} storageContext - The storage context for the aggregator node.
|
|
2297
|
+
* @returns {Promise<Endpoint<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
|
|
2298
|
+
*/
|
|
1694
2299
|
async createAggregatorNode(storageContext) {
|
|
1695
2300
|
this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator `);
|
|
1696
2301
|
const aggregatorNode = new Endpoint(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
|
|
1697
2302
|
return aggregatorNode;
|
|
1698
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Adds a MatterbridgeEndpoint to the specified plugin.
|
|
2306
|
+
*
|
|
2307
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2308
|
+
* @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
|
|
2309
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been added.
|
|
2310
|
+
*/
|
|
1699
2311
|
async addBridgedEndpoint(pluginName, device) {
|
|
2312
|
+
// Check if the plugin is registered
|
|
1700
2313
|
const plugin = this.plugins.get(pluginName);
|
|
1701
2314
|
if (!plugin) {
|
|
1702
2315
|
this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
|
|
1703
2316
|
return;
|
|
1704
2317
|
}
|
|
1705
2318
|
if (this.bridgeMode === 'bridge') {
|
|
2319
|
+
// Register and add the device to the matterbridge aggregator node
|
|
1706
2320
|
this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
|
|
1707
2321
|
if (!this.aggregatorNode) {
|
|
1708
2322
|
this.log.error('Aggregator node not found for Matterbridge');
|
|
@@ -1719,6 +2333,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1719
2333
|
}
|
|
1720
2334
|
}
|
|
1721
2335
|
else if (this.bridgeMode === 'childbridge') {
|
|
2336
|
+
// Register and add the device to the plugin server node
|
|
1722
2337
|
if (plugin.type === 'AccessoryPlatform') {
|
|
1723
2338
|
try {
|
|
1724
2339
|
this.log.debug(`Creating endpoint ${dev}${device.deviceName}${db} for AccessoryPlatform plugin ${plg}${plugin.name}${db} server node`);
|
|
@@ -1735,10 +2350,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1735
2350
|
return;
|
|
1736
2351
|
}
|
|
1737
2352
|
}
|
|
2353
|
+
// Register and add the device to the plugin aggregator node
|
|
1738
2354
|
if (plugin.type === 'DynamicPlatform') {
|
|
1739
2355
|
try {
|
|
1740
2356
|
this.log.debug(`Adding bridged endpoint ${dev}${device.deviceName}${db} for DynamicPlatform plugin ${plg}${plugin.name}${db} aggregator node`);
|
|
1741
2357
|
await this.createDynamicPlugin(plugin);
|
|
2358
|
+
// Fast plugins can add another device before the server node is created
|
|
1742
2359
|
await waiter(`createDynamicPlugin(${plugin.name})`, () => plugin.serverNode?.hasParts === true);
|
|
1743
2360
|
if (!plugin.aggregatorNode) {
|
|
1744
2361
|
this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er}`);
|
|
@@ -1758,17 +2375,28 @@ export class Matterbridge extends EventEmitter {
|
|
|
1758
2375
|
plugin.registeredDevices++;
|
|
1759
2376
|
if (plugin.addedDevices !== undefined)
|
|
1760
2377
|
plugin.addedDevices++;
|
|
2378
|
+
// Add the device to the DeviceManager
|
|
1761
2379
|
this.devices.set(device);
|
|
2380
|
+
// Subscribe to the reachable$Changed event
|
|
1762
2381
|
await this.subscribeAttributeChanged(plugin, device);
|
|
1763
2382
|
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}`);
|
|
1764
2383
|
}
|
|
2384
|
+
/**
|
|
2385
|
+
* Removes a MatterbridgeEndpoint from the specified plugin.
|
|
2386
|
+
*
|
|
2387
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2388
|
+
* @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
|
|
2389
|
+
* @returns {Promise<void>} A promise that resolves when the bridged endpoint has been removed.
|
|
2390
|
+
*/
|
|
1765
2391
|
async removeBridgedEndpoint(pluginName, device) {
|
|
1766
2392
|
this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
|
|
2393
|
+
// Check if the plugin is registered
|
|
1767
2394
|
const plugin = this.plugins.get(pluginName);
|
|
1768
2395
|
if (!plugin) {
|
|
1769
2396
|
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
1770
2397
|
return;
|
|
1771
2398
|
}
|
|
2399
|
+
// Register and add the device to the matterbridge aggregator node
|
|
1772
2400
|
if (this.bridgeMode === 'bridge') {
|
|
1773
2401
|
if (!this.aggregatorNode) {
|
|
1774
2402
|
this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
|
|
@@ -1783,6 +2411,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1783
2411
|
}
|
|
1784
2412
|
else if (this.bridgeMode === 'childbridge') {
|
|
1785
2413
|
if (plugin.type === 'AccessoryPlatform') {
|
|
2414
|
+
// Nothing to do here since the server node has no aggregator node but only the device itself
|
|
1786
2415
|
}
|
|
1787
2416
|
else if (plugin.type === 'DynamicPlatform') {
|
|
1788
2417
|
if (!plugin.aggregatorNode) {
|
|
@@ -1797,8 +2426,21 @@ export class Matterbridge extends EventEmitter {
|
|
|
1797
2426
|
if (plugin.addedDevices !== undefined)
|
|
1798
2427
|
plugin.addedDevices--;
|
|
1799
2428
|
}
|
|
2429
|
+
// Remove the device from the DeviceManager
|
|
1800
2430
|
this.devices.remove(device);
|
|
1801
2431
|
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Removes all bridged endpoints from the specified plugin.
|
|
2434
|
+
*
|
|
2435
|
+
* @param {string} pluginName - The name of the plugin.
|
|
2436
|
+
* @param {number} [delay=0] - The delay in milliseconds between removing each bridged endpoint (default: 0).
|
|
2437
|
+
* @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
|
|
2438
|
+
*
|
|
2439
|
+
* @remarks
|
|
2440
|
+
* This method iterates through all devices in the DeviceManager and removes each bridged endpoint associated with the specified plugin.
|
|
2441
|
+
* It also applies a delay between each removal if specified.
|
|
2442
|
+
* The delay is useful to allow the controllers to receive a single subscription for each device removed.
|
|
2443
|
+
*/
|
|
1802
2444
|
async removeAllBridgedEndpoints(pluginName, delay = 0) {
|
|
1803
2445
|
this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}`);
|
|
1804
2446
|
for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
|
|
@@ -1809,9 +2451,25 @@ export class Matterbridge extends EventEmitter {
|
|
|
1809
2451
|
if (delay > 0)
|
|
1810
2452
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1811
2453
|
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Subscribes to the attribute change event for the given device and plugin.
|
|
2456
|
+
* Specifically, it listens for changes in the 'reachable' attribute of the
|
|
2457
|
+
* BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
|
|
2458
|
+
*
|
|
2459
|
+
* @param {RegisteredPlugin} plugin - The plugin associated with the device.
|
|
2460
|
+
* @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
|
|
2461
|
+
* @returns {Promise<void>} A promise that resolves when the subscription is set up.
|
|
2462
|
+
*/
|
|
1812
2463
|
async subscribeAttributeChanged(plugin, device) {
|
|
1813
2464
|
this.log.info(`Subscribing attributes for endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) plugin ${plg}${plugin.name}${nf}`);
|
|
1814
2465
|
if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && plugin.serverNode) {
|
|
2466
|
+
/*
|
|
2467
|
+
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) subscribed to reachable$Changed`);
|
|
2468
|
+
setTimeout(async () => {
|
|
2469
|
+
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) changed to reachable false`);
|
|
2470
|
+
await plugin.serverNode?.setStateOf(BasicInformationServer, { reachable: false });
|
|
2471
|
+
}, 60000).unref();
|
|
2472
|
+
*/
|
|
1815
2473
|
plugin.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
|
|
1816
2474
|
this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) is ${reachable ? 'reachable' : 'unreachable'}`);
|
|
1817
2475
|
this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, 'BasicInformationServer', 'reachable', reachable);
|
|
@@ -1824,6 +2482,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1824
2482
|
});
|
|
1825
2483
|
}
|
|
1826
2484
|
}
|
|
2485
|
+
/**
|
|
2486
|
+
* Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2487
|
+
*
|
|
2488
|
+
* @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
|
|
2489
|
+
* @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
|
|
2490
|
+
*/
|
|
1827
2491
|
sanitizeFabricInformations(fabricInfo) {
|
|
1828
2492
|
return fabricInfo.map((info) => {
|
|
1829
2493
|
return {
|
|
@@ -1837,6 +2501,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1837
2501
|
};
|
|
1838
2502
|
});
|
|
1839
2503
|
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
|
|
2506
|
+
*
|
|
2507
|
+
* @param {SessionInformation[]} sessionInfo - The array of session information objects.
|
|
2508
|
+
* @returns {SanitizedSessionInformation[]} An array of sanitized session information objects.
|
|
2509
|
+
*/
|
|
1840
2510
|
sanitizeSessionInformation(sessionInfo) {
|
|
1841
2511
|
return sessionInfo
|
|
1842
2512
|
.filter((session) => session.isPeerActive)
|
|
@@ -1864,7 +2534,20 @@ export class Matterbridge extends EventEmitter {
|
|
|
1864
2534
|
};
|
|
1865
2535
|
});
|
|
1866
2536
|
}
|
|
2537
|
+
/**
|
|
2538
|
+
* Sets the reachability of the specified aggregator node bridged devices and trigger.
|
|
2539
|
+
* @param {Endpoint<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
|
|
2540
|
+
* @param {boolean} reachable - A boolean indicating the reachability status to set.
|
|
2541
|
+
*/
|
|
2542
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1867
2543
|
async setAggregatorReachability(aggregatorNode, reachable) {
|
|
2544
|
+
/*
|
|
2545
|
+
for (const child of aggregatorNode.parts) {
|
|
2546
|
+
this.log.debug(`Setting reachability of ${(child as unknown as MatterbridgeEndpoint)?.deviceName} to ${reachable}`);
|
|
2547
|
+
await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
|
|
2548
|
+
child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
|
|
2549
|
+
}
|
|
2550
|
+
*/
|
|
1868
2551
|
}
|
|
1869
2552
|
getVendorIdName = (vendorId) => {
|
|
1870
2553
|
if (!vendorId)
|
|
@@ -1907,14 +2590,29 @@ export class Matterbridge extends EventEmitter {
|
|
|
1907
2590
|
}
|
|
1908
2591
|
return vendorName;
|
|
1909
2592
|
};
|
|
2593
|
+
/**
|
|
2594
|
+
* Spawns a child process with the given command and arguments.
|
|
2595
|
+
* @param {string} command - The command to execute.
|
|
2596
|
+
* @param {string[]} args - The arguments to pass to the command (default: []).
|
|
2597
|
+
* @returns {Promise<boolean>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
|
|
2598
|
+
*/
|
|
1910
2599
|
async spawnCommand(command, args = []) {
|
|
1911
2600
|
const { spawn } = await import('node:child_process');
|
|
2601
|
+
/*
|
|
2602
|
+
npm > npm.cmd on windows
|
|
2603
|
+
cmd.exe ['dir'] on windows
|
|
2604
|
+
await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
|
|
2605
|
+
*/
|
|
1912
2606
|
const cmdLine = command + ' ' + args.join(' ');
|
|
1913
2607
|
if (process.platform === 'win32' && command === 'npm') {
|
|
2608
|
+
// Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
|
|
1914
2609
|
const argstring = 'npm ' + args.join(' ');
|
|
1915
2610
|
args.splice(0, args.length, '/c', argstring);
|
|
1916
2611
|
command = 'cmd.exe';
|
|
1917
2612
|
}
|
|
2613
|
+
// Decide when using sudo on linux
|
|
2614
|
+
// When you need sudo: Spawn stderr: npm error Error: EACCES: permission denied
|
|
2615
|
+
// When you don't need sudo: Failed to start child process "npm install -g matterbridge-eve-door": spawn sudo ENOENT
|
|
1918
2616
|
if (hasParameter('sudo') || (process.platform === 'linux' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo'))) {
|
|
1919
2617
|
args.unshift(command);
|
|
1920
2618
|
command = 'sudo';
|
|
@@ -1973,3 +2671,4 @@ export class Matterbridge extends EventEmitter {
|
|
|
1973
2671
|
});
|
|
1974
2672
|
}
|
|
1975
2673
|
}
|
|
2674
|
+
//# sourceMappingURL=matterbridge.js.map
|