matterbridge 2.2.6-dev.5 → 2.2.6

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 (147) hide show
  1. package/CHANGELOG.md +2 -1
  2. package/dist/cli.d.ts +29 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +37 -2
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cluster/export.d.ts +2 -0
  7. package/dist/cluster/export.d.ts.map +1 -0
  8. package/dist/cluster/export.js +2 -0
  9. package/dist/cluster/export.js.map +1 -0
  10. package/dist/defaultConfigSchema.d.ts +27 -0
  11. package/dist/defaultConfigSchema.d.ts.map +1 -0
  12. package/dist/defaultConfigSchema.js +23 -2
  13. package/dist/defaultConfigSchema.js.map +1 -0
  14. package/dist/deviceManager.d.ts +114 -0
  15. package/dist/deviceManager.d.ts.map +1 -0
  16. package/dist/deviceManager.js +94 -1
  17. package/dist/deviceManager.js.map +1 -0
  18. package/dist/frontend.d.ts +221 -0
  19. package/dist/frontend.d.ts.map +1 -0
  20. package/dist/frontend.js +325 -19
  21. package/dist/frontend.js.map +1 -0
  22. package/dist/index.d.ts +35 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +28 -1
  25. package/dist/index.js.map +1 -0
  26. package/dist/logger/export.d.ts +2 -0
  27. package/dist/logger/export.d.ts.map +1 -0
  28. package/dist/logger/export.js +1 -0
  29. package/dist/logger/export.js.map +1 -0
  30. package/dist/matter/behaviors.d.ts +2 -0
  31. package/dist/matter/behaviors.d.ts.map +1 -0
  32. package/dist/matter/behaviors.js +2 -0
  33. package/dist/matter/behaviors.js.map +1 -0
  34. package/dist/matter/clusters.d.ts +2 -0
  35. package/dist/matter/clusters.d.ts.map +1 -0
  36. package/dist/matter/clusters.js +2 -0
  37. package/dist/matter/clusters.js.map +1 -0
  38. package/dist/matter/devices.d.ts +2 -0
  39. package/dist/matter/devices.d.ts.map +1 -0
  40. package/dist/matter/devices.js +2 -0
  41. package/dist/matter/devices.js.map +1 -0
  42. package/dist/matter/endpoints.d.ts +2 -0
  43. package/dist/matter/endpoints.d.ts.map +1 -0
  44. package/dist/matter/endpoints.js +2 -0
  45. package/dist/matter/endpoints.js.map +1 -0
  46. package/dist/matter/export.d.ts +5 -0
  47. package/dist/matter/export.d.ts.map +1 -0
  48. package/dist/matter/export.js +2 -0
  49. package/dist/matter/export.js.map +1 -0
  50. package/dist/matter/types.d.ts +3 -0
  51. package/dist/matter/types.d.ts.map +1 -0
  52. package/dist/matter/types.js +2 -0
  53. package/dist/matter/types.js.map +1 -0
  54. package/dist/matterbridge.d.ts +425 -0
  55. package/dist/matterbridge.d.ts.map +1 -0
  56. package/dist/matterbridge.js +753 -47
  57. package/dist/matterbridge.js.map +1 -0
  58. package/dist/matterbridgeAccessoryPlatform.d.ts +39 -0
  59. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  60. package/dist/matterbridgeAccessoryPlatform.js +33 -0
  61. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  62. package/dist/matterbridgeBehaviors.d.ts +1056 -0
  63. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  64. package/dist/matterbridgeBehaviors.js +32 -1
  65. package/dist/matterbridgeBehaviors.js.map +1 -0
  66. package/dist/matterbridgeDeviceTypes.d.ts +177 -0
  67. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  68. package/dist/matterbridgeDeviceTypes.js +112 -11
  69. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  70. package/dist/matterbridgeDynamicPlatform.d.ts +39 -0
  71. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  72. package/dist/matterbridgeDynamicPlatform.js +33 -0
  73. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  74. package/dist/matterbridgeEndpoint.d.ts +835 -0
  75. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  76. package/dist/matterbridgeEndpoint.js +690 -6
  77. package/dist/matterbridgeEndpoint.js.map +1 -0
  78. package/dist/matterbridgeEndpointHelpers.d.ts +2275 -0
  79. package/dist/matterbridgeEndpointHelpers.d.ts.map +1 -0
  80. package/dist/matterbridgeEndpointHelpers.js +118 -9
  81. package/dist/matterbridgeEndpointHelpers.js.map +1 -0
  82. package/dist/matterbridgePlatform.d.ts +285 -0
  83. package/dist/matterbridgePlatform.d.ts.map +1 -0
  84. package/dist/matterbridgePlatform.js +216 -7
  85. package/dist/matterbridgePlatform.js.map +1 -0
  86. package/dist/matterbridgeTypes.d.ts +179 -0
  87. package/dist/matterbridgeTypes.d.ts.map +1 -0
  88. package/dist/matterbridgeTypes.js +24 -0
  89. package/dist/matterbridgeTypes.js.map +1 -0
  90. package/dist/pluginManager.d.ts +236 -0
  91. package/dist/pluginManager.d.ts.map +1 -0
  92. package/dist/pluginManager.js +229 -3
  93. package/dist/pluginManager.js.map +1 -0
  94. package/dist/shelly.d.ts +92 -0
  95. package/dist/shelly.d.ts.map +1 -0
  96. package/dist/shelly.js +146 -6
  97. package/dist/shelly.js.map +1 -0
  98. package/dist/storage/export.d.ts +2 -0
  99. package/dist/storage/export.d.ts.map +1 -0
  100. package/dist/storage/export.js +1 -0
  101. package/dist/storage/export.js.map +1 -0
  102. package/dist/update.d.ts +32 -0
  103. package/dist/update.d.ts.map +1 -0
  104. package/dist/update.js +45 -0
  105. package/dist/update.js.map +1 -0
  106. package/dist/utils/colorUtils.d.ts +61 -0
  107. package/dist/utils/colorUtils.d.ts.map +1 -0
  108. package/dist/utils/colorUtils.js +205 -2
  109. package/dist/utils/colorUtils.js.map +1 -0
  110. package/dist/utils/copyDirectory.d.ts +32 -0
  111. package/dist/utils/copyDirectory.d.ts.map +1 -0
  112. package/dist/utils/copyDirectory.js +37 -1
  113. package/dist/utils/copyDirectory.js.map +1 -0
  114. package/dist/utils/createZip.d.ts +38 -0
  115. package/dist/utils/createZip.d.ts.map +1 -0
  116. package/dist/utils/createZip.js +42 -2
  117. package/dist/utils/createZip.js.map +1 -0
  118. package/dist/utils/deepCopy.d.ts +31 -0
  119. package/dist/utils/deepCopy.d.ts.map +1 -0
  120. package/dist/utils/deepCopy.js +40 -0
  121. package/dist/utils/deepCopy.js.map +1 -0
  122. package/dist/utils/deepEqual.d.ts +53 -0
  123. package/dist/utils/deepEqual.d.ts.map +1 -0
  124. package/dist/utils/deepEqual.js +65 -1
  125. package/dist/utils/deepEqual.js.map +1 -0
  126. package/dist/utils/export.d.ts +10 -0
  127. package/dist/utils/export.d.ts.map +1 -0
  128. package/dist/utils/export.js +1 -0
  129. package/dist/utils/export.js.map +1 -0
  130. package/dist/utils/isvalid.d.ts +87 -0
  131. package/dist/utils/isvalid.d.ts.map +1 -0
  132. package/dist/utils/isvalid.js +86 -0
  133. package/dist/utils/isvalid.js.map +1 -0
  134. package/dist/utils/network.d.ts +69 -0
  135. package/dist/utils/network.d.ts.map +1 -0
  136. package/dist/utils/network.js +77 -8
  137. package/dist/utils/network.js.map +1 -0
  138. package/dist/utils/parameter.d.ts +44 -0
  139. package/dist/utils/parameter.d.ts.map +1 -0
  140. package/dist/utils/parameter.js +41 -0
  141. package/dist/utils/parameter.js.map +1 -0
  142. package/dist/utils/wait.d.ts +43 -0
  143. package/dist/utils/wait.d.ts.map +1 -0
  144. package/dist/utils/wait.js +48 -5
  145. package/dist/utils/wait.js.map +1 -0
  146. package/npm-shrinkwrap.json +2 -2
  147. package/package.json +2 -1
@@ -1,9 +1,35 @@
1
+ /**
2
+ * This file contains the class Matterbridge.
3
+ *
4
+ * @file matterbridge.ts
5
+ * @author Luca Liguori
6
+ * @date 2023-12-29
7
+ * @version 1.5.2
8
+ *
9
+ * Copyright 2023, 2024, 2025 Luca Liguori.
10
+ *
11
+ * Licensed under the Apache License, Version 2.0 (the "License");
12
+ * you may not use this file except in compliance with the License.
13
+ * You may obtain a copy of the License at
14
+ *
15
+ * http://www.apache.org/licenses/LICENSE-2.0
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License. *
22
+ */
23
+ // Node.js modules
1
24
  import os from 'node:os';
2
25
  import path from 'node:path';
3
26
  import { promises as fs } from 'node:fs';
4
27
  import EventEmitter from 'node:events';
28
+ // AnsiLogger module
5
29
  import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN } from './logger/export.js';
30
+ // NodeStorage module
6
31
  import { NodeStorageManager } from './storage/export.js';
32
+ // Matterbridge
7
33
  import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout } from './utils/export.js';
8
34
  import { logInterfaces, getGlobalNodeModules } from './utils/network.js';
9
35
  import { PluginManager } from './pluginManager.js';
@@ -11,14 +37,19 @@ import { DeviceManager } from './deviceManager.js';
11
37
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
12
38
  import { bridge } from './matterbridgeDeviceTypes.js';
13
39
  import { Frontend } from './frontend.js';
40
+ // @matter
14
41
  import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode } from '@matter/main';
15
42
  import { DeviceCommissioner, FabricAction, MdnsService, PaseClient } from '@matter/main/protocol';
16
43
  import { AggregatorEndpoint } from '@matter/main/endpoints';
17
44
  import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors/bridged-device-basic-information';
18
45
  import { BasicInformationServer } from '@matter/main/behaviors/basic-information';
46
+ // Default colors
19
47
  const plg = '\u001B[38;5;33m';
20
48
  const dev = '\u001B[38;5;79m';
21
49
  const typ = '\u001B[38;5;207m';
50
+ /**
51
+ * Represents the Matterbridge application.
52
+ */
22
53
  export class Matterbridge extends EventEmitter {
23
54
  systemInformation = {
24
55
  interfaceName: '',
@@ -63,7 +94,7 @@ export class Matterbridge extends EventEmitter {
63
94
  shellySysUpdate: false,
64
95
  shellyMainUpdate: false,
65
96
  profile: getParameter('profile'),
66
- loggerLevel: "info",
97
+ loggerLevel: "info" /* LogLevel.INFO */,
67
98
  fileLogger: false,
68
99
  matterLoggerLevel: MatterLogLevel.INFO,
69
100
  matterFileLogger: false,
@@ -101,9 +132,11 @@ export class Matterbridge extends EventEmitter {
101
132
  plugins;
102
133
  devices;
103
134
  frontend = new Frontend(this);
135
+ // Matterbridge storage
104
136
  nodeStorage;
105
137
  nodeContext;
106
138
  nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
139
+ // Cleanup
107
140
  hasCleanupStarted = false;
108
141
  initialized = false;
109
142
  execRunningCount = 0;
@@ -116,38 +149,72 @@ export class Matterbridge extends EventEmitter {
116
149
  sigtermHandler;
117
150
  exceptionHandler;
118
151
  rejectionHandler;
152
+ // Matter environment
119
153
  environment = Environment.default;
154
+ // Matter storage
120
155
  matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
121
156
  matterStorageService;
122
157
  matterStorageManager;
123
158
  matterbridgeContext;
124
159
  mattercontrollerContext;
125
- mdnsInterface;
126
- ipv4address;
127
- ipv6address;
128
- port;
129
- passcode;
130
- discriminator;
160
+ // Matter parameters
161
+ mdnsInterface; // matter server node mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
162
+ ipv4address; // matter server node listeningAddressIpv4
163
+ ipv6address; // matter server node listeningAddressIpv6
164
+ port; // first server node port
165
+ passcode; // first server node passcode
166
+ discriminator; // first server node discriminator
131
167
  serverNode;
132
168
  aggregatorNode;
133
169
  aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
170
+ aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
134
171
  aggregatorProductId = getIntParameter('productId') ?? 0x8000;
172
+ aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
135
173
  static instance;
174
+ // We load asyncronously so is private
136
175
  constructor() {
137
176
  super();
138
177
  }
178
+ /**
179
+ * Emits an event of the specified type with the provided arguments.
180
+ *
181
+ * @template K - The type of the event.
182
+ * @param {K} eventName - The name of the event to emit.
183
+ * @param {...MatterbridgeEvent[K]} args - The arguments to pass to the event listeners.
184
+ * @returns {boolean} - Returns true if the event had listeners, false otherwise.
185
+ */
139
186
  emit(eventName, ...args) {
140
187
  return super.emit(eventName, ...args);
141
188
  }
189
+ /**
190
+ * Registers an event listener for the specified event type.
191
+ *
192
+ * @template K - The type of the event.
193
+ * @param {K} eventName - The name of the event to listen for.
194
+ * @param {(...args: MatterbridgeEvent[K]) => void} listener - The callback function to invoke when the event is emitted.
195
+ * @returns {this} - Returns the instance of the Matterbridge class.
196
+ */
142
197
  on(eventName, listener) {
143
198
  return super.on(eventName, listener);
144
199
  }
200
+ /**
201
+ * Retrieves the list of Matterbridge devices.
202
+ * @returns {MatterbridgeEndpoint[]} An array of MatterbridgeDevice objects.
203
+ */
145
204
  getDevices() {
146
205
  return this.devices.array();
147
206
  }
207
+ /**
208
+ * Retrieves the list of registered plugins.
209
+ * @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects.
210
+ */
148
211
  getPlugins() {
149
212
  return this.plugins.array();
150
213
  }
214
+ /**
215
+ * Set the logger logLevel for the Matterbridge classes.
216
+ * @param {LogLevel} logLevel The logger logLevel to set.
217
+ */
151
218
  async setLogLevel(logLevel) {
152
219
  if (this.log)
153
220
  this.log.logLevel = logLevel;
@@ -161,19 +228,31 @@ export class Matterbridge extends EventEmitter {
161
228
  for (const plugin of this.plugins) {
162
229
  if (!plugin.platform || !plugin.platform.log || !plugin.platform.config)
163
230
  continue;
164
- plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" : this.log.logLevel;
165
- await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" : this.log.logLevel);
166
- }
167
- let callbackLogLevel = "notice";
168
- if (this.matterbridgeInformation.loggerLevel === "info" || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
169
- callbackLogLevel = "info";
170
- if (this.matterbridgeInformation.loggerLevel === "debug" || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
171
- callbackLogLevel = "debug";
231
+ plugin.platform.log.logLevel = plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
232
+ await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug === true ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
233
+ }
234
+ // Set the global logger callback for the WebSocketServer to the common minimum logLevel
235
+ let callbackLogLevel = "notice" /* LogLevel.NOTICE */;
236
+ if (this.matterbridgeInformation.loggerLevel === "info" /* LogLevel.INFO */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.INFO)
237
+ callbackLogLevel = "info" /* LogLevel.INFO */;
238
+ if (this.matterbridgeInformation.loggerLevel === "debug" /* LogLevel.DEBUG */ || this.matterbridgeInformation.matterLoggerLevel === MatterLogLevel.DEBUG)
239
+ callbackLogLevel = "debug" /* LogLevel.DEBUG */;
172
240
  AnsiLogger.setGlobalCallback(this.frontend.wssSendMessage.bind(this.frontend), callbackLogLevel);
173
241
  this.log.debug(`WebSocketServer logger global callback set to ${callbackLogLevel}`);
174
242
  }
243
+ /** ***********************************************************************************************************************************/
244
+ /** loadInstance() and cleanup() methods */
245
+ /** ***********************************************************************************************************************************/
246
+ /**
247
+ * Loads an instance of the Matterbridge class.
248
+ * If an instance already exists, return that instance.
249
+ *
250
+ * @param initialize - Whether to initialize the Matterbridge instance after loading.
251
+ * @returns The loaded Matterbridge instance.
252
+ */
175
253
  static async loadInstance(initialize = false) {
176
254
  if (!Matterbridge.instance) {
255
+ // eslint-disable-next-line no-console
177
256
  if (hasParameter('debug'))
178
257
  console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
179
258
  Matterbridge.instance = new Matterbridge();
@@ -182,8 +261,14 @@ export class Matterbridge extends EventEmitter {
182
261
  }
183
262
  return Matterbridge.instance;
184
263
  }
264
+ /**
265
+ * Call cleanup().
266
+ * @deprecated This method is deprecated and is only used for jest tests.
267
+ *
268
+ */
185
269
  async destroyInstance() {
186
270
  this.log.info(`Destroy instance...`);
271
+ // Save server nodes to close
187
272
  const servers = [];
188
273
  if (this.bridgeMode === 'bridge') {
189
274
  if (this.serverNode)
@@ -195,55 +280,81 @@ export class Matterbridge extends EventEmitter {
195
280
  servers.push(plugin.serverNode);
196
281
  }
197
282
  }
283
+ // Cleanup
198
284
  await this.cleanup('destroying instance...', false);
285
+ // Close servers mdns service
199
286
  this.log.info(`Dispose ${servers.length} MdnsService...`);
200
287
  for (const server of servers) {
201
288
  await server.env.get(MdnsService)[Symbol.asyncDispose]();
202
289
  this.log.info(`Closed ${server.id} MdnsService`);
203
290
  }
291
+ // Wait for the cleanup to finish
204
292
  await new Promise((resolve) => {
205
293
  setTimeout(resolve, 1000);
206
294
  });
207
295
  }
296
+ /**
297
+ * Initializes the Matterbridge application.
298
+ *
299
+ * @remarks
300
+ * This method performs the necessary setup and initialization steps for the Matterbridge application.
301
+ * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
302
+ * node version, registers signal handlers, initializes storage, and parses the command line.
303
+ *
304
+ * @returns A Promise that resolves when the initialization is complete.
305
+ */
208
306
  async initialize() {
307
+ // Set the restart mode
209
308
  if (hasParameter('service'))
210
309
  this.restartMode = 'service';
211
310
  if (hasParameter('docker'))
212
311
  this.restartMode = 'docker';
312
+ // Set the matterbridge directory
213
313
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
214
314
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
315
+ // Setup the matter environment
215
316
  this.environment.vars.set('log.level', MatterLogLevel.INFO);
216
317
  this.environment.vars.set('log.format', MatterLogFormat.ANSI);
217
318
  this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName));
218
319
  this.environment.vars.set('runtime.signals', false);
219
320
  this.environment.vars.set('runtime.exitcode', false);
220
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
321
+ // Create the matterbridge logger
322
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
323
+ // Register process handlers
221
324
  this.registerProcessHandlers();
325
+ // Initialize nodeStorage and nodeContext
222
326
  try {
223
327
  this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
224
328
  this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
225
329
  this.log.debug('Creating node storage context for matterbridge');
226
330
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
331
+ // TODO: Remove this code when node-persist-manager is updated
332
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
333
  const keys = (await this.nodeStorage?.storage.keys());
228
334
  for (const key of keys) {
229
335
  this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
336
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
337
  await this.nodeStorage?.storage.get(key);
231
338
  }
232
339
  const storages = await this.nodeStorage.getStorageNames();
233
340
  for (const storage of storages) {
234
341
  this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
235
342
  const nodeContext = await this.nodeStorage?.createStorage(storage);
343
+ // TODO: Remove this code when node-persist-manager is updated
344
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
236
345
  const keys = (await nodeContext?.storage.keys());
237
346
  keys.forEach(async (key) => {
238
347
  this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
239
348
  await nodeContext?.get(key);
240
349
  });
241
350
  }
351
+ // Creating a backup of the node storage since it is not corrupted
242
352
  this.log.debug('Creating node storage backup...');
243
353
  await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'));
244
354
  this.log.debug('Created node storage backup');
245
355
  }
246
356
  catch (error) {
357
+ // Restoring the backup of the node storage since it is corrupted
247
358
  this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
248
359
  if (hasParameter('norestore')) {
249
360
  this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
@@ -258,41 +369,46 @@ export class Matterbridge extends EventEmitter {
258
369
  this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
259
370
  throw new Error('Fatal error creating node storage manager and context for matterbridge');
260
371
  }
372
+ // Set the first port to use for the commissioning server (will be incremented in childbridge mode)
261
373
  this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
374
+ // Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
262
375
  this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
376
+ // Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
263
377
  this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
264
378
  this.log.debug(`Initializing server node for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
379
+ // Set matterbridge logger level (context: matterbridgeLogLevel)
265
380
  if (hasParameter('logger')) {
266
381
  const level = getParameter('logger');
267
382
  if (level === 'debug') {
268
- this.log.logLevel = "debug";
383
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
269
384
  }
270
385
  else if (level === 'info') {
271
- this.log.logLevel = "info";
386
+ this.log.logLevel = "info" /* LogLevel.INFO */;
272
387
  }
273
388
  else if (level === 'notice') {
274
- this.log.logLevel = "notice";
389
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
275
390
  }
276
391
  else if (level === 'warn') {
277
- this.log.logLevel = "warn";
392
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
278
393
  }
279
394
  else if (level === 'error') {
280
- this.log.logLevel = "error";
395
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
281
396
  }
282
397
  else if (level === 'fatal') {
283
- this.log.logLevel = "fatal";
398
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
284
399
  }
285
400
  else {
286
401
  this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
287
- this.log.logLevel = "info";
402
+ this.log.logLevel = "info" /* LogLevel.INFO */;
288
403
  }
289
404
  }
290
405
  else {
291
- this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" : "info");
406
+ this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', this.matterbridgeInformation.shellyBoard ? "notice" /* LogLevel.NOTICE */ : "info" /* LogLevel.INFO */);
292
407
  }
293
408
  this.frontend.logLevel = this.log.logLevel;
294
409
  MatterbridgeEndpoint.logLevel = this.log.logLevel;
295
410
  this.matterbridgeInformation.loggerLevel = this.log.logLevel;
411
+ // Create the file logger for matterbridge (context: matterbridgeFileLog)
296
412
  if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
297
413
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true);
298
414
  this.matterbridgeInformation.fileLogger = true;
@@ -301,6 +417,7 @@ export class Matterbridge extends EventEmitter {
301
417
  this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`);
302
418
  if (this.profile !== undefined)
303
419
  this.log.debug(`Matterbridge profile: ${this.profile}.`);
420
+ // Set matter.js logger level, format and logger (context: matterLogLevel)
304
421
  if (hasParameter('matterlogger')) {
305
422
  const level = getParameter('matterlogger');
306
423
  if (level === 'debug') {
@@ -332,6 +449,7 @@ export class Matterbridge extends EventEmitter {
332
449
  Logger.format = MatterLogFormat.ANSI;
333
450
  Logger.setLogger('default', this.createMatterLogger());
334
451
  this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
452
+ // Create the file logger for matter.js (context: matterFileLog)
335
453
  if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
336
454
  this.matterbridgeInformation.matterFileLogger = true;
337
455
  Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
@@ -340,6 +458,7 @@ export class Matterbridge extends EventEmitter {
340
458
  });
341
459
  }
342
460
  this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
461
+ // Log network interfaces
343
462
  const networkInterfaces = os.networkInterfaces();
344
463
  const availableAddresses = Object.entries(networkInterfaces);
345
464
  const availableInterfaces = Object.keys(networkInterfaces);
@@ -351,6 +470,7 @@ export class Matterbridge extends EventEmitter {
351
470
  });
352
471
  }
353
472
  }
473
+ // Set the interface to use for matter server node mdnsInterface
354
474
  if (hasParameter('mdnsinterface')) {
355
475
  this.mdnsInterface = getParameter('mdnsinterface');
356
476
  }
@@ -359,6 +479,7 @@ export class Matterbridge extends EventEmitter {
359
479
  if (this.mdnsInterface === '')
360
480
  this.mdnsInterface = undefined;
361
481
  }
482
+ // Validate mdnsInterface
362
483
  if (this.mdnsInterface) {
363
484
  if (!availableInterfaces.includes(this.mdnsInterface)) {
364
485
  this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
@@ -370,6 +491,7 @@ export class Matterbridge extends EventEmitter {
370
491
  }
371
492
  if (this.mdnsInterface)
372
493
  this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
494
+ // Set the listeningAddressIpv4 for the matter commissioning server
373
495
  if (hasParameter('ipv4address')) {
374
496
  this.ipv4address = getParameter('ipv4address');
375
497
  }
@@ -378,6 +500,7 @@ export class Matterbridge extends EventEmitter {
378
500
  if (this.ipv4address === '')
379
501
  this.ipv4address = undefined;
380
502
  }
503
+ // Validate ipv4address
381
504
  if (this.ipv4address) {
382
505
  let isValid = false;
383
506
  for (const [ifaceName, ifaces] of availableAddresses) {
@@ -392,6 +515,7 @@ export class Matterbridge extends EventEmitter {
392
515
  this.ipv4address = undefined;
393
516
  }
394
517
  }
518
+ // Set the listeningAddressIpv6 for the matter commissioning server
395
519
  if (hasParameter('ipv6address')) {
396
520
  this.ipv6address = getParameter('ipv6address');
397
521
  }
@@ -400,6 +524,7 @@ export class Matterbridge extends EventEmitter {
400
524
  if (this.ipv6address === '')
401
525
  this.ipv6address = undefined;
402
526
  }
527
+ // Validate ipv6address
403
528
  if (this.ipv6address) {
404
529
  let isValid = false;
405
530
  for (const [ifaceName, ifaces] of availableAddresses) {
@@ -419,14 +544,19 @@ export class Matterbridge extends EventEmitter {
419
544
  this.ipv6address = undefined;
420
545
  }
421
546
  }
547
+ // Initialize PluginManager
422
548
  this.plugins = new PluginManager(this);
423
549
  await this.plugins.loadFromStorage();
424
550
  this.plugins.logLevel = this.log.logLevel;
551
+ // Initialize DeviceManager
425
552
  this.devices = new DeviceManager(this, this.nodeContext);
426
553
  this.devices.logLevel = this.log.logLevel;
554
+ // Get the plugins from node storage and create the plugins node storage contexts
427
555
  for (const plugin of this.plugins) {
428
556
  const packageJson = await this.plugins.parse(plugin);
429
557
  if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) {
558
+ // Try to reinstall the plugin from npm (for Docker pull and external plugins)
559
+ // We don't do this when the add and other parameters are set because we shut down the process after adding the plugin
430
560
  this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
431
561
  try {
432
562
  await this.spawnCommand('npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']);
@@ -448,6 +578,7 @@ export class Matterbridge extends EventEmitter {
448
578
  await plugin.nodeContext.set('description', plugin.description);
449
579
  await plugin.nodeContext.set('author', plugin.author);
450
580
  }
581
+ // Log system info and create .matterbridge directory
451
582
  await this.logNodeAndSystemInfo();
452
583
  this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
453
584
  `${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
@@ -455,6 +586,7 @@ export class Matterbridge extends EventEmitter {
455
586
  `${hasParameter('controller') ? 'mode controller ' : ''}` +
456
587
  `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
457
588
  `running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
589
+ // Check node version and throw error
458
590
  const minNodeVersion = 18;
459
591
  const nodeVersion = process.versions.node;
460
592
  const versionMajor = parseInt(nodeVersion.split('.')[0]);
@@ -462,9 +594,15 @@ export class Matterbridge extends EventEmitter {
462
594
  this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
463
595
  throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
464
596
  }
597
+ // Parse command line
465
598
  await this.parseCommandLine();
466
599
  this.initialized = true;
467
600
  }
601
+ /**
602
+ * Parses the command line arguments and performs the corresponding actions.
603
+ * @private
604
+ * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
605
+ */
468
606
  async parseCommandLine() {
469
607
  if (hasParameter('help')) {
470
608
  this.log.info(`\nUsage: matterbridge [options]\n
@@ -576,6 +714,7 @@ export class Matterbridge extends EventEmitter {
576
714
  this.shutdown = true;
577
715
  return;
578
716
  }
717
+ // Start the matter storage and create the matterbridge context
579
718
  try {
580
719
  await this.startMatterStorage();
581
720
  }
@@ -583,12 +722,14 @@ export class Matterbridge extends EventEmitter {
583
722
  this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
584
723
  throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
585
724
  }
725
+ // Clear the matterbridge context if the reset parameter is set
586
726
  if (hasParameter('reset') && getParameter('reset') === undefined) {
587
727
  this.initialized = true;
588
728
  await this.shutdownProcessAndReset();
589
729
  this.shutdown = true;
590
730
  return;
591
731
  }
732
+ // Clear matterbridge plugin context if the reset parameter is set
592
733
  if (hasParameter('reset') && getParameter('reset') !== undefined) {
593
734
  this.log.debug(`Reset plugin ${getParameter('reset')}`);
594
735
  const plugin = this.plugins.get(getParameter('reset'));
@@ -613,30 +754,37 @@ export class Matterbridge extends EventEmitter {
613
754
  this.shutdown = true;
614
755
  return;
615
756
  }
757
+ // Initialize frontend
616
758
  if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
617
759
  await this.frontend.start(getIntParameter('frontend'));
760
+ // Check in 30 seconds the latest versions
618
761
  this.checkUpdateTimeout = setTimeout(async () => {
619
762
  const { checkUpdates } = await import('./update.js');
620
763
  checkUpdates(this);
621
764
  }, 30 * 1000).unref();
765
+ // Check each 24 hours the latest versions
622
766
  this.checkUpdateInterval = setInterval(async () => {
623
767
  const { checkUpdates } = await import('./update.js');
624
768
  checkUpdates(this);
625
769
  }, 24 * 60 * 60 * 1000).unref();
770
+ // Start the matterbridge in mode test
626
771
  if (hasParameter('test')) {
627
772
  this.bridgeMode = 'bridge';
628
773
  MatterbridgeEndpoint.bridgeMode = 'bridge';
629
774
  return;
630
775
  }
776
+ // Start the matterbridge in mode controller
631
777
  if (hasParameter('controller')) {
632
778
  this.bridgeMode = 'controller';
633
779
  await this.startController();
634
780
  return;
635
781
  }
782
+ // Check if the bridge mode is set and start matterbridge in bridge mode if not set
636
783
  if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
637
784
  this.log.info('Setting default matterbridge start mode to bridge');
638
785
  await this.nodeContext?.set('bridgeMode', 'bridge');
639
786
  }
787
+ // Start matterbridge in bridge mode
640
788
  if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
641
789
  this.bridgeMode = 'bridge';
642
790
  MatterbridgeEndpoint.bridgeMode = 'bridge';
@@ -644,6 +792,7 @@ export class Matterbridge extends EventEmitter {
644
792
  await this.startBridge();
645
793
  return;
646
794
  }
795
+ // Start matterbridge in childbridge mode
647
796
  if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
648
797
  this.bridgeMode = 'childbridge';
649
798
  MatterbridgeEndpoint.bridgeMode = 'childbridge';
@@ -652,10 +801,20 @@ export class Matterbridge extends EventEmitter {
652
801
  return;
653
802
  }
654
803
  }
804
+ /**
805
+ * Asynchronously loads and starts the registered plugins.
806
+ *
807
+ * This method is responsible for initializing and staarting all enabled plugins.
808
+ * It ensures that each plugin is properly loaded and started before the bridge starts.
809
+ *
810
+ * @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
811
+ */
655
812
  async startPlugins() {
813
+ // Check, load and start the plugins
656
814
  for (const plugin of this.plugins) {
657
815
  plugin.configJson = await this.plugins.loadConfig(plugin);
658
816
  plugin.schemaJson = await this.plugins.loadSchema(plugin);
817
+ // Check if the plugin is available
659
818
  if (!(await this.plugins.resolve(plugin.path))) {
660
819
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
661
820
  plugin.enabled = false;
@@ -675,20 +834,26 @@ export class Matterbridge extends EventEmitter {
675
834
  plugin.addedDevices = undefined;
676
835
  plugin.qrPairingCode = undefined;
677
836
  plugin.manualPairingCode = undefined;
678
- this.plugins.load(plugin, true, 'Matterbridge is starting');
837
+ this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
679
838
  }
680
839
  this.frontend.wssSendRefreshRequired('plugins');
681
840
  }
841
+ /**
842
+ * Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
843
+ * When either of these signals are received, the cleanup method is called with an appropriate message.
844
+ */
682
845
  registerProcessHandlers() {
683
846
  this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
684
847
  process.removeAllListeners('uncaughtException');
685
848
  process.removeAllListeners('unhandledRejection');
686
849
  this.exceptionHandler = async (error) => {
687
850
  this.log.error('Unhandled Exception detected at:', error.stack || error, rs);
851
+ // await this.cleanup('Unhandled Exception detected, cleaning up...');
688
852
  };
689
853
  process.on('uncaughtException', this.exceptionHandler);
690
854
  this.rejectionHandler = async (reason, promise) => {
691
855
  this.log.error('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
856
+ // await this.cleanup('Unhandled Rejection detected, cleaning up...');
692
857
  };
693
858
  process.on('unhandledRejection', this.rejectionHandler);
694
859
  this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
@@ -701,6 +866,9 @@ export class Matterbridge extends EventEmitter {
701
866
  };
702
867
  process.on('SIGTERM', this.sigtermHandler);
703
868
  }
869
+ /**
870
+ * Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
871
+ */
704
872
  deregisterProcesslHandlers() {
705
873
  this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
706
874
  if (this.exceptionHandler)
@@ -717,12 +885,17 @@ export class Matterbridge extends EventEmitter {
717
885
  process.off('SIGTERM', this.sigtermHandler);
718
886
  this.sigtermHandler = undefined;
719
887
  }
888
+ /**
889
+ * Logs the node and system information.
890
+ */
720
891
  async logNodeAndSystemInfo() {
892
+ // IP address information
721
893
  const networkInterfaces = os.networkInterfaces();
722
894
  this.systemInformation.interfaceName = '';
723
895
  this.systemInformation.ipv4Address = '';
724
896
  this.systemInformation.ipv6Address = '';
725
897
  for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
898
+ // this.log.debug(`Checking interface: '${interfaceName}' for '${this.mdnsInterface}'`);
726
899
  if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
727
900
  continue;
728
901
  if (!interfaceDetails) {
@@ -748,19 +921,22 @@ export class Matterbridge extends EventEmitter {
748
921
  break;
749
922
  }
750
923
  }
924
+ // Node information
751
925
  this.systemInformation.nodeVersion = process.versions.node;
752
926
  const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
753
927
  const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
754
928
  const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
929
+ // Host system information
755
930
  this.systemInformation.hostname = os.hostname();
756
931
  this.systemInformation.user = os.userInfo().username;
757
- this.systemInformation.osType = os.type();
758
- this.systemInformation.osRelease = os.release();
759
- this.systemInformation.osPlatform = os.platform();
760
- this.systemInformation.osArch = os.arch();
761
- this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
762
- this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
763
- this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours';
932
+ this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
933
+ this.systemInformation.osRelease = os.release(); // Kernel version
934
+ this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
935
+ this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
936
+ this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
937
+ this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
938
+ this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
939
+ // Log the system information
764
940
  this.log.debug('Host System Information:');
765
941
  this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
766
942
  this.log.debug(`- User: ${this.systemInformation.user}`);
@@ -776,16 +952,20 @@ export class Matterbridge extends EventEmitter {
776
952
  this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
777
953
  this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
778
954
  this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
955
+ // Home directory
779
956
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
780
957
  this.matterbridgeInformation.homeDirectory = this.homeDirectory;
781
958
  this.log.debug(`Home Directory: ${this.homeDirectory}`);
959
+ // Package root directory
782
960
  const { fileURLToPath } = await import('node:url');
783
961
  const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
784
962
  this.rootDirectory = path.resolve(currentFileDirectory, '../');
785
963
  this.matterbridgeInformation.rootDirectory = this.rootDirectory;
786
964
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
965
+ // Global node_modules directory
787
966
  if (this.nodeContext)
788
967
  this.globalModulesDirectory = this.matterbridgeInformation.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
968
+ // First run of Matterbridge so the node storage is empty
789
969
  if (this.globalModulesDirectory === '') {
790
970
  try {
791
971
  this.execRunningCount++;
@@ -801,6 +981,20 @@ export class Matterbridge extends EventEmitter {
801
981
  }
802
982
  else
803
983
  this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
984
+ /* removed cause is too expensive for the shelly board and not really needed. Why should it change the globalModulesDirectory?
985
+ else {
986
+ this.getGlobalNodeModules()
987
+ .then(async (globalModulesDirectory) => {
988
+ this.globalModulesDirectory = globalModulesDirectory;
989
+ this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
990
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
991
+ await this.nodeContext?.set<string>('globalModulesDirectory', this.globalModulesDirectory);
992
+ })
993
+ .catch((error) => {
994
+ this.log.error(`Error getting global node_modules directory: ${error}`);
995
+ });
996
+ }*/
997
+ // Create the data directory .matterbridge in the home directory
804
998
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
805
999
  this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
806
1000
  try {
@@ -824,6 +1018,7 @@ export class Matterbridge extends EventEmitter {
824
1018
  }
825
1019
  }
826
1020
  this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
1021
+ // Create the plugin directory Matterbridge in the home directory
827
1022
  this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
828
1023
  this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
829
1024
  try {
@@ -847,6 +1042,7 @@ export class Matterbridge extends EventEmitter {
847
1042
  }
848
1043
  }
849
1044
  this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
1045
+ // Create the matter cert directory in the home directory
850
1046
  this.matterbridgeCertDirectory = path.join(this.homeDirectory, '.mattercert');
851
1047
  this.matterbridgeInformation.matterbridgeCertDirectory = this.matterbridgeCertDirectory;
852
1048
  try {
@@ -870,50 +1066,68 @@ export class Matterbridge extends EventEmitter {
870
1066
  }
871
1067
  }
872
1068
  this.log.debug(`Matterbridge Matter Cert Directory: ${this.matterbridgeCertDirectory}`);
1069
+ // Matterbridge version
873
1070
  const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
874
1071
  this.matterbridgeVersion = this.matterbridgeLatestVersion = packageJson.version;
875
1072
  this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeVersion;
876
1073
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
1074
+ // Matterbridge latest version
877
1075
  if (this.nodeContext)
878
1076
  this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
879
1077
  this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
1078
+ // this.getMatterbridgeLatestVersion();
1079
+ // Current working directory
880
1080
  const currentDir = process.cwd();
881
1081
  this.log.debug(`Current Working Directory: ${currentDir}`);
1082
+ // Command line arguments (excluding 'node' and the script name)
882
1083
  const cmdArgs = process.argv.slice(2).join(' ');
883
1084
  this.log.debug(`Command Line Arguments: ${cmdArgs}`);
884
1085
  }
1086
+ /**
1087
+ * Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
1088
+ *
1089
+ * @returns {Function} The MatterLogger function.
1090
+ */
885
1091
  createMatterLogger() {
886
- const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4, logLevel: "debug" });
1092
+ const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
887
1093
  return (_level, formattedLog) => {
888
1094
  const logger = formattedLog.slice(44, 44 + 20).trim();
889
1095
  const message = formattedLog.slice(65);
890
1096
  matterLogger.logName = logger;
891
1097
  switch (_level) {
892
1098
  case MatterLogLevel.DEBUG:
893
- matterLogger.log("debug", message);
1099
+ matterLogger.log("debug" /* LogLevel.DEBUG */, message);
894
1100
  break;
895
1101
  case MatterLogLevel.INFO:
896
- matterLogger.log("info", message);
1102
+ matterLogger.log("info" /* LogLevel.INFO */, message);
897
1103
  break;
898
1104
  case MatterLogLevel.NOTICE:
899
- matterLogger.log("notice", message);
1105
+ matterLogger.log("notice" /* LogLevel.NOTICE */, message);
900
1106
  break;
901
1107
  case MatterLogLevel.WARN:
902
- matterLogger.log("warn", message);
1108
+ matterLogger.log("warn" /* LogLevel.WARN */, message);
903
1109
  break;
904
1110
  case MatterLogLevel.ERROR:
905
- matterLogger.log("error", message);
1111
+ matterLogger.log("error" /* LogLevel.ERROR */, message);
906
1112
  break;
907
1113
  case MatterLogLevel.FATAL:
908
- matterLogger.log("fatal", message);
1114
+ matterLogger.log("fatal" /* LogLevel.FATAL */, message);
909
1115
  break;
910
1116
  default:
911
- matterLogger.log("debug", message);
1117
+ matterLogger.log("debug" /* LogLevel.DEBUG */, message);
912
1118
  break;
913
1119
  }
914
1120
  };
915
1121
  }
1122
+ /**
1123
+ * Creates a Matter File Logger.
1124
+ *
1125
+ * @param {string} filePath - The path to the log file.
1126
+ * @param {boolean} [unlink=false] - Whether to unlink the log file before creating a new one.
1127
+ * @returns {Function} - A function that logs formatted messages to the log file.
1128
+ */
916
1129
  async createMatterFileLogger(filePath, unlink = false) {
1130
+ // 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
917
1131
  let fileSize = 0;
918
1132
  if (unlink) {
919
1133
  try {
@@ -962,12 +1176,21 @@ export class Matterbridge extends EventEmitter {
962
1176
  }
963
1177
  };
964
1178
  }
1179
+ /**
1180
+ * Restarts the process by exiting the current instance and loading a new instance.
1181
+ */
965
1182
  async restartProcess() {
966
1183
  await this.cleanup('restarting...', true);
967
1184
  }
1185
+ /**
1186
+ * Shut down the process by exiting the current process.
1187
+ */
968
1188
  async shutdownProcess() {
969
1189
  await this.cleanup('shutting down...', false);
970
1190
  }
1191
+ /**
1192
+ * Update matterbridge and and shut down the process.
1193
+ */
971
1194
  async updateProcess() {
972
1195
  this.log.info('Updating matterbridge...');
973
1196
  try {
@@ -980,51 +1203,72 @@ export class Matterbridge extends EventEmitter {
980
1203
  this.frontend.wssSendRestartRequired();
981
1204
  await this.cleanup('updating...', false);
982
1205
  }
1206
+ /**
1207
+ * Unregister all devices and shut down the process.
1208
+ */
983
1209
  async unregisterAndShutdownProcess() {
984
1210
  this.log.info('Unregistering all devices and shutting down...');
985
1211
  for (const plugin of this.plugins) {
986
1212
  await this.removeAllBridgedEndpoints(plugin.name, 250);
987
1213
  }
988
1214
  this.log.debug('Waiting for the MessageExchange to finish...');
989
- await new Promise((resolve) => setTimeout(resolve, 1000));
1215
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
990
1216
  this.log.debug('Cleaning up and shutting down...');
991
1217
  await this.cleanup('unregistered all devices and shutting down...', false);
992
1218
  }
1219
+ /**
1220
+ * Reset commissioning and shut down the process.
1221
+ */
993
1222
  async shutdownProcessAndReset() {
994
1223
  await this.cleanup('shutting down with reset...', false);
995
1224
  }
1225
+ /**
1226
+ * Factory reset and shut down the process.
1227
+ */
996
1228
  async shutdownProcessAndFactoryReset() {
997
1229
  await this.cleanup('shutting down with factory reset...', false);
998
1230
  }
1231
+ /**
1232
+ * Cleans up the Matterbridge instance.
1233
+ * @param message - The cleanup message.
1234
+ * @param restart - Indicates whether to restart the instance after cleanup. Default is `false`.
1235
+ * @returns A promise that resolves when the cleanup is completed.
1236
+ */
999
1237
  async cleanup(message, restart = false) {
1000
1238
  if (this.initialized && !this.hasCleanupStarted) {
1001
1239
  this.hasCleanupStarted = true;
1002
1240
  this.log.info(message);
1241
+ // Clear the start matter interval
1003
1242
  if (this.startMatterInterval) {
1004
1243
  clearInterval(this.startMatterInterval);
1005
1244
  this.startMatterInterval = undefined;
1006
1245
  this.log.debug('Start matter interval cleared');
1007
1246
  }
1247
+ // Clear the check update timeout
1008
1248
  if (this.checkUpdateTimeout) {
1009
1249
  clearInterval(this.checkUpdateTimeout);
1010
1250
  this.checkUpdateTimeout = undefined;
1011
1251
  this.log.debug('Check update timeout cleared');
1012
1252
  }
1253
+ // Clear the check update interval
1013
1254
  if (this.checkUpdateInterval) {
1014
1255
  clearInterval(this.checkUpdateInterval);
1015
1256
  this.checkUpdateInterval = undefined;
1016
1257
  this.log.debug('Check update interval cleared');
1017
1258
  }
1259
+ // Clear the configure timeout
1018
1260
  if (this.configureTimeout) {
1019
1261
  clearTimeout(this.configureTimeout);
1020
1262
  this.configureTimeout = undefined;
1021
1263
  this.log.debug('Matterbridge configure timeout cleared');
1022
1264
  }
1265
+ // Clear the reachability timeout
1023
1266
  if (this.reachabilityTimeout) {
1024
1267
  clearTimeout(this.reachabilityTimeout);
1025
1268
  this.reachabilityTimeout = undefined;
1026
1269
  this.log.debug('Matterbridge reachability timeout cleared');
1027
1270
  }
1271
+ // Calling the shutdown method of each plugin and clear the plugins reachability timeout
1028
1272
  for (const plugin of this.plugins) {
1029
1273
  if (!plugin.enabled || plugin.error)
1030
1274
  continue;
@@ -1035,9 +1279,10 @@ export class Matterbridge extends EventEmitter {
1035
1279
  this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
1036
1280
  }
1037
1281
  }
1282
+ // Stopping matter server nodes
1038
1283
  this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
1039
1284
  this.log.debug('Waiting for the MessageExchange to finish...');
1040
- await new Promise((resolve) => setTimeout(resolve, 1000));
1285
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second for MessageExchange to finish
1041
1286
  if (this.bridgeMode === 'bridge') {
1042
1287
  if (this.serverNode) {
1043
1288
  await this.stopServerNode(this.serverNode);
@@ -1053,6 +1298,7 @@ export class Matterbridge extends EventEmitter {
1053
1298
  }
1054
1299
  }
1055
1300
  this.log.notice('Stopped matter server nodes');
1301
+ // Matter commisioning reset
1056
1302
  if (message === 'shutting down with reset...') {
1057
1303
  this.log.info('Resetting Matterbridge commissioning information...');
1058
1304
  await this.matterStorageManager?.createContext('events')?.clearAll();
@@ -1062,17 +1308,37 @@ export class Matterbridge extends EventEmitter {
1062
1308
  await this.matterbridgeContext?.clearAll();
1063
1309
  this.log.info('Matter storage reset done! Remove the bridge from the controller.');
1064
1310
  }
1311
+ // Stop matter storage
1065
1312
  await this.stopMatterStorage();
1313
+ // Stop the frontend
1066
1314
  await this.frontend.stop();
1315
+ // Remove the matterfilelogger
1067
1316
  try {
1068
1317
  Logger.removeLogger('matterfilelogger');
1318
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1069
1319
  }
1070
1320
  catch (error) {
1321
+ // this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1071
1322
  }
1323
+ // Serialize registeredDevices
1072
1324
  if (this.nodeStorage && this.nodeContext) {
1325
+ /*
1326
+ TODO: Implement serialization of registered devices in edge mode
1327
+ this.log.info('Saving registered devices...');
1328
+ const serializedRegisteredDevices: SerializedMatterbridgeEndpoint[] = [];
1329
+ this.devices.forEach(async (device) => {
1330
+ const serializedMatterbridgeDevice = MatterbridgeEndpoint.serialize(device);
1331
+ // this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
1332
+ if (serializedMatterbridgeDevice) serializedRegisteredDevices.push(serializedMatterbridgeDevice);
1333
+ });
1334
+ await this.nodeContext.set<SerializedMatterbridgeEndpoint[]>('devices', serializedRegisteredDevices);
1335
+ this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
1336
+ */
1337
+ // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
1073
1338
  this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
1074
1339
  await this.nodeContext.close();
1075
1340
  this.nodeContext = undefined;
1341
+ // Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
1076
1342
  for (const plugin of this.plugins) {
1077
1343
  if (plugin.nodeContext) {
1078
1344
  this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
@@ -1089,8 +1355,10 @@ export class Matterbridge extends EventEmitter {
1089
1355
  }
1090
1356
  this.plugins.clear();
1091
1357
  this.devices.clear();
1358
+ // Factory reset
1092
1359
  if (message === 'shutting down with factory reset...') {
1093
1360
  try {
1361
+ // Delete old matter storage file and backup
1094
1362
  const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
1095
1363
  this.log.info(`Unlinking old matter storage file: ${file}`);
1096
1364
  await fs.unlink(file);
@@ -1104,6 +1372,7 @@ export class Matterbridge extends EventEmitter {
1104
1372
  }
1105
1373
  }
1106
1374
  try {
1375
+ // Delete matter node storage directory with its subdirectories and backup
1107
1376
  const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
1108
1377
  this.log.info(`Removing matter node storage directory: ${dir}`);
1109
1378
  await fs.rm(dir, { recursive: true });
@@ -1117,6 +1386,7 @@ export class Matterbridge extends EventEmitter {
1117
1386
  }
1118
1387
  }
1119
1388
  try {
1389
+ // Delete node storage directory with its subdirectories and backup
1120
1390
  const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
1121
1391
  this.log.info(`Removing storage directory: ${dir}`);
1122
1392
  await fs.rm(dir, { recursive: true });
@@ -1131,12 +1401,13 @@ export class Matterbridge extends EventEmitter {
1131
1401
  }
1132
1402
  this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
1133
1403
  }
1404
+ // Deregisters the process handlers
1134
1405
  this.deregisterProcesslHandlers();
1135
1406
  if (restart) {
1136
1407
  if (message === 'updating...') {
1137
1408
  this.log.info('Cleanup completed. Updating...');
1138
1409
  Matterbridge.instance = undefined;
1139
- this.emit('update');
1410
+ this.emit('update'); // Restart the process but the update has been done before. TODO move all updates to the cli
1140
1411
  }
1141
1412
  else if (message === 'restarting...') {
1142
1413
  this.log.info('Cleanup completed. Restarting...');
@@ -1156,6 +1427,14 @@ export class Matterbridge extends EventEmitter {
1156
1427
  this.log.debug('Cleanup already started...');
1157
1428
  }
1158
1429
  }
1430
+ /**
1431
+ * Creates and configures the server node for an accessory plugin for a given device.
1432
+ *
1433
+ * @param {RegisteredPlugin} plugin - The plugin to configure.
1434
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
1435
+ * @param {boolean} [start=false] - Whether to start the server node after adding the device.
1436
+ * @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
1437
+ */
1159
1438
  async createAccessoryPlugin(plugin, device, start = false) {
1160
1439
  if (!plugin.locked && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1161
1440
  plugin.locked = true;
@@ -1168,6 +1447,13 @@ export class Matterbridge extends EventEmitter {
1168
1447
  await this.startServerNode(plugin.serverNode);
1169
1448
  }
1170
1449
  }
1450
+ /**
1451
+ * Creates and configures the server node for a dynamic plugin.
1452
+ *
1453
+ * @param {RegisteredPlugin} plugin - The plugin to configure.
1454
+ * @param {boolean} [start=false] - Whether to start the server node after adding the aggregator node.
1455
+ * @returns {Promise<void>} A promise that resolves when the server node for the dynamic plugin is created and configured.
1456
+ */
1171
1457
  async createDynamicPlugin(plugin, start = false) {
1172
1458
  if (!plugin.locked) {
1173
1459
  plugin.locked = true;
@@ -1179,7 +1465,13 @@ export class Matterbridge extends EventEmitter {
1179
1465
  await this.startServerNode(plugin.serverNode);
1180
1466
  }
1181
1467
  }
1468
+ /**
1469
+ * Starts the Matterbridge in bridge mode.
1470
+ * @private
1471
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1472
+ */
1182
1473
  async startBridge() {
1474
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1183
1475
  if (!this.matterStorageManager)
1184
1476
  throw new Error('No storage manager initialized');
1185
1477
  if (!this.matterbridgeContext)
@@ -1217,7 +1509,9 @@ export class Matterbridge extends EventEmitter {
1217
1509
  clearInterval(this.startMatterInterval);
1218
1510
  this.startMatterInterval = undefined;
1219
1511
  this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1512
+ // Start the Matter server node
1220
1513
  this.startServerNode(this.serverNode);
1514
+ // Configure the plugins
1221
1515
  this.configureTimeout = setTimeout(async () => {
1222
1516
  for (const plugin of this.plugins) {
1223
1517
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
@@ -1235,6 +1529,7 @@ export class Matterbridge extends EventEmitter {
1235
1529
  }
1236
1530
  this.frontend.wssSendRefreshRequired('plugins');
1237
1531
  }, 30 * 1000);
1532
+ // Setting reachability to true
1238
1533
  this.reachabilityTimeout = setTimeout(() => {
1239
1534
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1240
1535
  if (this.aggregatorNode)
@@ -1243,6 +1538,11 @@ export class Matterbridge extends EventEmitter {
1243
1538
  }, 60 * 1000);
1244
1539
  }, 1000);
1245
1540
  }
1541
+ /**
1542
+ * Starts the Matterbridge in childbridge mode.
1543
+ * @private
1544
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1545
+ */
1246
1546
  async startChildbridge() {
1247
1547
  if (!this.matterStorageManager)
1248
1548
  throw new Error('No storage manager initialized');
@@ -1287,6 +1587,7 @@ export class Matterbridge extends EventEmitter {
1287
1587
  clearInterval(this.startMatterInterval);
1288
1588
  this.startMatterInterval = undefined;
1289
1589
  this.log.debug('Cleared startMatterInterval interval in childbridge mode');
1590
+ // Configure the plugins
1290
1591
  this.configureTimeout = setTimeout(async () => {
1291
1592
  for (const plugin of this.plugins) {
1292
1593
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
@@ -1323,7 +1624,9 @@ export class Matterbridge extends EventEmitter {
1323
1624
  this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1324
1625
  continue;
1325
1626
  }
1627
+ // Start the Matter server node
1326
1628
  this.startServerNode(plugin.serverNode);
1629
+ // Setting reachability to true
1327
1630
  plugin.reachabilityTimeout = setTimeout(() => {
1328
1631
  this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db} type ${plugin.type} server node ${plugin.serverNode !== undefined} aggragator node ${plugin.aggregatorNode !== undefined} device ${plugin.device !== undefined}`);
1329
1632
  if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
@@ -1333,23 +1636,245 @@ export class Matterbridge extends EventEmitter {
1333
1636
  }
1334
1637
  }, 1000);
1335
1638
  }
1639
+ /**
1640
+ * Starts the Matterbridge controller.
1641
+ * @private
1642
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1643
+ */
1336
1644
  async startController() {
1645
+ /*
1646
+ if (!this.storageManager) {
1647
+ this.log.error('No storage manager initialized');
1648
+ await this.cleanup('No storage manager initialized');
1649
+ return;
1650
+ }
1651
+ this.log.info('Creating context: mattercontrollerContext');
1652
+ this.mattercontrollerContext = this.storageManager.createContext('mattercontrollerContext');
1653
+ if (!this.mattercontrollerContext) {
1654
+ this.log.error('No storage context mattercontrollerContext initialized');
1655
+ await this.cleanup('No storage context mattercontrollerContext initialized');
1656
+ return;
1657
+ }
1658
+
1659
+ this.log.debug('Starting matterbridge in mode', this.bridgeMode);
1660
+ this.matterServer = await this.createMatterServer(this.storageManager);
1661
+ this.log.info('Creating matter commissioning controller');
1662
+ this.commissioningController = new CommissioningController({
1663
+ autoConnect: false,
1664
+ });
1665
+ this.log.info('Adding matter commissioning controller to matter server');
1666
+ await this.matterServer.addCommissioningController(this.commissioningController);
1667
+
1668
+ this.log.info('Starting matter server');
1669
+ await this.matterServer.start();
1670
+ this.log.info('Matter server started');
1671
+
1672
+ if (hasParameter('pairingcode')) {
1673
+ this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
1674
+ const pairingCode = getParameter('pairingcode');
1675
+ const ip = this.mattercontrollerContext.has('ip') ? this.mattercontrollerContext.get<string>('ip') : undefined;
1676
+ const port = this.mattercontrollerContext.has('port') ? this.mattercontrollerContext.get<number>('port') : undefined;
1677
+
1678
+ let longDiscriminator, setupPin, shortDiscriminator;
1679
+ if (pairingCode !== undefined) {
1680
+ const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
1681
+ shortDiscriminator = pairingCodeCodec.shortDiscriminator;
1682
+ longDiscriminator = undefined;
1683
+ setupPin = pairingCodeCodec.passcode;
1684
+ this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
1685
+ } else {
1686
+ longDiscriminator = await this.mattercontrollerContext.get('longDiscriminator', 3840);
1687
+ if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
1688
+ setupPin = this.mattercontrollerContext.get('pin', 20202021);
1689
+ }
1690
+ if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
1691
+ throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
1692
+ }
1693
+
1694
+ const commissioningOptions: ControllerCommissioningFlowOptions = {
1695
+ regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
1696
+ regulatoryCountryCode: 'XX',
1697
+ };
1698
+ const options = {
1699
+ commissioning: commissioningOptions,
1700
+ discovery: {
1701
+ knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
1702
+ identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
1703
+ },
1704
+ passcode: setupPin,
1705
+ } as NodeCommissioningOptions;
1706
+ this.log.info('Commissioning with options:', options);
1707
+ const nodeId = await this.commissioningController.commissionNode(options);
1708
+ this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
1709
+ this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
1710
+ } // (hasParameter('pairingcode'))
1711
+
1712
+ if (hasParameter('unpairall')) {
1713
+ this.log.info('***Commissioning controller unpairing all nodes...');
1714
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1715
+ for (const nodeId of nodeIds) {
1716
+ this.log.info('***Commissioning controller unpairing node:', nodeId);
1717
+ await this.commissioningController.removeNode(nodeId);
1718
+ }
1719
+ return;
1720
+ }
1721
+
1722
+ if (hasParameter('discover')) {
1723
+ // const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
1724
+ // console.log(discover);
1725
+ }
1726
+
1727
+ if (!this.commissioningController.isCommissioned()) {
1728
+ this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
1729
+ return;
1730
+ }
1731
+
1732
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1733
+ this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
1734
+ for (const nodeId of nodeIds) {
1735
+ this.log.info(`***Connecting to commissioned node: ${nodeId}`);
1736
+
1737
+ const node = await this.commissioningController.connectNode(nodeId, {
1738
+ autoSubscribe: false,
1739
+ attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
1740
+ this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
1741
+ eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
1742
+ this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
1743
+ stateInformationCallback: (peerNodeId, info) => {
1744
+ switch (info) {
1745
+ case NodeStateInformation.Connected:
1746
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
1747
+ break;
1748
+ case NodeStateInformation.Disconnected:
1749
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
1750
+ break;
1751
+ case NodeStateInformation.Reconnecting:
1752
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
1753
+ break;
1754
+ case NodeStateInformation.WaitingForDeviceDiscovery:
1755
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
1756
+ break;
1757
+ case NodeStateInformation.StructureChanged:
1758
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
1759
+ break;
1760
+ case NodeStateInformation.Decommissioned:
1761
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
1762
+ break;
1763
+ default:
1764
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
1765
+ break;
1766
+ }
1767
+ },
1768
+ });
1769
+
1770
+ node.logStructure();
1771
+
1772
+ // Get the interaction client
1773
+ this.log.info('Getting the interaction client');
1774
+ const interactionClient = await node.getInteractionClient();
1775
+ let cluster;
1776
+ let attributes;
1777
+
1778
+ // Log BasicInformationCluster
1779
+ cluster = BasicInformationCluster;
1780
+ attributes = await interactionClient.getMultipleAttributes({
1781
+ attributes: [{ clusterId: cluster.id }],
1782
+ });
1783
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1784
+ attributes.forEach((attribute) => {
1785
+ this.log.info(
1786
+ `- 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}`,
1787
+ );
1788
+ });
1789
+
1790
+ // Log PowerSourceCluster
1791
+ cluster = PowerSourceCluster;
1792
+ attributes = await interactionClient.getMultipleAttributes({
1793
+ attributes: [{ clusterId: cluster.id }],
1794
+ });
1795
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1796
+ attributes.forEach((attribute) => {
1797
+ this.log.info(
1798
+ `- 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}`,
1799
+ );
1800
+ });
1801
+
1802
+ // Log ThreadNetworkDiagnostics
1803
+ cluster = ThreadNetworkDiagnosticsCluster;
1804
+ attributes = await interactionClient.getMultipleAttributes({
1805
+ attributes: [{ clusterId: cluster.id }],
1806
+ });
1807
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1808
+ attributes.forEach((attribute) => {
1809
+ this.log.info(
1810
+ `- 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}`,
1811
+ );
1812
+ });
1813
+
1814
+ // Log SwitchCluster
1815
+ cluster = SwitchCluster;
1816
+ attributes = await interactionClient.getMultipleAttributes({
1817
+ attributes: [{ clusterId: cluster.id }],
1818
+ });
1819
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1820
+ attributes.forEach((attribute) => {
1821
+ this.log.info(
1822
+ `- 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}`,
1823
+ );
1824
+ });
1825
+
1826
+ this.log.info('Subscribing to all attributes and events');
1827
+ await node.subscribeAllAttributesAndEvents({
1828
+ ignoreInitialTriggers: false,
1829
+ attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
1830
+ this.log.info(
1831
+ `***${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}`,
1832
+ ),
1833
+ eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
1834
+ this.log.info(
1835
+ `***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
1836
+ );
1837
+ },
1838
+ });
1839
+ this.log.info('Subscribed to all attributes and events');
1840
+ }
1841
+ */
1337
1842
  }
1843
+ /** ***********************************************************************************************************************************/
1844
+ /** Matter.js methods */
1845
+ /** ***********************************************************************************************************************************/
1846
+ /**
1847
+ * Starts the matter storage process with name Matterbridge.
1848
+ * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1849
+ */
1338
1850
  async startMatterStorage() {
1851
+ // Setup Matter storage
1339
1852
  this.log.info(`Starting matter node storage...`);
1340
1853
  this.matterStorageService = this.environment.get(StorageService);
1341
1854
  this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
1342
1855
  this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
1343
1856
  this.log.info('Matter node storage manager "Matterbridge" created');
1344
- this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, 'Matterbridge aggregator');
1857
+ this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
1345
1858
  this.log.info('Matter node storage started');
1859
+ // Backup matter storage since it is created/opened correctly
1346
1860
  await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
1347
1861
  }
1862
+ /**
1863
+ * Makes a backup copy of the specified matter storage directory.
1864
+ *
1865
+ * @param storageName - The name of the storage directory to be backed up.
1866
+ * @param backupName - The name of the backup directory to be created.
1867
+ * @returns {Promise<void>} A promise that resolves when the has been done.
1868
+ */
1348
1869
  async backupMatterStorage(storageName, backupName) {
1349
1870
  this.log.info('Creating matter node storage backup...');
1350
1871
  await copyDirectory(storageName, backupName);
1351
1872
  this.log.info('Created matter node storage backup');
1352
1873
  }
1874
+ /**
1875
+ * Stops the matter storage.
1876
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
1877
+ */
1353
1878
  async stopMatterStorage() {
1354
1879
  this.log.info('Closing matter node storage...');
1355
1880
  this.matterStorageManager?.close();
@@ -1358,6 +1883,19 @@ export class Matterbridge extends EventEmitter {
1358
1883
  this.matterbridgeContext = undefined;
1359
1884
  this.log.info('Matter node storage closed');
1360
1885
  }
1886
+ /**
1887
+ * Creates a server node storage context.
1888
+ *
1889
+ * @param {string} pluginName - The name of the plugin.
1890
+ * @param {string} deviceName - The name of the device.
1891
+ * @param {DeviceTypeId} deviceType - The device type of the device.
1892
+ * @param {number} vendorId - The vendor ID.
1893
+ * @param {string} vendorName - The vendor name.
1894
+ * @param {number} productId - The product ID.
1895
+ * @param {string} productName - The product name.
1896
+ * @param {string} [serialNumber] - The serial number of the device (optional).
1897
+ * @returns {Promise<StorageContext>} The storage context for the commissioning server.
1898
+ */
1361
1899
  async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
1362
1900
  const { randomBytes } = await import('node:crypto');
1363
1901
  if (!this.matterStorageService)
@@ -1391,6 +1929,15 @@ export class Matterbridge extends EventEmitter {
1391
1929
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1392
1930
  return storageContext;
1393
1931
  }
1932
+ /**
1933
+ * Creates a server node.
1934
+ *
1935
+ * @param {StorageContext} storageContext - The storage context for the server node.
1936
+ * @param {number} [port=5540] - The port number for the server node. Defaults to 5540.
1937
+ * @param {number} [passcode=20242025] - The passcode for the server node. Defaults to 20242025.
1938
+ * @param {number} [discriminator=3850] - The discriminator for the server node. Defaults to 3850.
1939
+ * @returns {Promise<ServerNode<ServerNode.RootEndpoint>>} A promise that resolves to the created server node.
1940
+ */
1394
1941
  async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
1395
1942
  const storeId = await storageContext.get('storeId');
1396
1943
  this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
@@ -1400,21 +1947,33 @@ export class Matterbridge extends EventEmitter {
1400
1947
  this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
1401
1948
  this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1402
1949
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1950
+ /**
1951
+ * Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
1952
+ */
1403
1953
  const serverNode = await ServerNode.create({
1954
+ // Required: Give the Node a unique ID which is used to store the state of this node
1404
1955
  id: storeId,
1956
+ // Provide Network relevant configuration like the port
1957
+ // Optional when operating only one device on a host, Default port is 5540
1405
1958
  network: {
1406
1959
  listeningAddressIpv4: this.ipv4address,
1407
1960
  listeningAddressIpv6: this.ipv6address,
1408
1961
  port,
1409
1962
  },
1963
+ // Provide Commissioning relevant settings
1964
+ // Optional for development/testing purposes
1410
1965
  commissioning: {
1411
1966
  passcode,
1412
1967
  discriminator,
1413
1968
  },
1969
+ // Provide Node announcement settings
1970
+ // Optional: If Ommitted some development defaults are used
1414
1971
  productDescription: {
1415
1972
  name: await storageContext.get('deviceName'),
1416
1973
  deviceType: DeviceTypeId(await storageContext.get('deviceType')),
1417
1974
  },
1975
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
1976
+ // Optional: If Omitted some development defaults are used
1418
1977
  basicInformation: {
1419
1978
  vendorId: VendorId(await storageContext.get('vendorId')),
1420
1979
  vendorName: await storageContext.get('vendorName'),
@@ -1431,12 +1990,13 @@ export class Matterbridge extends EventEmitter {
1431
1990
  },
1432
1991
  });
1433
1992
  const sanitizeFabrics = (fabrics, resetSessions = false) => {
1993
+ // New type of fabric information: Record<FabricIndex, ExposedFabricInformation>
1434
1994
  const sanitizedFabrics = this.sanitizeFabricInformations(Array.from(Object.values(fabrics)));
1435
1995
  this.log.info(`Fabrics: ${debugStringify(sanitizedFabrics)}`);
1436
1996
  if (this.bridgeMode === 'bridge') {
1437
1997
  this.matterbridgeFabricInformations = sanitizedFabrics;
1438
1998
  if (resetSessions)
1439
- this.matterbridgeSessionInformations = undefined;
1999
+ this.matterbridgeSessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
1440
2000
  this.matterbridgePaired = true;
1441
2001
  }
1442
2002
  if (this.bridgeMode === 'childbridge') {
@@ -1444,13 +2004,19 @@ export class Matterbridge extends EventEmitter {
1444
2004
  if (plugin) {
1445
2005
  plugin.fabricInformations = sanitizedFabrics;
1446
2006
  if (resetSessions)
1447
- plugin.sessionInformations = undefined;
2007
+ plugin.sessionInformations = undefined; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
1448
2008
  plugin.paired = true;
1449
2009
  }
1450
2010
  }
1451
2011
  };
2012
+ /**
2013
+ * This event is triggered when the device is initially commissioned successfully.
2014
+ * This means: It is added to the first fabric.
2015
+ */
1452
2016
  serverNode.lifecycle.commissioned.on(() => this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`));
2017
+ /** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
1453
2018
  serverNode.lifecycle.decommissioned.on(() => this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`));
2019
+ /** This event is triggered when the device went online. This means that it is discoverable in the network. */
1454
2020
  serverNode.lifecycle.online.on(async () => {
1455
2021
  this.log.notice(`Server node for ${storeId} is online`);
1456
2022
  if (!serverNode.lifecycle.isCommissioned) {
@@ -1515,6 +2081,7 @@ export class Matterbridge extends EventEmitter {
1515
2081
  this.frontend.wssSendRefreshRequired('settings');
1516
2082
  this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
1517
2083
  });
2084
+ /** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
1518
2085
  serverNode.lifecycle.offline.on(() => {
1519
2086
  this.log.notice(`Server node for ${storeId} is offline`);
1520
2087
  if (this.bridgeMode === 'bridge') {
@@ -1538,6 +2105,10 @@ export class Matterbridge extends EventEmitter {
1538
2105
  this.frontend.wssSendRefreshRequired('settings');
1539
2106
  this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
1540
2107
  });
2108
+ /**
2109
+ * This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
2110
+ * information is needed.
2111
+ */
1541
2112
  serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
1542
2113
  let action = '';
1543
2114
  switch (fabricAction) {
@@ -1571,16 +2142,24 @@ export class Matterbridge extends EventEmitter {
1571
2142
  }
1572
2143
  }
1573
2144
  };
2145
+ /**
2146
+ * This event is triggered when an operative new session was opened by a Controller.
2147
+ * It is not triggered for the initial commissioning process, just afterwards for real connections.
2148
+ */
1574
2149
  serverNode.events.sessions.opened.on((session) => {
1575
2150
  this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
1576
2151
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1577
2152
  this.frontend.wssSendRefreshRequired('sessions');
1578
2153
  });
2154
+ /**
2155
+ * This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
2156
+ */
1579
2157
  serverNode.events.sessions.closed.on((session) => {
1580
2158
  this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
1581
2159
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1582
2160
  this.frontend.wssSendRefreshRequired('sessions');
1583
2161
  });
2162
+ /** This event is triggered when a subscription gets added or removed on an operative session. */
1584
2163
  serverNode.events.sessions.subscriptionsChanged.on((session) => {
1585
2164
  this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
1586
2165
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
@@ -1589,24 +2168,42 @@ export class Matterbridge extends EventEmitter {
1589
2168
  this.log.info(`Created server node for ${storeId}`);
1590
2169
  return serverNode;
1591
2170
  }
2171
+ /**
2172
+ * Starts the specified server node.
2173
+ *
2174
+ * @param {ServerNode} [matterServerNode] - The server node to start.
2175
+ * @returns {Promise<void>} A promise that resolves when the server node has started.
2176
+ */
1592
2177
  async startServerNode(matterServerNode) {
1593
2178
  if (!matterServerNode)
1594
2179
  return;
1595
2180
  this.log.notice(`Starting ${matterServerNode.id} server node`);
1596
2181
  await matterServerNode.start();
1597
2182
  }
2183
+ /**
2184
+ * Stops the specified server node.
2185
+ *
2186
+ * @param {ServerNode} matterServerNode - The server node to stop.
2187
+ * @returns {Promise<void>} A promise that resolves when the server node has stopped.
2188
+ */
1598
2189
  async stopServerNode(matterServerNode) {
1599
2190
  if (!matterServerNode)
1600
2191
  return;
1601
2192
  this.log.notice(`Closing ${matterServerNode.id} server node`);
1602
2193
  try {
1603
- await withTimeout(matterServerNode.close(), 30000);
2194
+ await withTimeout(matterServerNode.close(), 30000); // 30 seconds timeout to allow slow devices to close gracefully
1604
2195
  this.log.info(`Closed ${matterServerNode.id} server node`);
1605
2196
  }
1606
2197
  catch (error) {
1607
2198
  this.log.error(`Failed to close ${matterServerNode.id} server node: ${error instanceof Error ? error.message : error}`);
1608
2199
  }
1609
2200
  }
2201
+ /**
2202
+ * Advertises the specified server node.
2203
+ *
2204
+ * @param {ServerNode} [matterServerNode] - The server node to advertise.
2205
+ * @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.
2206
+ */
1610
2207
  async advertiseServerNode(matterServerNode) {
1611
2208
  if (matterServerNode) {
1612
2209
  await matterServerNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
@@ -1615,23 +2212,44 @@ export class Matterbridge extends EventEmitter {
1615
2212
  return { qrPairingCode, manualPairingCode };
1616
2213
  }
1617
2214
  }
2215
+ /**
2216
+ * Stop advertise the specified server node.
2217
+ *
2218
+ * @param {ServerNode} [matterServerNode] - The server node to advertise.
2219
+ * @returns {Promise<void>} A promise that resolves when the server node has stopped advertising.
2220
+ */
1618
2221
  async stopAdvertiseServerNode(matterServerNode) {
1619
2222
  if (matterServerNode && matterServerNode.lifecycle.isOnline) {
1620
2223
  await matterServerNode.env.get(DeviceCommissioner)?.endCommissioning();
1621
2224
  this.log.notice(`Stopped advertising for ${matterServerNode.id}`);
1622
2225
  }
1623
2226
  }
2227
+ /**
2228
+ * Creates an aggregator node with the specified storage context.
2229
+ *
2230
+ * @param {StorageContext} storageContext - The storage context for the aggregator node.
2231
+ * @returns {Promise<EndpointNode<AggregatorEndpoint>>} A promise that resolves to the created aggregator node.
2232
+ */
1624
2233
  async createAggregatorNode(storageContext) {
1625
2234
  this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator `);
1626
2235
  const aggregatorNode = new EndpointNode(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
1627
2236
  return aggregatorNode;
1628
2237
  }
2238
+ /**
2239
+ * Adds a MatterbridgeEndpoint to the specified plugin.
2240
+ *
2241
+ * @param {string} pluginName - The name of the plugin.
2242
+ * @param {MatterbridgeEndpoint} device - The device to add as a bridged endpoint.
2243
+ * @returns {Promise<void>} A promise that resolves when the bridged endpoint has been added.
2244
+ */
1629
2245
  async addBridgedEndpoint(pluginName, device) {
2246
+ // Check if the plugin is registered
1630
2247
  const plugin = this.plugins.get(pluginName);
1631
2248
  if (!plugin) {
1632
2249
  this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
1633
2250
  return;
1634
2251
  }
2252
+ // Register and add the device to the matterbridge aggregator node
1635
2253
  if (this.bridgeMode === 'bridge') {
1636
2254
  this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
1637
2255
  if (!this.aggregatorNode)
@@ -1654,17 +2272,28 @@ export class Matterbridge extends EventEmitter {
1654
2272
  plugin.registeredDevices++;
1655
2273
  if (plugin.addedDevices !== undefined)
1656
2274
  plugin.addedDevices++;
2275
+ // Add the device to the DeviceManager
1657
2276
  this.devices.set(device);
2277
+ // Subscribe to the reachable$Changed event
1658
2278
  await this.subscribeAttributeChanged(plugin, device);
1659
2279
  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}`);
1660
2280
  }
2281
+ /**
2282
+ * Removes a MatterbridgeEndpoint from the specified plugin.
2283
+ *
2284
+ * @param {string} pluginName - The name of the plugin.
2285
+ * @param {MatterbridgeEndpoint} device - The device to remove as a bridged endpoint.
2286
+ * @returns {Promise<void>} A promise that resolves when the bridged endpoint has been removed.
2287
+ */
1661
2288
  async removeBridgedEndpoint(pluginName, device) {
1662
2289
  this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
2290
+ // Check if the plugin is registered
1663
2291
  const plugin = this.plugins.get(pluginName);
1664
2292
  if (!plugin) {
1665
2293
  this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1666
2294
  return;
1667
2295
  }
2296
+ // Register and add the device to the matterbridge aggregator node
1668
2297
  if (this.bridgeMode === 'bridge') {
1669
2298
  if (!this.aggregatorNode) {
1670
2299
  this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
@@ -1679,6 +2308,7 @@ export class Matterbridge extends EventEmitter {
1679
2308
  }
1680
2309
  else if (this.bridgeMode === 'childbridge') {
1681
2310
  if (plugin.type === 'AccessoryPlatform') {
2311
+ // Nothing to do here since the server node has no aggregator node but only the device itself
1682
2312
  }
1683
2313
  else if (plugin.type === 'DynamicPlatform') {
1684
2314
  if (!plugin.aggregatorNode) {
@@ -1693,8 +2323,16 @@ export class Matterbridge extends EventEmitter {
1693
2323
  if (plugin.addedDevices !== undefined)
1694
2324
  plugin.addedDevices--;
1695
2325
  }
2326
+ // Remove the device from the DeviceManager
1696
2327
  this.devices.remove(device);
1697
2328
  }
2329
+ /**
2330
+ * Removes all bridged endpoints from the specified plugin.
2331
+ *
2332
+ * @param {string} pluginName - The name of the plugin.
2333
+ * @param {number} [delay=0] - The delay in milliseconds between removing each bridged endpoint (default: 0).
2334
+ * @returns {Promise<void>} A promise that resolves when all bridged endpoints have been removed.
2335
+ */
1698
2336
  async removeAllBridgedEndpoints(pluginName, delay = 0) {
1699
2337
  this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}`);
1700
2338
  for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
@@ -1703,9 +2341,25 @@ export class Matterbridge extends EventEmitter {
1703
2341
  await new Promise((resolve) => setTimeout(resolve, delay));
1704
2342
  }
1705
2343
  }
2344
+ /**
2345
+ * Subscribes to the attribute change event for the given device and plugin.
2346
+ * Specifically, it listens for changes in the 'reachable' attribute of the
2347
+ * BridgedDeviceBasicInformationServer cluster server of the bridged device or BasicInformationServer cluster server of server node.
2348
+ *
2349
+ * @param {RegisteredPlugin} plugin - The plugin associated with the device.
2350
+ * @param {MatterbridgeEndpoint} device - The device to subscribe to attribute changes for.
2351
+ * @returns {Promise<void>} A promise that resolves when the subscription is set up.
2352
+ */
1706
2353
  async subscribeAttributeChanged(plugin, device) {
1707
2354
  this.log.info(`Subscribing attributes for endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) plugin ${plg}${plugin.name}${nf}`);
1708
2355
  if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && plugin.serverNode) {
2356
+ /*
2357
+ this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) subscribed to reachable$Changed`);
2358
+ setTimeout(async () => {
2359
+ this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) changed to reachable false`);
2360
+ await plugin.serverNode?.setStateOf(BasicInformationServer, { reachable: false });
2361
+ }, 60000).unref();
2362
+ */
1709
2363
  plugin.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
1710
2364
  this.log.info(`Accessory endpoint ${dev}${device.deviceName}${nf} (${dev}${device.id}${nf}) is ${reachable ? 'reachable' : 'unreachable'}`);
1711
2365
  this.frontend.wssSendAttributeChangedMessage(device.plugin, device.serialNumber, device.uniqueId, 'BasicInformationServer', 'reachable', reachable);
@@ -1718,6 +2372,12 @@ export class Matterbridge extends EventEmitter {
1718
2372
  });
1719
2373
  }
1720
2374
  }
2375
+ /**
2376
+ * Sanitizes the fabric information by converting bigint properties to strings because `res.json` doesn't support bigint.
2377
+ *
2378
+ * @param {ExposedFabricInformation[]} fabricInfo - The array of exposed fabric information objects.
2379
+ * @returns {SanitizedExposedFabricInformation[]} An array of sanitized exposed fabric information objects.
2380
+ */
1721
2381
  sanitizeFabricInformations(fabricInfo) {
1722
2382
  return fabricInfo.map((info) => {
1723
2383
  return {
@@ -1731,6 +2391,12 @@ export class Matterbridge extends EventEmitter {
1731
2391
  };
1732
2392
  });
1733
2393
  }
2394
+ /**
2395
+ * Sanitizes the session information by converting bigint properties to strings because `res.json` doesn't support bigint.
2396
+ *
2397
+ * @param {SessionInformation[]} sessionInfo - The array of session information objects.
2398
+ * @returns {SanitizedSessionInformation[]} An array of sanitized session information objects.
2399
+ */
1734
2400
  sanitizeSessionInformation(sessionInfo) {
1735
2401
  return sessionInfo
1736
2402
  .filter((session) => session.isPeerActive)
@@ -1758,7 +2424,20 @@ export class Matterbridge extends EventEmitter {
1758
2424
  };
1759
2425
  });
1760
2426
  }
2427
+ /**
2428
+ * Sets the reachability of the specified aggregator node bridged devices and trigger.
2429
+ * @param {EndpointNode<AggregatorEndpoint>} aggregatorNode - The aggregator node to set the reachability for.
2430
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
2431
+ */
2432
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1761
2433
  async setAggregatorReachability(aggregatorNode, reachable) {
2434
+ /*
2435
+ for (const child of aggregatorNode.parts) {
2436
+ this.log.debug(`Setting reachability of ${(child as unknown as MatterbridgeEndpoint)?.deviceName} to ${reachable}`);
2437
+ await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
2438
+ child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
2439
+ }
2440
+ */
1762
2441
  }
1763
2442
  getVendorIdName = (vendorId) => {
1764
2443
  if (!vendorId)
@@ -1792,20 +2471,46 @@ export class Matterbridge extends EventEmitter {
1792
2471
  case 4742:
1793
2472
  vendorName = '(eWeLink)';
1794
2473
  break;
2474
+ case 5264:
2475
+ vendorName = '(Shelly)';
2476
+ break;
1795
2477
  case 65521:
1796
2478
  vendorName = '(MatterServer)';
1797
2479
  break;
1798
2480
  }
1799
2481
  return vendorName;
1800
2482
  };
2483
+ /**
2484
+ * Spawns a child process with the given command and arguments.
2485
+ * @param {string} command - The command to execute.
2486
+ * @param {string[]} args - The arguments to pass to the command (default: []).
2487
+ * @returns {Promise<boolean>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
2488
+ */
1801
2489
  async spawnCommand(command, args = []) {
1802
2490
  const { spawn } = await import('node:child_process');
2491
+ /*
2492
+ npm > npm.cmd on windows
2493
+ cmd.exe ['dir'] on windows
2494
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2495
+ process.on('unhandledRejection', (reason, promise) => {
2496
+ this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
2497
+ });
2498
+
2499
+ spawn - [14:27:21.125] [Matterbridge:spawn]: changed 38 packages in 4s
2500
+ spawn - [14:27:21.125] [Matterbridge:spawn]: 10 packages are looking for funding run `npm fund` for details
2501
+ debug - [14:27:21.131] [Matterbridge]: Child process exited with code 0 and signal null
2502
+ debug - [14:27:21.131] [Matterbridge]: Child process stdio streams have closed with code 0
2503
+ */
1803
2504
  const cmdLine = command + ' ' + args.join(' ');
1804
2505
  if (process.platform === 'win32' && command === 'npm') {
2506
+ // Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
1805
2507
  const argstring = 'npm ' + args.join(' ');
1806
2508
  args.splice(0, args.length, '/c', argstring);
1807
2509
  command = 'cmd.exe';
1808
2510
  }
2511
+ // Decide when using sudo on linux
2512
+ // When you need sudo: Spawn stderr: npm error Error: EACCES: permission denied
2513
+ // When you don't need sudo: Failed to start child process "npm install -g matterbridge-eve-door": spawn sudo ENOENT
1809
2514
  if (hasParameter('sudo') || (process.platform === 'linux' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo'))) {
1810
2515
  args.unshift(command);
1811
2516
  command = 'sudo';
@@ -1864,3 +2569,4 @@ export class Matterbridge extends EventEmitter {
1864
2569
  });
1865
2570
  }
1866
2571
  }
2572
+ //# sourceMappingURL=matterbridge.js.map