matterbridge 2.0.0-edge.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +3 -2
  2. package/dist/cli.d.ts +25 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +26 -0
  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 -0
  13. package/dist/defaultConfigSchema.js.map +1 -0
  14. package/dist/deviceManager.d.ts +46 -0
  15. package/dist/deviceManager.d.ts.map +1 -0
  16. package/dist/deviceManager.js +26 -1
  17. package/dist/deviceManager.js.map +1 -0
  18. package/dist/frontend.d.ts +98 -0
  19. package/dist/frontend.d.ts.map +1 -0
  20. package/dist/frontend.js +239 -22
  21. package/dist/frontend.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  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/export.d.ts +10 -0
  31. package/dist/matter/export.d.ts.map +1 -0
  32. package/dist/matter/export.js +4 -0
  33. package/dist/matter/export.js.map +1 -0
  34. package/dist/matterbridge.d.ts +356 -0
  35. package/dist/matterbridge.d.ts.map +1 -0
  36. package/dist/matterbridge.js +687 -38
  37. package/dist/matterbridge.js.map +1 -0
  38. package/dist/matterbridgeAccessoryPlatform.d.ts +39 -0
  39. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -0
  40. package/dist/matterbridgeAccessoryPlatform.js +33 -0
  41. package/dist/matterbridgeAccessoryPlatform.js.map +1 -0
  42. package/dist/matterbridgeBehaviors.d.ts +963 -0
  43. package/dist/matterbridgeBehaviors.d.ts.map +1 -0
  44. package/dist/matterbridgeBehaviors.js +31 -1
  45. package/dist/matterbridgeBehaviors.js.map +1 -0
  46. package/dist/matterbridgeDeviceTypes.d.ts +177 -0
  47. package/dist/matterbridgeDeviceTypes.d.ts.map +1 -0
  48. package/dist/matterbridgeDeviceTypes.js +112 -11
  49. package/dist/matterbridgeDeviceTypes.js.map +1 -0
  50. package/dist/matterbridgeDynamicPlatform.d.ts +39 -0
  51. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -0
  52. package/dist/matterbridgeDynamicPlatform.js +33 -0
  53. package/dist/matterbridgeDynamicPlatform.js.map +1 -0
  54. package/dist/matterbridgeEndpoint.d.ts +10254 -0
  55. package/dist/matterbridgeEndpoint.d.ts.map +1 -0
  56. package/dist/matterbridgeEndpoint.js +1174 -10
  57. package/dist/matterbridgeEndpoint.js.map +1 -0
  58. package/dist/matterbridgeEndpointDefault.d.ts +2 -0
  59. package/dist/matterbridgeEndpointDefault.d.ts.map +1 -0
  60. package/dist/matterbridgeEndpointDefault.js +55 -0
  61. package/dist/matterbridgeEndpointDefault.js.map +1 -0
  62. package/dist/matterbridgePlatform.d.ts +164 -0
  63. package/dist/matterbridgePlatform.d.ts.map +1 -0
  64. package/dist/matterbridgePlatform.js +123 -3
  65. package/dist/matterbridgePlatform.js.map +1 -0
  66. package/dist/matterbridgeTypes.d.ts +167 -0
  67. package/dist/matterbridgeTypes.d.ts.map +1 -0
  68. package/dist/matterbridgeTypes.js +25 -0
  69. package/dist/matterbridgeTypes.js.map +1 -0
  70. package/dist/pluginManager.d.ts +238 -0
  71. package/dist/pluginManager.d.ts.map +1 -0
  72. package/dist/pluginManager.js +240 -3
  73. package/dist/pluginManager.js.map +1 -0
  74. package/dist/storage/export.d.ts +2 -0
  75. package/dist/storage/export.d.ts.map +1 -0
  76. package/dist/storage/export.js +1 -0
  77. package/dist/storage/export.js.map +1 -0
  78. package/dist/utils/colorUtils.d.ts +61 -0
  79. package/dist/utils/colorUtils.d.ts.map +1 -0
  80. package/dist/utils/colorUtils.js +205 -2
  81. package/dist/utils/colorUtils.js.map +1 -0
  82. package/dist/utils/export.d.ts +3 -0
  83. package/dist/utils/export.d.ts.map +1 -0
  84. package/dist/utils/export.js +1 -0
  85. package/dist/utils/export.js.map +1 -0
  86. package/dist/utils/utils.d.ts +221 -0
  87. package/dist/utils/utils.d.ts.map +1 -0
  88. package/dist/utils/utils.js +253 -9
  89. package/dist/utils/utils.js.map +1 -0
  90. package/npm-shrinkwrap.json +2 -2
  91. package/package.json +2 -1
@@ -1,3 +1,26 @@
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 { fileURLToPath } from 'url';
2
25
  import { promises as fs } from 'fs';
3
26
  import { exec, spawn } from 'child_process';
@@ -5,20 +28,27 @@ import EventEmitter from 'events';
5
28
  import os from 'os';
6
29
  import path from 'path';
7
30
  import { randomBytes } from 'crypto';
31
+ // NodeStorage and AnsiLogger modules
8
32
  import { NodeStorageManager } from './storage/export.js';
9
33
  import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt } from './logger/export.js';
34
+ // Matterbridge
10
35
  import { logInterfaces, wait, waiter, copyDirectory, getParameter, getIntParameter, hasParameter } from './utils/utils.js';
11
36
  import { PluginManager } from './pluginManager.js';
12
37
  import { DeviceManager } from './deviceManager.js';
13
38
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
14
39
  import { bridge } from './matterbridgeDeviceTypes.js';
15
40
  import { Frontend } from './frontend.js';
41
+ // @matter
16
42
  import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode } from '@matter/main';
17
43
  import { FabricAction, PaseClient } from '@matter/main/protocol';
18
44
  import { AggregatorEndpoint } from '@matter/main/endpoints';
45
+ // Default colors
19
46
  const plg = '\u001B[38;5;33m';
20
47
  const dev = '\u001B[38;5;79m';
21
48
  const typ = '\u001B[38;5;207m';
49
+ /**
50
+ * Represents the Matterbridge application.
51
+ */
22
52
  export class Matterbridge extends EventEmitter {
23
53
  systemInformation = {
24
54
  interfaceName: '',
@@ -53,7 +83,7 @@ export class Matterbridge extends EventEmitter {
53
83
  restartMode: '',
54
84
  readOnly: hasParameter('readonly'),
55
85
  profile: getParameter('profile'),
56
- loggerLevel: "info",
86
+ loggerLevel: "info" /* LogLevel.INFO */,
57
87
  fileLogger: false,
58
88
  matterLoggerLevel: MatterLogLevel.INFO,
59
89
  matterFileLogger: false,
@@ -88,9 +118,11 @@ export class Matterbridge extends EventEmitter {
88
118
  plugins;
89
119
  devices;
90
120
  frontend = new Frontend(this);
121
+ // Matterbridge storage
91
122
  nodeStorage;
92
123
  nodeContext;
93
124
  nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
125
+ // Cleanup
94
126
  hasCleanupStarted = false;
95
127
  initialized = false;
96
128
  execRunningCount = 0;
@@ -102,34 +134,57 @@ export class Matterbridge extends EventEmitter {
102
134
  sigtermHandler;
103
135
  exceptionHandler;
104
136
  rejectionHandler;
137
+ // Matter environment
105
138
  environment = Environment.default;
139
+ // Matter storage
106
140
  matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
107
141
  matterStorageService;
108
142
  matterStorageManager;
109
143
  matterbridgeContext;
110
144
  mattercontrollerContext;
111
- mdnsInterface;
112
- ipv4address;
113
- ipv6address;
114
- port;
115
- passcode;
116
- discriminator;
145
+ // Matter parameters
146
+ mdnsInterface; // matter server node mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
147
+ ipv4address; // matter server node listeningAddressIpv4
148
+ ipv6address; // matter server node listeningAddressIpv6
149
+ port; // first server node port
150
+ passcode; // first server node passcode
151
+ discriminator; // first server node discriminator
117
152
  serverNode;
118
153
  aggregatorNode;
119
154
  aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
120
155
  aggregatorProductId = getIntParameter('productId') ?? 0x8000;
121
156
  static instance;
157
+ // We load asyncronously so is private
122
158
  constructor() {
123
159
  super();
124
160
  }
161
+ /**
162
+ * Retrieves the list of Matterbridge devices.
163
+ * @returns {MatterbridgeEndpoint[]} An array of MatterbridgeDevice objects.
164
+ */
125
165
  getDevices() {
126
166
  return this.devices.array();
127
167
  }
168
+ /**
169
+ * Retrieves the list of registered plugins.
170
+ * @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects.
171
+ */
128
172
  getPlugins() {
129
173
  return this.plugins.array();
130
174
  }
175
+ /** ***********************************************************************************************************************************/
176
+ /** loadInstance() and cleanup() methods */
177
+ /** ***********************************************************************************************************************************/
178
+ /**
179
+ * Loads an instance of the Matterbridge class.
180
+ * If an instance already exists, return that instance.
181
+ *
182
+ * @param initialize - Whether to initialize the Matterbridge instance after loading.
183
+ * @returns The loaded Matterbridge instance.
184
+ */
131
185
  static async loadInstance(initialize = false) {
132
186
  if (!Matterbridge.instance) {
187
+ // eslint-disable-next-line no-console
133
188
  if (hasParameter('debug'))
134
189
  console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
135
190
  Matterbridge.instance = new Matterbridge();
@@ -138,6 +193,11 @@ export class Matterbridge extends EventEmitter {
138
193
  }
139
194
  return Matterbridge.instance;
140
195
  }
196
+ /**
197
+ * Call cleanup().
198
+ * @deprecated This method is deprecated and is only used for jest tests.
199
+ *
200
+ */
141
201
  async destroyInstance() {
142
202
  await this.cleanup('destroying instance...', false);
143
203
  await waiter('destroying instance...', () => {
@@ -145,45 +205,68 @@ export class Matterbridge extends EventEmitter {
145
205
  }, false, 60000, 100, false);
146
206
  await wait(1000, 'Wait for the global node_modules and matterbridge version', false);
147
207
  }
208
+ /**
209
+ * Initializes the Matterbridge application.
210
+ *
211
+ * @remarks
212
+ * This method performs the necessary setup and initialization steps for the Matterbridge application.
213
+ * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
214
+ * node version, registers signal handlers, initializes storage, and parses the command line.
215
+ *
216
+ * @returns A Promise that resolves when the initialization is complete.
217
+ */
148
218
  async initialize() {
219
+ // Set the restart mode
149
220
  if (hasParameter('service'))
150
221
  this.restartMode = 'service';
151
222
  if (hasParameter('docker'))
152
223
  this.restartMode = 'docker';
224
+ // Set the matterbridge directory
153
225
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
154
226
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
227
+ // Setup matter environment
155
228
  this.environment.vars.set('log.level', MatterLogLevel.INFO);
156
229
  this.environment.vars.set('log.format', MatterLogFormat.ANSI);
157
230
  this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName));
158
231
  this.environment.vars.set('runtime.signals', false);
159
232
  this.environment.vars.set('runtime.exitcode', false);
160
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
233
+ // Create matterbridge logger
234
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
235
+ // Register process handlers
161
236
  this.registerProcessHandlers();
237
+ // Initialize nodeStorage and nodeContext
162
238
  try {
163
239
  this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
164
240
  this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
165
241
  this.log.debug('Creating node storage context for matterbridge');
166
242
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
243
+ // TODO: Remove this code when node-persist-manager is updated
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
167
245
  const keys = (await this.nodeStorage?.storage.keys());
168
246
  for (const key of keys) {
169
247
  this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
248
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
249
  await this.nodeStorage?.storage.get(key);
171
250
  }
172
251
  const storages = await this.nodeStorage.getStorageNames();
173
252
  for (const storage of storages) {
174
253
  this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
175
254
  const nodeContext = await this.nodeStorage?.createStorage(storage);
255
+ // TODO: Remove this code when node-persist-manager is updated
256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
257
  const keys = (await nodeContext?.storage.keys());
177
258
  keys.forEach(async (key) => {
178
259
  this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
179
260
  await nodeContext?.get(key);
180
261
  });
181
262
  }
263
+ // Creating a backup of the node storage since it is not corrupted
182
264
  this.log.debug('Creating node storage backup...');
183
265
  await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'));
184
266
  this.log.debug('Created node storage backup');
185
267
  }
186
268
  catch (error) {
269
+ // Restoring the backup of the node storage since it is corrupted
187
270
  this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
188
271
  if (hasParameter('norestore')) {
189
272
  this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
@@ -198,45 +281,51 @@ export class Matterbridge extends EventEmitter {
198
281
  this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
199
282
  throw new Error('Fatal error creating node storage manager and context for matterbridge');
200
283
  }
284
+ // Set the first port to use for the commissioning server (will be incremented in childbridge mode)
201
285
  this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
286
+ // Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
202
287
  this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
288
+ // Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
203
289
  this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
204
290
  this.log.debug(`Initializing commissioning server for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
291
+ // Set matterbridge logger level (context: matterbridgeLogLevel)
205
292
  if (hasParameter('logger')) {
206
293
  const level = getParameter('logger');
207
294
  if (level === 'debug') {
208
- this.log.logLevel = "debug";
295
+ this.log.logLevel = "debug" /* LogLevel.DEBUG */;
209
296
  }
210
297
  else if (level === 'info') {
211
- this.log.logLevel = "info";
298
+ this.log.logLevel = "info" /* LogLevel.INFO */;
212
299
  }
213
300
  else if (level === 'notice') {
214
- this.log.logLevel = "notice";
301
+ this.log.logLevel = "notice" /* LogLevel.NOTICE */;
215
302
  }
216
303
  else if (level === 'warn') {
217
- this.log.logLevel = "warn";
304
+ this.log.logLevel = "warn" /* LogLevel.WARN */;
218
305
  }
219
306
  else if (level === 'error') {
220
- this.log.logLevel = "error";
307
+ this.log.logLevel = "error" /* LogLevel.ERROR */;
221
308
  }
222
309
  else if (level === 'fatal') {
223
- this.log.logLevel = "fatal";
310
+ this.log.logLevel = "fatal" /* LogLevel.FATAL */;
224
311
  }
225
312
  else {
226
313
  this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
227
- this.log.logLevel = "info";
314
+ this.log.logLevel = "info" /* LogLevel.INFO */;
228
315
  }
229
316
  }
230
317
  else {
231
- this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', "info");
318
+ this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', "info" /* LogLevel.INFO */);
232
319
  }
233
320
  MatterbridgeEndpoint.logLevel = this.log.logLevel;
321
+ // Create the file logger for matterbridge (context: matterbridgeFileLog)
234
322
  if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
235
323
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true);
236
324
  this.matterbridgeInformation.fileLogger = true;
237
325
  }
238
326
  this.log.notice('Matterbridge is starting...');
239
327
  this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`);
328
+ // Set matter.js logger level, format and logger (context: matterLogLevel)
240
329
  if (hasParameter('matterlogger')) {
241
330
  const level = getParameter('matterlogger');
242
331
  if (level === 'debug') {
@@ -267,6 +356,7 @@ export class Matterbridge extends EventEmitter {
267
356
  }
268
357
  Logger.format = MatterLogFormat.ANSI;
269
358
  Logger.setLogger('default', this.createMatterLogger());
359
+ // Create the file logger for matter.js (context: matterFileLog)
270
360
  if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
271
361
  this.matterbridgeInformation.matterFileLogger = true;
272
362
  Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
@@ -275,6 +365,7 @@ export class Matterbridge extends EventEmitter {
275
365
  });
276
366
  }
277
367
  this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
368
+ // Set the interface to use for matter server node mdnsInterface
278
369
  if (hasParameter('mdnsinterface')) {
279
370
  this.mdnsInterface = getParameter('mdnsinterface');
280
371
  }
@@ -283,6 +374,7 @@ export class Matterbridge extends EventEmitter {
283
374
  if (this.mdnsInterface === '')
284
375
  this.mdnsInterface = undefined;
285
376
  }
377
+ // Validate mdnsInterface
286
378
  if (this.mdnsInterface) {
287
379
  const networkInterfaces = os.networkInterfaces();
288
380
  const availableInterfaces = Object.keys(networkInterfaces);
@@ -296,6 +388,7 @@ export class Matterbridge extends EventEmitter {
296
388
  }
297
389
  if (this.mdnsInterface)
298
390
  this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
391
+ // Set the listeningAddressIpv4 for the matter commissioning server
299
392
  if (hasParameter('ipv4address')) {
300
393
  this.ipv4address = getParameter('ipv4address');
301
394
  }
@@ -304,6 +397,7 @@ export class Matterbridge extends EventEmitter {
304
397
  if (this.ipv4address === '')
305
398
  this.ipv4address = undefined;
306
399
  }
400
+ // Set the listeningAddressIpv6 for the matter commissioning server
307
401
  if (hasParameter('ipv6address')) {
308
402
  this.ipv6address = getParameter('ipv6address');
309
403
  }
@@ -312,12 +406,17 @@ export class Matterbridge extends EventEmitter {
312
406
  if (this.ipv6address === '')
313
407
  this.ipv6address = undefined;
314
408
  }
409
+ // Initialize PluginManager
315
410
  this.plugins = new PluginManager(this);
316
411
  await this.plugins.loadFromStorage();
412
+ // Initialize DeviceManager
317
413
  this.devices = new DeviceManager(this, this.nodeContext);
414
+ // Get the plugins from node storage and create the plugins node storage contexts
318
415
  for (const plugin of this.plugins) {
319
416
  const packageJson = await this.plugins.parse(plugin);
320
417
  if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) {
418
+ // Try to reinstall the plugin from npm (for Docker pull and external plugins)
419
+ // We don't do this when the add and other parameters are set because we shut down the process after adding the plugin
321
420
  this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
322
421
  try {
323
422
  await this.spawnCommand('npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']);
@@ -339,6 +438,7 @@ export class Matterbridge extends EventEmitter {
339
438
  await plugin.nodeContext.set('description', plugin.description);
340
439
  await plugin.nodeContext.set('author', plugin.author);
341
440
  }
441
+ // Log system info and create .matterbridge directory
342
442
  await this.logNodeAndSystemInfo();
343
443
  this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
344
444
  `${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
@@ -346,6 +446,7 @@ export class Matterbridge extends EventEmitter {
346
446
  `${hasParameter('controller') ? 'mode controller ' : ''}` +
347
447
  `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
348
448
  `running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
449
+ // Check node version and throw error
349
450
  const minNodeVersion = 18;
350
451
  const nodeVersion = process.versions.node;
351
452
  const versionMajor = parseInt(nodeVersion.split('.')[0]);
@@ -353,9 +454,15 @@ export class Matterbridge extends EventEmitter {
353
454
  this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
354
455
  throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
355
456
  }
457
+ // Parse command line
356
458
  await this.parseCommandLine();
357
459
  this.initialized = true;
358
460
  }
461
+ /**
462
+ * Parses the command line arguments and performs the corresponding actions.
463
+ * @private
464
+ * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
465
+ */
359
466
  async parseCommandLine() {
360
467
  if (hasParameter('help')) {
361
468
  this.log.info(`\nUsage: matterbridge [options]\n
@@ -465,6 +572,7 @@ export class Matterbridge extends EventEmitter {
465
572
  await this.shutdownProcessAndFactoryReset();
466
573
  return;
467
574
  }
575
+ // Start the matter storage and create the matterbridge context
468
576
  try {
469
577
  await this.startMatterStorage();
470
578
  }
@@ -472,10 +580,12 @@ export class Matterbridge extends EventEmitter {
472
580
  this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
473
581
  throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
474
582
  }
583
+ // Clear the matterbridge context if the reset parameter is set
475
584
  if (hasParameter('reset') && getParameter('reset') === undefined) {
476
585
  await this.shutdownProcessAndReset();
477
586
  return;
478
587
  }
588
+ // Clear matterbridge plugin context if the reset parameter is set
479
589
  if (hasParameter('reset') && getParameter('reset') !== undefined) {
480
590
  this.log.debug(`Reset plugin ${getParameter('reset')}`);
481
591
  const plugin = this.plugins.get(getParameter('reset'));
@@ -497,8 +607,10 @@ export class Matterbridge extends EventEmitter {
497
607
  this.emit('shutdown');
498
608
  return;
499
609
  }
610
+ // Initialize frontend
500
611
  if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
501
612
  await this.frontend.start(getIntParameter('frontend'));
613
+ // Check each 60 minutes the latest versions
502
614
  this.checkUpdateInterval = setInterval(() => {
503
615
  this.getMatterbridgeLatestVersion();
504
616
  for (const plugin of this.plugins) {
@@ -506,20 +618,24 @@ export class Matterbridge extends EventEmitter {
506
618
  }
507
619
  this.frontend.wssSendRefreshRequired();
508
620
  }, 60 * 60 * 1000);
621
+ // Start the matterbridge in mode test
509
622
  if (hasParameter('test')) {
510
623
  this.bridgeMode = 'bridge';
511
624
  MatterbridgeEndpoint.bridgeMode = 'bridge';
512
625
  return;
513
626
  }
627
+ // Start the matterbridge in mode controller
514
628
  if (hasParameter('controller')) {
515
629
  this.bridgeMode = 'controller';
516
630
  await this.startController();
517
631
  return;
518
632
  }
633
+ // Check if the bridge mode is set and start matterbridge in bridge mode if not set
519
634
  if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
520
635
  this.log.info('Setting default matterbridge start mode to bridge');
521
636
  await this.nodeContext?.set('bridgeMode', 'bridge');
522
637
  }
638
+ // Start matterbridge in bridge mode
523
639
  if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
524
640
  this.bridgeMode = 'bridge';
525
641
  MatterbridgeEndpoint.bridgeMode = 'bridge';
@@ -527,6 +643,7 @@ export class Matterbridge extends EventEmitter {
527
643
  await this.startBridge();
528
644
  return;
529
645
  }
646
+ // Start matterbridge in childbridge mode
530
647
  if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
531
648
  this.bridgeMode = 'childbridge';
532
649
  MatterbridgeEndpoint.bridgeMode = 'childbridge';
@@ -535,16 +652,28 @@ export class Matterbridge extends EventEmitter {
535
652
  return;
536
653
  }
537
654
  }
655
+ /**
656
+ * Asynchronously loads and starts the registered plugins.
657
+ *
658
+ * This method is responsible for initializing and staarting all enabled plugins.
659
+ * It ensures that each plugin is properly loaded and started before the bridge starts.
660
+ *
661
+ * @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
662
+ */
538
663
  async startPlugins() {
664
+ // Check, load and start the plugins
539
665
  for (const plugin of this.plugins) {
540
666
  plugin.configJson = await this.plugins.loadConfig(plugin);
541
667
  plugin.schemaJson = await this.plugins.loadSchema(plugin);
668
+ // Check if the plugin is available
542
669
  if (!(await this.plugins.resolve(plugin.path))) {
543
670
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
544
671
  plugin.enabled = false;
545
672
  plugin.error = true;
546
673
  continue;
547
674
  }
675
+ // Check if the plugin has a new version
676
+ // this.getPluginLatestVersion(plugin); // No await do it asyncronously
548
677
  if (!plugin.enabled) {
549
678
  this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
550
679
  continue;
@@ -558,20 +687,26 @@ export class Matterbridge extends EventEmitter {
558
687
  plugin.addedDevices = undefined;
559
688
  plugin.qrPairingCode = undefined;
560
689
  plugin.manualPairingCode = undefined;
561
- this.plugins.load(plugin, true, 'Matterbridge is starting');
690
+ this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
562
691
  }
563
692
  this.frontend.wssSendRefreshRequired();
564
693
  }
694
+ /**
695
+ * Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
696
+ * When either of these signals are received, the cleanup method is called with an appropriate message.
697
+ */
565
698
  registerProcessHandlers() {
566
699
  this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
567
700
  process.removeAllListeners('uncaughtException');
568
701
  process.removeAllListeners('unhandledRejection');
569
702
  this.exceptionHandler = async (error) => {
570
703
  this.log.fatal('Unhandled Exception detected at:', error.stack || error, rs);
704
+ // await this.cleanup('Unhandled Exception detected, cleaning up...');
571
705
  };
572
706
  process.on('uncaughtException', this.exceptionHandler);
573
707
  this.rejectionHandler = async (reason, promise) => {
574
708
  this.log.fatal('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
709
+ // await this.cleanup('Unhandled Rejection detected, cleaning up...');
575
710
  };
576
711
  process.on('unhandledRejection', this.rejectionHandler);
577
712
  this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
@@ -584,6 +719,9 @@ export class Matterbridge extends EventEmitter {
584
719
  };
585
720
  process.on('SIGTERM', this.sigtermHandler);
586
721
  }
722
+ /**
723
+ * Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
724
+ */
587
725
  deregisterProcesslHandlers() {
588
726
  this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
589
727
  if (this.exceptionHandler)
@@ -600,12 +738,17 @@ export class Matterbridge extends EventEmitter {
600
738
  process.off('SIGTERM', this.sigtermHandler);
601
739
  this.sigtermHandler = undefined;
602
740
  }
741
+ /**
742
+ * Logs the node and system information.
743
+ */
603
744
  async logNodeAndSystemInfo() {
745
+ // IP address information
604
746
  const networkInterfaces = os.networkInterfaces();
605
747
  this.systemInformation.interfaceName = '';
606
748
  this.systemInformation.ipv4Address = '';
607
749
  this.systemInformation.ipv6Address = '';
608
750
  for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
751
+ // this.log.debug(`Checking interface: '${interfaceName}' for '${this.mdnsInterface}'`);
609
752
  if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
610
753
  continue;
611
754
  if (!interfaceDetails) {
@@ -631,19 +774,22 @@ export class Matterbridge extends EventEmitter {
631
774
  break;
632
775
  }
633
776
  }
777
+ // Node information
634
778
  this.systemInformation.nodeVersion = process.versions.node;
635
779
  const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
636
780
  const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
637
781
  const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
782
+ // Host system information
638
783
  this.systemInformation.hostname = os.hostname();
639
784
  this.systemInformation.user = os.userInfo().username;
640
- this.systemInformation.osType = os.type();
641
- this.systemInformation.osRelease = os.release();
642
- this.systemInformation.osPlatform = os.platform();
643
- this.systemInformation.osArch = os.arch();
644
- this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
645
- this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
646
- this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours';
785
+ this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
786
+ this.systemInformation.osRelease = os.release(); // Kernel version
787
+ this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
788
+ this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
789
+ this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
790
+ this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
791
+ this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
792
+ // Log the system information
647
793
  this.log.debug('Host System Information:');
648
794
  this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
649
795
  this.log.debug(`- User: ${this.systemInformation.user}`);
@@ -659,15 +805,19 @@ export class Matterbridge extends EventEmitter {
659
805
  this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
660
806
  this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
661
807
  this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
808
+ // Home directory
662
809
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
663
810
  this.matterbridgeInformation.homeDirectory = this.homeDirectory;
664
811
  this.log.debug(`Home Directory: ${this.homeDirectory}`);
812
+ // Package root directory
665
813
  const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
666
814
  this.rootDirectory = path.resolve(currentFileDirectory, '../');
667
815
  this.matterbridgeInformation.rootDirectory = this.rootDirectory;
668
816
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
817
+ // Global node_modules directory
669
818
  if (this.nodeContext)
670
- this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
819
+ this.globalModulesDirectory = this.matterbridgeInformation.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
820
+ // First run of Matterbridge so the node storage is empty
671
821
  if (this.globalModulesDirectory === '') {
672
822
  try {
673
823
  this.globalModulesDirectory = await this.getGlobalNodeModules();
@@ -681,6 +831,20 @@ export class Matterbridge extends EventEmitter {
681
831
  }
682
832
  else
683
833
  this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
834
+ /* removed cause is too expensive for the shelly board and not really needed. Why should it change the globalModulesDirectory?
835
+ else {
836
+ this.getGlobalNodeModules()
837
+ .then(async (globalModulesDirectory) => {
838
+ this.globalModulesDirectory = globalModulesDirectory;
839
+ this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
840
+ this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
841
+ await this.nodeContext?.set<string>('globalModulesDirectory', this.globalModulesDirectory);
842
+ })
843
+ .catch((error) => {
844
+ this.log.error(`Error getting global node_modules directory: ${error}`);
845
+ });
846
+ }*/
847
+ // Create the data directory .matterbridge in the home directory
684
848
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
685
849
  this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
686
850
  try {
@@ -704,6 +868,7 @@ export class Matterbridge extends EventEmitter {
704
868
  }
705
869
  }
706
870
  this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
871
+ // Create the plugin directory Matterbridge in the home directory
707
872
  this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
708
873
  this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
709
874
  try {
@@ -727,18 +892,28 @@ export class Matterbridge extends EventEmitter {
727
892
  }
728
893
  }
729
894
  this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
895
+ // Matterbridge version
730
896
  const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
731
897
  this.matterbridgeVersion = this.matterbridgeLatestVersion = packageJson.version;
732
898
  this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeVersion;
733
899
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
900
+ // Matterbridge latest version
734
901
  if (this.nodeContext)
735
902
  this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', this.matterbridgeVersion);
736
903
  this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
904
+ // this.getMatterbridgeLatestVersion();
905
+ // Current working directory
737
906
  const currentDir = process.cwd();
738
907
  this.log.debug(`Current Working Directory: ${currentDir}`);
908
+ // Command line arguments (excluding 'node' and the script name)
739
909
  const cmdArgs = process.argv.slice(2).join(' ');
740
910
  this.log.debug(`Command Line Arguments: ${cmdArgs}`);
741
911
  }
912
+ /**
913
+ * Retrieves the latest version of a package from the npm registry.
914
+ * @param packageName - The name of the package.
915
+ * @returns A Promise that resolves to the latest version of the package.
916
+ */
742
917
  async getLatestVersion(packageName) {
743
918
  return new Promise((resolve, reject) => {
744
919
  this.execRunningCount++;
@@ -753,7 +928,16 @@ export class Matterbridge extends EventEmitter {
753
928
  });
754
929
  });
755
930
  }
931
+ /**
932
+ * Retrieves the path to the global Node.js modules directory.
933
+ * @returns A promise that resolves to the path of the global Node.js modules directory.
934
+ */
756
935
  async getGlobalNodeModules() {
936
+ /*
937
+ const { Module } = await import('module'); // Dynamic import to access the `Module` class
938
+ const globalPaths = 'globalPaths' in Module && Array.isArray(Module.globalPaths) && typeof Module.globalPaths[0] === 'string' ? (Module.globalPaths as string[])[0] : '';
939
+ this.log.debug('Module.globalPaths:', globalPaths);
940
+ */
757
941
  return new Promise((resolve, reject) => {
758
942
  this.execRunningCount++;
759
943
  exec('npm root -g', (error, stdout) => {
@@ -767,6 +951,11 @@ export class Matterbridge extends EventEmitter {
767
951
  });
768
952
  });
769
953
  }
954
+ /**
955
+ * Retrieves the latest version of Matterbridge and performs necessary actions based on the version comparison.
956
+ * @private
957
+ * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
958
+ */
770
959
  async getMatterbridgeLatestVersion() {
771
960
  this.getLatestVersion('matterbridge')
772
961
  .then(async (matterbridgeLatestVersion) => {
@@ -783,8 +972,19 @@ export class Matterbridge extends EventEmitter {
783
972
  })
784
973
  .catch((error) => {
785
974
  this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
975
+ // error.stack && this.log.debug(error.stack);
786
976
  });
787
977
  }
978
+ /**
979
+ * Retrieves the latest version of a plugin and updates the plugin's latestVersion property.
980
+ * If the plugin's version is different from the latest version, logs a warning message.
981
+ * If the plugin's version is the same as the latest version, logs an info message.
982
+ * If there is an error retrieving the latest version, logs an error message.
983
+ *
984
+ * @private
985
+ * @param {RegisteredPlugin} plugin - The plugin for which to retrieve the latest version.
986
+ * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
987
+ */
788
988
  async getPluginLatestVersion(plugin) {
789
989
  this.getLatestVersion(plugin.name)
790
990
  .then(async (latestVersion) => {
@@ -796,40 +996,54 @@ export class Matterbridge extends EventEmitter {
796
996
  })
797
997
  .catch((error) => {
798
998
  this.log.error(`Error getting ${plg}${plugin.name}${er} latest version: ${error.message}`);
999
+ // error.stack && this.log.debug(error.stack);
799
1000
  });
800
1001
  }
1002
+ /**
1003
+ * Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
1004
+ *
1005
+ * @returns {Function} The MatterLogger function.
1006
+ */
801
1007
  createMatterLogger() {
802
- const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4, logLevel: "debug" });
1008
+ const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
803
1009
  return (_level, formattedLog) => {
804
1010
  const logger = formattedLog.slice(44, 44 + 20).trim();
805
1011
  const message = formattedLog.slice(65);
806
1012
  matterLogger.logName = logger;
807
1013
  switch (_level) {
808
1014
  case MatterLogLevel.DEBUG:
809
- matterLogger.log("debug", message);
1015
+ matterLogger.log("debug" /* LogLevel.DEBUG */, message);
810
1016
  break;
811
1017
  case MatterLogLevel.INFO:
812
- matterLogger.log("info", message);
1018
+ matterLogger.log("info" /* LogLevel.INFO */, message);
813
1019
  break;
814
1020
  case MatterLogLevel.NOTICE:
815
- matterLogger.log("notice", message);
1021
+ matterLogger.log("notice" /* LogLevel.NOTICE */, message);
816
1022
  break;
817
1023
  case MatterLogLevel.WARN:
818
- matterLogger.log("warn", message);
1024
+ matterLogger.log("warn" /* LogLevel.WARN */, message);
819
1025
  break;
820
1026
  case MatterLogLevel.ERROR:
821
- matterLogger.log("error", message);
1027
+ matterLogger.log("error" /* LogLevel.ERROR */, message);
822
1028
  break;
823
1029
  case MatterLogLevel.FATAL:
824
- matterLogger.log("fatal", message);
1030
+ matterLogger.log("fatal" /* LogLevel.FATAL */, message);
825
1031
  break;
826
1032
  default:
827
- matterLogger.log("debug", message);
1033
+ matterLogger.log("debug" /* LogLevel.DEBUG */, message);
828
1034
  break;
829
1035
  }
830
1036
  };
831
1037
  }
1038
+ /**
1039
+ * Creates a Matter File Logger.
1040
+ *
1041
+ * @param {string} filePath - The path to the log file.
1042
+ * @param {boolean} [unlink=false] - Whether to unlink the log file before creating a new one.
1043
+ * @returns {Function} - A function that logs formatted messages to the log file.
1044
+ */
832
1045
  async createMatterFileLogger(filePath, unlink = false) {
1046
+ // 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
833
1047
  let fileSize = 0;
834
1048
  if (unlink) {
835
1049
  try {
@@ -878,12 +1092,21 @@ export class Matterbridge extends EventEmitter {
878
1092
  }
879
1093
  };
880
1094
  }
1095
+ /**
1096
+ * Restarts the process by exiting the current instance and loading a new instance.
1097
+ */
881
1098
  async restartProcess() {
882
1099
  await this.cleanup('restarting...', true);
883
1100
  }
1101
+ /**
1102
+ * Shut down the process by exiting the current process.
1103
+ */
884
1104
  async shutdownProcess() {
885
1105
  await this.cleanup('shutting down...', false);
886
1106
  }
1107
+ /**
1108
+ * Update matterbridge and and shut down the process.
1109
+ */
887
1110
  async updateProcess() {
888
1111
  this.log.info('Updating matterbridge...');
889
1112
  try {
@@ -896,6 +1119,9 @@ export class Matterbridge extends EventEmitter {
896
1119
  this.frontend.wssSendRestartRequired();
897
1120
  await this.cleanup('updating...', false);
898
1121
  }
1122
+ /**
1123
+ * Unregister all devices and shut down the process.
1124
+ */
899
1125
  async unregisterAndShutdownProcess() {
900
1126
  this.log.info('Unregistering all devices and shutting down...');
901
1127
  for (const plugin of this.plugins) {
@@ -903,6 +1129,9 @@ export class Matterbridge extends EventEmitter {
903
1129
  }
904
1130
  await this.cleanup('unregistered all devices and shutting down...', false);
905
1131
  }
1132
+ /**
1133
+ * Reset commissioning and shut down the process.
1134
+ */
906
1135
  async shutdownProcessAndReset() {
907
1136
  this.log.info('Resetting Matterbridge commissioning information...');
908
1137
  await this.matterStorageManager?.createContext('events')?.clearAll();
@@ -914,8 +1143,12 @@ export class Matterbridge extends EventEmitter {
914
1143
  this.log.info('Matter storage reset done! Remove the bridge from the controller.');
915
1144
  await this.cleanup('shutting down with reset...', false);
916
1145
  }
1146
+ /**
1147
+ * Factory reset and shut down the process.
1148
+ */
917
1149
  async shutdownProcessAndFactoryReset() {
918
1150
  try {
1151
+ // Delete old matter storage file and backup
919
1152
  const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
920
1153
  this.log.info(`Unlinking old matter storage file: ${file}`);
921
1154
  await fs.unlink(file);
@@ -929,6 +1162,7 @@ export class Matterbridge extends EventEmitter {
929
1162
  }
930
1163
  }
931
1164
  try {
1165
+ // Delete matter node storage directory with its subdirectories and backup
932
1166
  const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
933
1167
  this.log.info(`Removing matter node storage directory: ${dir}`);
934
1168
  await fs.rm(dir, { recursive: true });
@@ -942,6 +1176,7 @@ export class Matterbridge extends EventEmitter {
942
1176
  }
943
1177
  }
944
1178
  try {
1179
+ // Delete node storage directory with its subdirectories and backup
945
1180
  const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
946
1181
  this.log.info(`Removing storage directory: ${dir}`);
947
1182
  await fs.rm(dir, { recursive: true });
@@ -961,30 +1196,41 @@ export class Matterbridge extends EventEmitter {
961
1196
  this.devices.clear();
962
1197
  await this.cleanup('shutting down with factory reset...', false);
963
1198
  }
1199
+ /**
1200
+ * Cleans up the Matterbridge instance.
1201
+ * @param message - The cleanup message.
1202
+ * @param restart - Indicates whether to restart the instance after cleanup. Default is `false`.
1203
+ * @returns A promise that resolves when the cleanup is completed.
1204
+ */
964
1205
  async cleanup(message, restart = false) {
965
1206
  if (this.initialized && !this.hasCleanupStarted) {
966
1207
  this.hasCleanupStarted = true;
967
1208
  this.log.info(message);
1209
+ // Clear the start matter interval
968
1210
  if (this.startMatterInterval) {
969
1211
  clearInterval(this.startMatterInterval);
970
1212
  this.startMatterInterval = undefined;
971
1213
  this.log.debug('Start matter interval cleared');
972
1214
  }
1215
+ // Clear the check update interval
973
1216
  if (this.checkUpdateInterval) {
974
1217
  clearInterval(this.checkUpdateInterval);
975
1218
  this.checkUpdateInterval = undefined;
976
1219
  this.log.debug('Check update interval cleared');
977
1220
  }
1221
+ // Clear the configure timeout
978
1222
  if (this.configureTimeout) {
979
1223
  clearTimeout(this.configureTimeout);
980
1224
  this.configureTimeout = undefined;
981
1225
  this.log.debug('Matterbridge configure timeout cleared');
982
1226
  }
1227
+ // Clear the reachability timeout
983
1228
  if (this.reachabilityTimeout) {
984
1229
  clearTimeout(this.reachabilityTimeout);
985
1230
  this.reachabilityTimeout = undefined;
986
1231
  this.log.debug('Matterbridge reachability timeout cleared');
987
1232
  }
1233
+ // Calling the shutdown method of each plugin and clear the plugins reachability timeout
988
1234
  for (const plugin of this.plugins) {
989
1235
  if (!plugin.enabled || plugin.error)
990
1236
  continue;
@@ -995,7 +1241,9 @@ export class Matterbridge extends EventEmitter {
995
1241
  this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
996
1242
  }
997
1243
  }
1244
+ // Stop the frontend
998
1245
  this.frontend.stop();
1246
+ // Stopping matter server nodes
999
1247
  this.log.info(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
1000
1248
  if (this.bridgeMode === 'bridge') {
1001
1249
  if (this.serverNode) {
@@ -1012,25 +1260,36 @@ export class Matterbridge extends EventEmitter {
1012
1260
  }
1013
1261
  }
1014
1262
  this.log.info('Stopped matter server nodes');
1263
+ // Stop matter MdnsService
1264
+ // await this.environment.get(MdnsService)[Symbol.asyncDispose]();
1265
+ // this.log.info('Stopped MdnsService');
1266
+ // Stop matter storage
1015
1267
  await this.stopMatterStorage();
1268
+ // Remove the matterfilelogger
1016
1269
  try {
1017
1270
  Logger.removeLogger('matterfilelogger');
1271
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1018
1272
  }
1019
1273
  catch (error) {
1274
+ // this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1020
1275
  }
1276
+ // Serialize registeredDevices
1021
1277
  if (this.nodeStorage && this.nodeContext) {
1022
1278
  this.log.info('Saving registered devices...');
1023
1279
  const serializedRegisteredDevices = [];
1024
1280
  this.devices.forEach(async (device) => {
1025
1281
  const serializedMatterbridgeDevice = device.serialize();
1282
+ // this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
1026
1283
  if (serializedMatterbridgeDevice)
1027
1284
  serializedRegisteredDevices.push(serializedMatterbridgeDevice);
1028
1285
  });
1029
1286
  await this.nodeContext.set('devices', serializedRegisteredDevices);
1030
1287
  this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
1288
+ // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
1031
1289
  this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
1032
1290
  await this.nodeContext.close();
1033
1291
  this.nodeContext = undefined;
1292
+ // Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
1034
1293
  for (const plugin of this.plugins) {
1035
1294
  if (plugin.nodeContext) {
1036
1295
  this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
@@ -1047,12 +1306,13 @@ export class Matterbridge extends EventEmitter {
1047
1306
  }
1048
1307
  this.plugins.clear();
1049
1308
  this.devices.clear();
1309
+ // Deregisters the process handlers
1050
1310
  this.deregisterProcesslHandlers();
1051
1311
  if (restart) {
1052
1312
  if (message === 'updating...') {
1053
1313
  this.log.info('Cleanup completed. Updating...');
1054
1314
  Matterbridge.instance = undefined;
1055
- this.emit('update');
1315
+ this.emit('update'); // Restart the process but the update has been done before
1056
1316
  }
1057
1317
  else if (message === 'restarting...') {
1058
1318
  this.log.info('Cleanup completed. Restarting...');
@@ -1069,6 +1329,14 @@ export class Matterbridge extends EventEmitter {
1069
1329
  this.initialized = false;
1070
1330
  }
1071
1331
  }
1332
+ /**
1333
+ * Creates and configures the server node for an accessory plugin for a given device.
1334
+ *
1335
+ * @param {RegisteredPlugin} plugin - The plugin to configure.
1336
+ * @param {MatterbridgeEndpoint} device - The device to associate with the plugin.
1337
+ * @param {boolean} [start=false] - Whether to start the server node after adding the device.
1338
+ * @returns {Promise<void>} A promise that resolves when the server node for the accessory plugin is created and configured.
1339
+ */
1072
1340
  async createAccessoryPlugin(plugin, device, start = false) {
1073
1341
  if (!plugin.locked && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1074
1342
  plugin.locked = true;
@@ -1080,6 +1348,13 @@ export class Matterbridge extends EventEmitter {
1080
1348
  await this.startServerNode(plugin.serverNode);
1081
1349
  }
1082
1350
  }
1351
+ /**
1352
+ * Creates and configures the server node for a dynamic plugin.
1353
+ *
1354
+ * @param {RegisteredPlugin} plugin - The plugin to configure.
1355
+ * @param {boolean} [start=false] - Whether to start the server node after adding the aggregator node.
1356
+ * @returns {Promise<void>} A promise that resolves when the server node for the dynamic plugin is created and configured.
1357
+ */
1083
1358
  async createDynamicPlugin(plugin, start = false) {
1084
1359
  if (!plugin.locked) {
1085
1360
  plugin.locked = true;
@@ -1091,7 +1366,13 @@ export class Matterbridge extends EventEmitter {
1091
1366
  await this.startServerNode(plugin.serverNode);
1092
1367
  }
1093
1368
  }
1369
+ /**
1370
+ * Starts the Matterbridge in bridge mode.
1371
+ * @private
1372
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1373
+ */
1094
1374
  async startBridge() {
1375
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1095
1376
  if (!this.matterStorageManager)
1096
1377
  throw new Error('No storage manager initialized');
1097
1378
  if (!this.matterbridgeContext)
@@ -1128,13 +1409,15 @@ export class Matterbridge extends EventEmitter {
1128
1409
  clearInterval(this.startMatterInterval);
1129
1410
  this.startMatterInterval = undefined;
1130
1411
  this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1412
+ // Start the Matter server node
1131
1413
  this.startServerNode(this.serverNode);
1414
+ // Configure the plugins
1132
1415
  this.configureTimeout = setTimeout(async () => {
1133
1416
  for (const plugin of this.plugins) {
1134
1417
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1135
1418
  continue;
1136
1419
  try {
1137
- await this.plugins.configure(plugin);
1420
+ await this.plugins.configure(plugin); // TODO No await do it in parallel
1138
1421
  }
1139
1422
  catch (error) {
1140
1423
  plugin.error = true;
@@ -1143,6 +1426,7 @@ export class Matterbridge extends EventEmitter {
1143
1426
  }
1144
1427
  this.frontend.wssSendRefreshRequired();
1145
1428
  }, 30 * 1000);
1429
+ // Setting reachability to true
1146
1430
  this.reachabilityTimeout = setTimeout(() => {
1147
1431
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1148
1432
  if (this.serverNode)
@@ -1153,7 +1437,14 @@ export class Matterbridge extends EventEmitter {
1153
1437
  }, 60 * 1000);
1154
1438
  }, 1000);
1155
1439
  }
1440
+ /**
1441
+ * Starts the Matterbridge in childbridge mode.
1442
+ * @private
1443
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1444
+ */
1156
1445
  async startChildbridge() {
1446
+ // Matterbridge.addBridgedDevice creates the commissionig servers and add the devices to the the commissioning server or to the aggregator
1447
+ // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1157
1448
  if (!this.matterStorageManager)
1158
1449
  throw new Error('No storage manager initialized');
1159
1450
  for (const plugin of this.plugins) {
@@ -1200,12 +1491,13 @@ export class Matterbridge extends EventEmitter {
1200
1491
  clearInterval(this.startMatterInterval);
1201
1492
  this.startMatterInterval = undefined;
1202
1493
  this.log.debug('Cleared startMatterInterval interval in childbridge mode');
1494
+ // Configure the plugins
1203
1495
  this.configureTimeout = setTimeout(async () => {
1204
1496
  for (const plugin of this.plugins) {
1205
1497
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1206
1498
  continue;
1207
1499
  try {
1208
- await this.plugins.configure(plugin);
1500
+ await this.plugins.configure(plugin); // TODO No await do it in parallel
1209
1501
  }
1210
1502
  catch (error) {
1211
1503
  plugin.error = true;
@@ -1233,7 +1525,9 @@ export class Matterbridge extends EventEmitter {
1233
1525
  this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1234
1526
  continue;
1235
1527
  }
1528
+ // Start the Matter server node
1236
1529
  this.startServerNode(plugin.serverNode);
1530
+ // Setting reachability to true
1237
1531
  plugin.reachabilityTimeout = setTimeout(() => {
1238
1532
  this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1239
1533
  if (plugin.serverNode)
@@ -1247,9 +1541,219 @@ export class Matterbridge extends EventEmitter {
1247
1541
  }
1248
1542
  }, 1000);
1249
1543
  }
1544
+ /**
1545
+ * Starts the Matterbridge controller.
1546
+ * @private
1547
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1548
+ */
1250
1549
  async startController() {
1550
+ /*
1551
+ if (!this.storageManager) {
1552
+ this.log.error('No storage manager initialized');
1553
+ await this.cleanup('No storage manager initialized');
1554
+ return;
1555
+ }
1556
+ this.log.info('Creating context: mattercontrollerContext');
1557
+ this.mattercontrollerContext = this.storageManager.createContext('mattercontrollerContext');
1558
+ if (!this.mattercontrollerContext) {
1559
+ this.log.error('No storage context mattercontrollerContext initialized');
1560
+ await this.cleanup('No storage context mattercontrollerContext initialized');
1561
+ return;
1562
+ }
1563
+
1564
+ this.log.debug('Starting matterbridge in mode', this.bridgeMode);
1565
+ this.matterServer = await this.createMatterServer(this.storageManager);
1566
+ this.log.info('Creating matter commissioning controller');
1567
+ this.commissioningController = new CommissioningController({
1568
+ autoConnect: false,
1569
+ });
1570
+ this.log.info('Adding matter commissioning controller to matter server');
1571
+ await this.matterServer.addCommissioningController(this.commissioningController);
1572
+
1573
+ this.log.info('Starting matter server');
1574
+ await this.matterServer.start();
1575
+ this.log.info('Matter server started');
1576
+
1577
+ if (hasParameter('pairingcode')) {
1578
+ this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
1579
+ const pairingCode = getParameter('pairingcode');
1580
+ const ip = this.mattercontrollerContext.has('ip') ? this.mattercontrollerContext.get<string>('ip') : undefined;
1581
+ const port = this.mattercontrollerContext.has('port') ? this.mattercontrollerContext.get<number>('port') : undefined;
1582
+
1583
+ let longDiscriminator, setupPin, shortDiscriminator;
1584
+ if (pairingCode !== undefined) {
1585
+ const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
1586
+ shortDiscriminator = pairingCodeCodec.shortDiscriminator;
1587
+ longDiscriminator = undefined;
1588
+ setupPin = pairingCodeCodec.passcode;
1589
+ this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
1590
+ } else {
1591
+ longDiscriminator = await this.mattercontrollerContext.get('longDiscriminator', 3840);
1592
+ if (longDiscriminator > 4095) throw new Error('Discriminator value must be less than 4096');
1593
+ setupPin = this.mattercontrollerContext.get('pin', 20202021);
1594
+ }
1595
+ if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
1596
+ throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
1597
+ }
1598
+
1599
+ const commissioningOptions: ControllerCommissioningFlowOptions = {
1600
+ regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
1601
+ regulatoryCountryCode: 'XX',
1602
+ };
1603
+ const options = {
1604
+ commissioning: commissioningOptions,
1605
+ discovery: {
1606
+ knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
1607
+ identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
1608
+ },
1609
+ passcode: setupPin,
1610
+ } as NodeCommissioningOptions;
1611
+ this.log.info('Commissioning with options:', options);
1612
+ const nodeId = await this.commissioningController.commissionNode(options);
1613
+ this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
1614
+ this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
1615
+ } // (hasParameter('pairingcode'))
1616
+
1617
+ if (hasParameter('unpairall')) {
1618
+ this.log.info('***Commissioning controller unpairing all nodes...');
1619
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1620
+ for (const nodeId of nodeIds) {
1621
+ this.log.info('***Commissioning controller unpairing node:', nodeId);
1622
+ await this.commissioningController.removeNode(nodeId);
1623
+ }
1624
+ return;
1625
+ }
1626
+
1627
+ if (hasParameter('discover')) {
1628
+ // const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
1629
+ // console.log(discover);
1630
+ }
1631
+
1632
+ if (!this.commissioningController.isCommissioned()) {
1633
+ this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
1634
+ return;
1635
+ }
1636
+
1637
+ const nodeIds = this.commissioningController.getCommissionedNodes();
1638
+ this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
1639
+ for (const nodeId of nodeIds) {
1640
+ this.log.info(`***Connecting to commissioned node: ${nodeId}`);
1641
+
1642
+ const node = await this.commissioningController.connectNode(nodeId, {
1643
+ autoSubscribe: false,
1644
+ attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) =>
1645
+ this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
1646
+ eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) =>
1647
+ this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
1648
+ stateInformationCallback: (peerNodeId, info) => {
1649
+ switch (info) {
1650
+ case NodeStateInformation.Connected:
1651
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
1652
+ break;
1653
+ case NodeStateInformation.Disconnected:
1654
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
1655
+ break;
1656
+ case NodeStateInformation.Reconnecting:
1657
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
1658
+ break;
1659
+ case NodeStateInformation.WaitingForDeviceDiscovery:
1660
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
1661
+ break;
1662
+ case NodeStateInformation.StructureChanged:
1663
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
1664
+ break;
1665
+ case NodeStateInformation.Decommissioned:
1666
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
1667
+ break;
1668
+ default:
1669
+ this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
1670
+ break;
1671
+ }
1672
+ },
1673
+ });
1674
+
1675
+ node.logStructure();
1676
+
1677
+ // Get the interaction client
1678
+ this.log.info('Getting the interaction client');
1679
+ const interactionClient = await node.getInteractionClient();
1680
+ let cluster;
1681
+ let attributes;
1682
+
1683
+ // Log BasicInformationCluster
1684
+ cluster = BasicInformationCluster;
1685
+ attributes = await interactionClient.getMultipleAttributes({
1686
+ attributes: [{ clusterId: cluster.id }],
1687
+ });
1688
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1689
+ attributes.forEach((attribute) => {
1690
+ this.log.info(
1691
+ `- 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}`,
1692
+ );
1693
+ });
1694
+
1695
+ // Log PowerSourceCluster
1696
+ cluster = PowerSourceCluster;
1697
+ attributes = await interactionClient.getMultipleAttributes({
1698
+ attributes: [{ clusterId: cluster.id }],
1699
+ });
1700
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1701
+ attributes.forEach((attribute) => {
1702
+ this.log.info(
1703
+ `- 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}`,
1704
+ );
1705
+ });
1706
+
1707
+ // Log ThreadNetworkDiagnostics
1708
+ cluster = ThreadNetworkDiagnosticsCluster;
1709
+ attributes = await interactionClient.getMultipleAttributes({
1710
+ attributes: [{ clusterId: cluster.id }],
1711
+ });
1712
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1713
+ attributes.forEach((attribute) => {
1714
+ this.log.info(
1715
+ `- 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}`,
1716
+ );
1717
+ });
1718
+
1719
+ // Log SwitchCluster
1720
+ cluster = SwitchCluster;
1721
+ attributes = await interactionClient.getMultipleAttributes({
1722
+ attributes: [{ clusterId: cluster.id }],
1723
+ });
1724
+ if (attributes.length > 0) this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1725
+ attributes.forEach((attribute) => {
1726
+ this.log.info(
1727
+ `- 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}`,
1728
+ );
1729
+ });
1730
+
1731
+ this.log.info('Subscribing to all attributes and events');
1732
+ await node.subscribeAllAttributesAndEvents({
1733
+ ignoreInitialTriggers: false,
1734
+ attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) =>
1735
+ this.log.info(
1736
+ `***${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}`,
1737
+ ),
1738
+ eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
1739
+ this.log.info(
1740
+ `***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`,
1741
+ );
1742
+ },
1743
+ });
1744
+ this.log.info('Subscribed to all attributes and events');
1745
+ }
1746
+ */
1251
1747
  }
1748
+ /** ***********************************************************************************************************************************/
1749
+ /** Matter.js methods */
1750
+ /** ***********************************************************************************************************************************/
1751
+ /**
1752
+ * Starts the matter storage process based on the specified storage type and name.
1753
+ * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1754
+ */
1252
1755
  async startMatterStorage() {
1756
+ // Setup Matter storage
1253
1757
  this.log.info(`Starting matter node storage...`);
1254
1758
  this.matterStorageService = this.environment.get(StorageService);
1255
1759
  this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
@@ -1257,13 +1761,24 @@ export class Matterbridge extends EventEmitter {
1257
1761
  this.log.info('Matter node storage manager "Matterbridge" created');
1258
1762
  this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, 'Matterbridge aggregator');
1259
1763
  this.log.info('Matter node storage started');
1764
+ // Backup matter storage since it is created/opened correctly
1260
1765
  await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
1261
1766
  }
1767
+ /**
1768
+ * Makes a backup copy of the specified matter storage directory.
1769
+ *
1770
+ * @param storageName - The name of the storage file to be backed up.
1771
+ * @param backupName - The name of the backup file to be created.
1772
+ */
1262
1773
  async backupMatterStorage(storageName, backupName) {
1263
1774
  this.log.info('Creating matter node storage backup...');
1264
1775
  await copyDirectory(storageName, backupName);
1265
1776
  this.log.info('Created matter node storage backup');
1266
1777
  }
1778
+ /**
1779
+ * Stops the matter storage.
1780
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
1781
+ */
1267
1782
  async stopMatterStorage() {
1268
1783
  this.log.info('Closing matter node storage...');
1269
1784
  this.matterStorageManager?.close();
@@ -1272,6 +1787,24 @@ export class Matterbridge extends EventEmitter {
1272
1787
  this.matterbridgeContext = undefined;
1273
1788
  this.log.info('Matter node storage closed');
1274
1789
  }
1790
+ /**
1791
+ * Creates a server node storage context.
1792
+ *
1793
+ * @param pluginName - The name of the plugin.
1794
+ * @param deviceName - The name of the device.
1795
+ * @param deviceType - The deviceType of the device.
1796
+ * @param vendorId - The vendor ID.
1797
+ * @param vendorName - The vendor name.
1798
+ * @param productId - The product ID.
1799
+ * @param productName - The product name.
1800
+ * @param serialNumber - The serial number of the device (optional).
1801
+ * @param uniqueId - The unique ID of the device (optional).
1802
+ * @param softwareVersion - The software version of the device (optional).
1803
+ * @param softwareVersionString - The software version string of the device (optional).
1804
+ * @param hardwareVersion - The hardware version of the device (optional).
1805
+ * @param hardwareVersionString - The hardware version string of the device (optional).
1806
+ * @returns The storage context for the commissioning server.
1807
+ */
1275
1808
  async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
1276
1809
  if (!this.matterStorageService)
1277
1810
  throw new Error('No storage service initialized');
@@ -1313,21 +1846,33 @@ export class Matterbridge extends EventEmitter {
1313
1846
  this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
1314
1847
  this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1315
1848
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1849
+ /**
1850
+ * Create a Matter ServerNode, which contains the Root Endpoint and all relevant data and configuration
1851
+ */
1316
1852
  const serverNode = await ServerNode.create({
1853
+ // Required: Give the Node a unique ID which is used to store the state of this node
1317
1854
  id: storeId,
1855
+ // Provide Network relevant configuration like the port
1856
+ // Optional when operating only one device on a host, Default port is 5540
1318
1857
  network: {
1319
1858
  listeningAddressIpv4: this.ipv4address,
1320
1859
  listeningAddressIpv6: this.ipv6address,
1321
1860
  port,
1322
1861
  },
1862
+ // Provide Commissioning relevant settings
1863
+ // Optional for development/testing purposes
1323
1864
  commissioning: {
1324
1865
  passcode,
1325
1866
  discriminator,
1326
1867
  },
1868
+ // Provide Node announcement settings
1869
+ // Optional: If Ommitted some development defaults are used
1327
1870
  productDescription: {
1328
1871
  name: await storageContext.get('deviceName'),
1329
1872
  deviceType: DeviceTypeId(await storageContext.get('deviceType')),
1330
1873
  },
1874
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
1875
+ // Optional: If Omitted some development defaults are used
1331
1876
  basicInformation: {
1332
1877
  vendorId: VendorId(await storageContext.get('vendorId')),
1333
1878
  vendorName: await storageContext.get('vendorName'),
@@ -1344,12 +1889,13 @@ export class Matterbridge extends EventEmitter {
1344
1889
  },
1345
1890
  });
1346
1891
  const sanitizeFabrics = (fabrics, resetSessions = false) => {
1892
+ // New type of fabric information: Record<FabricIndex, ExposedFabricInformation>
1347
1893
  const sanitizedFabrics = this.sanitizeFabricInformations(Array.from(Object.values(fabrics)));
1348
1894
  this.log.info(`Fabrics: ${debugStringify(sanitizedFabrics)}`);
1349
1895
  if (this.bridgeMode === 'bridge') {
1350
1896
  this.matterbridgeFabricInformations = sanitizedFabrics;
1351
1897
  if (resetSessions)
1352
- this.matterbridgeSessionInformations = [];
1898
+ this.matterbridgeSessionInformations = []; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
1353
1899
  this.matterbridgePaired = true;
1354
1900
  }
1355
1901
  if (this.bridgeMode === 'childbridge') {
@@ -1357,13 +1903,19 @@ export class Matterbridge extends EventEmitter {
1357
1903
  if (plugin) {
1358
1904
  plugin.fabricInformations = sanitizedFabrics;
1359
1905
  if (resetSessions)
1360
- plugin.sessionInformations = [];
1906
+ plugin.sessionInformations = []; // Changed cause Invoke Matterbridge.operationalCredentials.updateFabricLabel is sent after the session is created
1361
1907
  plugin.paired = true;
1362
1908
  }
1363
1909
  }
1364
1910
  };
1911
+ /**
1912
+ * This event is triggered when the device is initially commissioned successfully.
1913
+ * This means: It is added to the first fabric.
1914
+ */
1365
1915
  serverNode.lifecycle.commissioned.on(() => this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`));
1916
+ /** This event is triggered when all fabrics are removed from the device, usually it also does a factory reset then. */
1366
1917
  serverNode.lifecycle.decommissioned.on(() => this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`));
1918
+ /** This event is triggered when the device went online. This means that it is discoverable in the network. */
1367
1919
  serverNode.lifecycle.online.on(() => {
1368
1920
  this.log.notice(`Server node for ${storeId} is online`);
1369
1921
  if (!serverNode.lifecycle.isCommissioned) {
@@ -1397,6 +1949,7 @@ export class Matterbridge extends EventEmitter {
1397
1949
  }
1398
1950
  this.frontend.wssSendRefreshRequired();
1399
1951
  });
1952
+ /** This event is triggered when the device went offline. it is not longer discoverable or connectable in the network. */
1400
1953
  serverNode.lifecycle.offline.on(() => {
1401
1954
  this.log.notice(`Server node for ${storeId} is offline`);
1402
1955
  if (this.bridgeMode === 'bridge') {
@@ -1418,6 +1971,10 @@ export class Matterbridge extends EventEmitter {
1418
1971
  }
1419
1972
  this.frontend.wssSendRefreshRequired();
1420
1973
  });
1974
+ /**
1975
+ * This event is triggered when a fabric is added, removed or updated on the device. Use this if more granular
1976
+ * information is needed.
1977
+ */
1421
1978
  serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
1422
1979
  let action = '';
1423
1980
  switch (fabricAction) {
@@ -1451,16 +2008,24 @@ export class Matterbridge extends EventEmitter {
1451
2008
  }
1452
2009
  }
1453
2010
  };
2011
+ /**
2012
+ * This event is triggered when an operative new session was opened by a Controller.
2013
+ * It is not triggered for the initial commissioning process, just afterwards for real connections.
2014
+ */
1454
2015
  serverNode.events.sessions.opened.on((session) => {
1455
2016
  this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
1456
2017
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1457
2018
  this.frontend.wssSendRefreshRequired();
1458
2019
  });
2020
+ /**
2021
+ * This event is triggered when an operative session is closed by a Controller or because the Device goes offline.
2022
+ */
1459
2023
  serverNode.events.sessions.closed.on((session) => {
1460
2024
  this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
1461
2025
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1462
2026
  this.frontend.wssSendRefreshRequired();
1463
2027
  });
2028
+ /** This event is triggered when a subscription gets added or removed on an operative session. */
1464
2029
  serverNode.events.sessions.subscriptionsChanged.on((session) => {
1465
2030
  this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
1466
2031
  sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
@@ -1487,11 +2052,13 @@ export class Matterbridge extends EventEmitter {
1487
2052
  return aggregatorNode;
1488
2053
  }
1489
2054
  async addBridgedEndpoint(pluginName, device) {
2055
+ // Check if the plugin is registered
1490
2056
  const plugin = this.plugins.get(pluginName);
1491
2057
  if (!plugin) {
1492
2058
  this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
1493
2059
  return;
1494
2060
  }
2061
+ // Register and add the device to the matterbridge aggregator node
1495
2062
  if (this.bridgeMode === 'bridge') {
1496
2063
  this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
1497
2064
  if (!this.aggregatorNode)
@@ -1514,16 +2081,19 @@ export class Matterbridge extends EventEmitter {
1514
2081
  plugin.registeredDevices++;
1515
2082
  if (plugin.addedDevices !== undefined)
1516
2083
  plugin.addedDevices++;
2084
+ // Add the device to the DeviceManager
1517
2085
  this.devices.set(device);
1518
2086
  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}`);
1519
2087
  }
1520
2088
  async removeBridgedEndpoint(pluginName, device) {
1521
2089
  this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
2090
+ // Check if the plugin is registered
1522
2091
  const plugin = this.plugins.get(pluginName);
1523
2092
  if (!plugin) {
1524
2093
  this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1525
2094
  return;
1526
2095
  }
2096
+ // Register and add the device to the matterbridge aggregator node
1527
2097
  if (this.bridgeMode === 'bridge') {
1528
2098
  if (!this.aggregatorNode) {
1529
2099
  this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
@@ -1538,6 +2108,7 @@ export class Matterbridge extends EventEmitter {
1538
2108
  }
1539
2109
  else if (this.bridgeMode === 'childbridge') {
1540
2110
  if (plugin.type === 'AccessoryPlatform') {
2111
+ // Nothing to do here since the server node has no aggregator node but only the device itself
1541
2112
  }
1542
2113
  else if (plugin.type === 'DynamicPlatform') {
1543
2114
  if (!plugin.aggregatorNode) {
@@ -1551,6 +2122,7 @@ export class Matterbridge extends EventEmitter {
1551
2122
  plugin.registeredDevices--;
1552
2123
  if (plugin.addedDevices !== undefined)
1553
2124
  plugin.addedDevices--;
2125
+ // Close the server node TODO check if this is correct
1554
2126
  if (plugin.registeredDevices === 0 && plugin.addedDevices === 0) {
1555
2127
  if (plugin.serverNode) {
1556
2128
  await this.stopServerNode(plugin.serverNode);
@@ -1561,6 +2133,7 @@ export class Matterbridge extends EventEmitter {
1561
2133
  }
1562
2134
  }
1563
2135
  }
2136
+ // Remove the device from the DeviceManager
1564
2137
  this.devices.remove(device);
1565
2138
  }
1566
2139
  async removeAllBridgedEndpoints(pluginName) {
@@ -1569,6 +2142,12 @@ export class Matterbridge extends EventEmitter {
1569
2142
  await this.removeBridgedEndpoint(pluginName, device);
1570
2143
  }
1571
2144
  }
2145
+ /**
2146
+ * Sanitizes the fabric information by converting bigint properties to string cause res.json doesn't know bigint.
2147
+ *
2148
+ * @param fabricInfo - The array of exposed fabric information objects.
2149
+ * @returns An array of sanitized exposed fabric information objects.
2150
+ */
1572
2151
  sanitizeFabricInformations(fabricInfo) {
1573
2152
  return fabricInfo.map((info) => {
1574
2153
  return {
@@ -1582,6 +2161,12 @@ export class Matterbridge extends EventEmitter {
1582
2161
  };
1583
2162
  });
1584
2163
  }
2164
+ /**
2165
+ * Sanitizes the session information by converting bigint properties to string.
2166
+ *
2167
+ * @param sessionInfo - The array of session information objects.
2168
+ * @returns An array of sanitized session information objects.
2169
+ */
1585
2170
  sanitizeSessionInformation(sessionInfo) {
1586
2171
  return sessionInfo
1587
2172
  .filter((session) => session.isPeerActive)
@@ -1609,11 +2194,51 @@ export class Matterbridge extends EventEmitter {
1609
2194
  };
1610
2195
  });
1611
2196
  }
2197
+ /**
2198
+ * Sets the reachability of a matter server node and trigger ReachableChanged event.
2199
+ *
2200
+ * @param {ServerNode<ServerNode.RootEndpoint>} serverNode - The commissioning server to set the reachability for.
2201
+ * @param {boolean} reachable - The new reachability status.
2202
+ */
2203
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1612
2204
  setServerNodeReachability(serverNode, reachable) {
2205
+ /*
2206
+ const basicInformationCluster = commissioningServer?.getRootClusterServer(BasicInformationCluster);
2207
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined) basicInformationCluster.setReachableAttribute(reachable);
2208
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent) basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2209
+ */
1613
2210
  }
2211
+ /**
2212
+ * Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
2213
+ * @param {EndpointNode<AggregatorEndpoint>} aggregatorNode - The matter aggregator to set the reachability for.
2214
+ * @param {boolean} reachable - A boolean indicating the reachability status to set.
2215
+ */
2216
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1614
2217
  setAggregatorReachability(aggregatorNode, reachable) {
2218
+ /*
2219
+ const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
2220
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined) basicInformationCluster.setReachableAttribute(reachable);
2221
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent) basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2222
+ matterAggregator.getBridgedDevices().forEach((device) => {
2223
+ this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
2224
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
2225
+ device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
2226
+ });
2227
+ */
1615
2228
  }
2229
+ /**
2230
+ * Sets the reachability of a device and trigger.
2231
+ *
2232
+ * @param {MatterbridgeEndpoint} device - The device to set the reachability for.
2233
+ * @param {boolean} reachable - The new reachability status of the device.
2234
+ */
2235
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1616
2236
  setDeviceReachability(device, reachable) {
2237
+ /*
2238
+ const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
2239
+ if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined) basicInformationCluster.setReachableAttribute(reachable);
2240
+ if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent) basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2241
+ */
1617
2242
  }
1618
2243
  getVendorIdName = (vendorId) => {
1619
2244
  if (!vendorId)
@@ -1656,13 +2281,36 @@ export class Matterbridge extends EventEmitter {
1656
2281
  }
1657
2282
  return vendorName;
1658
2283
  };
2284
+ /**
2285
+ * Spawns a child process with the given command and arguments.
2286
+ * @param {string} command - The command to execute.
2287
+ * @param {string[]} args - The arguments to pass to the command (default: []).
2288
+ * @returns {Promise<boolean>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
2289
+ */
1659
2290
  async spawnCommand(command, args = []) {
2291
+ /*
2292
+ npm > npm.cmd on windows
2293
+ cmd.exe ['dir'] on windows
2294
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2295
+ process.on('unhandledRejection', (reason, promise) => {
2296
+ this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
2297
+ });
2298
+
2299
+ spawn - [14:27:21.125] [Matterbridge:spawn]: changed 38 packages in 4s
2300
+ spawn - [14:27:21.125] [Matterbridge:spawn]: 10 packages are looking for funding run `npm fund` for details
2301
+ debug - [14:27:21.131] [Matterbridge]: Child process exited with code 0 and signal null
2302
+ debug - [14:27:21.131] [Matterbridge]: Child process stdio streams have closed with code 0
2303
+ */
1660
2304
  const cmdLine = command + ' ' + args.join(' ');
1661
2305
  if (process.platform === 'win32' && command === 'npm') {
2306
+ // Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
1662
2307
  const argstring = 'npm ' + args.join(' ');
1663
2308
  args.splice(0, args.length, '/c', argstring);
1664
2309
  command = 'cmd.exe';
1665
2310
  }
2311
+ // Decide when using sudo on linux
2312
+ // When you need sudo: Spawn stderr: npm error Error: EACCES: permission denied
2313
+ // When you don't need sudo: Failed to start child process "npm install -g matterbridge-eve-door": spawn sudo ENOENT
1666
2314
  if (hasParameter('sudo') || (process.platform === 'linux' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo'))) {
1667
2315
  args.unshift(command);
1668
2316
  command = 'sudo';
@@ -1721,3 +2369,4 @@ export class Matterbridge extends EventEmitter {
1721
2369
  });
1722
2370
  }
1723
2371
  }
2372
+ //# sourceMappingURL=matterbridge.js.map