matterbridge 3.0.0-edge.8 → 3.0.0

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