matterbridge 3.0.2-dev-20250514-0b26f0a → 3.0.2

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