matterbridge 1.7.3 → 2.0.0-edge.3

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 (102) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/cli.js +3 -39
  3. package/dist/cluster/export.js +0 -2
  4. package/dist/defaultConfigSchema.js +0 -23
  5. package/dist/deviceManager.js +1 -26
  6. package/dist/frontend.js +1199 -0
  7. package/dist/index.js +1 -36
  8. package/dist/logger/export.js +0 -1
  9. package/dist/matter/export.js +0 -5
  10. package/dist/matterbridge.js +521 -2681
  11. package/dist/matterbridgeAccessoryPlatform.js +0 -33
  12. package/dist/matterbridgeBehaviors.js +23 -31
  13. package/dist/matterbridgeDeviceTypes.js +11 -82
  14. package/dist/matterbridgeDynamicPlatform.js +0 -33
  15. package/dist/matterbridgeEndpoint.js +45 -1145
  16. package/dist/matterbridgePlatform.js +14 -145
  17. package/dist/matterbridgeTypes.js +3 -24
  18. package/dist/pluginManager.js +5 -247
  19. package/dist/storage/export.js +0 -1
  20. package/dist/utils/colorUtils.js +2 -205
  21. package/dist/utils/export.js +0 -1
  22. package/dist/utils/utils.js +7 -252
  23. package/frontend/build/asset-manifest.json +3 -3
  24. package/frontend/build/index.html +1 -1
  25. package/frontend/build/static/js/{main.6bbd1772.js → main.6df4ebe4.js} +3 -3
  26. package/frontend/build/static/js/main.6df4ebe4.js.map +1 -0
  27. package/npm-shrinkwrap.json +9 -9
  28. package/package.json +2 -3
  29. package/dist/cli.d.ts +0 -25
  30. package/dist/cli.d.ts.map +0 -1
  31. package/dist/cli.js.map +0 -1
  32. package/dist/cluster/export.d.ts +0 -2
  33. package/dist/cluster/export.d.ts.map +0 -1
  34. package/dist/cluster/export.js.map +0 -1
  35. package/dist/defaultConfigSchema.d.ts +0 -27
  36. package/dist/defaultConfigSchema.d.ts.map +0 -1
  37. package/dist/defaultConfigSchema.js.map +0 -1
  38. package/dist/deviceManager.d.ts +0 -46
  39. package/dist/deviceManager.d.ts.map +0 -1
  40. package/dist/deviceManager.js.map +0 -1
  41. package/dist/index.d.ts +0 -40
  42. package/dist/index.d.ts.map +0 -1
  43. package/dist/index.js.map +0 -1
  44. package/dist/logger/export.d.ts +0 -2
  45. package/dist/logger/export.d.ts.map +0 -1
  46. package/dist/logger/export.js.map +0 -1
  47. package/dist/matter/export.d.ts +0 -11
  48. package/dist/matter/export.d.ts.map +0 -1
  49. package/dist/matter/export.js.map +0 -1
  50. package/dist/matterbridge.d.ts +0 -483
  51. package/dist/matterbridge.d.ts.map +0 -1
  52. package/dist/matterbridge.js.map +0 -1
  53. package/dist/matterbridgeAccessoryPlatform.d.ts +0 -39
  54. package/dist/matterbridgeAccessoryPlatform.d.ts.map +0 -1
  55. package/dist/matterbridgeAccessoryPlatform.js.map +0 -1
  56. package/dist/matterbridgeBehaviors.d.ts +0 -942
  57. package/dist/matterbridgeBehaviors.d.ts.map +0 -1
  58. package/dist/matterbridgeBehaviors.js.map +0 -1
  59. package/dist/matterbridgeDevice.d.ts +0 -7077
  60. package/dist/matterbridgeDevice.d.ts.map +0 -1
  61. package/dist/matterbridgeDevice.js +0 -2736
  62. package/dist/matterbridgeDevice.js.map +0 -1
  63. package/dist/matterbridgeDeviceTypes.d.ts +0 -109
  64. package/dist/matterbridgeDeviceTypes.d.ts.map +0 -1
  65. package/dist/matterbridgeDeviceTypes.js.map +0 -1
  66. package/dist/matterbridgeDynamicPlatform.d.ts +0 -39
  67. package/dist/matterbridgeDynamicPlatform.d.ts.map +0 -1
  68. package/dist/matterbridgeDynamicPlatform.js.map +0 -1
  69. package/dist/matterbridgeEdge.d.ts +0 -91
  70. package/dist/matterbridgeEdge.d.ts.map +0 -1
  71. package/dist/matterbridgeEdge.js +0 -1077
  72. package/dist/matterbridgeEdge.js.map +0 -1
  73. package/dist/matterbridgeEndpoint.d.ts +0 -10156
  74. package/dist/matterbridgeEndpoint.d.ts.map +0 -1
  75. package/dist/matterbridgeEndpoint.js.map +0 -1
  76. package/dist/matterbridgePlatform.d.ts +0 -168
  77. package/dist/matterbridgePlatform.d.ts.map +0 -1
  78. package/dist/matterbridgePlatform.js.map +0 -1
  79. package/dist/matterbridgeTypes.d.ts +0 -172
  80. package/dist/matterbridgeTypes.d.ts.map +0 -1
  81. package/dist/matterbridgeTypes.js.map +0 -1
  82. package/dist/matterbridgeWebsocket.d.ts +0 -49
  83. package/dist/matterbridgeWebsocket.d.ts.map +0 -1
  84. package/dist/matterbridgeWebsocket.js +0 -325
  85. package/dist/matterbridgeWebsocket.js.map +0 -1
  86. package/dist/pluginManager.d.ts +0 -238
  87. package/dist/pluginManager.d.ts.map +0 -1
  88. package/dist/pluginManager.js.map +0 -1
  89. package/dist/storage/export.d.ts +0 -2
  90. package/dist/storage/export.d.ts.map +0 -1
  91. package/dist/storage/export.js.map +0 -1
  92. package/dist/utils/colorUtils.d.ts +0 -61
  93. package/dist/utils/colorUtils.d.ts.map +0 -1
  94. package/dist/utils/colorUtils.js.map +0 -1
  95. package/dist/utils/export.d.ts +0 -3
  96. package/dist/utils/export.d.ts.map +0 -1
  97. package/dist/utils/export.js.map +0 -1
  98. package/dist/utils/utils.d.ts +0 -221
  99. package/dist/utils/utils.d.ts.map +0 -1
  100. package/dist/utils/utils.js.map +0 -1
  101. package/frontend/build/static/js/main.6bbd1772.js.map +0 -1
  102. /package/frontend/build/static/js/{main.6bbd1772.js.LICENSE.txt → main.6df4ebe4.js.LICENSE.txt} +0 -0
@@ -1,64 +1,24 @@
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
24
1
  import { fileURLToPath } from 'url';
25
2
  import { promises as fs } from 'fs';
26
3
  import { exec, spawn } from 'child_process';
27
- import { createServer } from 'http';
28
4
  import EventEmitter from 'events';
29
5
  import os from 'os';
30
6
  import path from 'path';
31
7
  import { randomBytes } from 'crypto';
32
- // Package modules
33
- import https from 'https';
34
- import express from 'express';
35
- import WebSocket, { WebSocketServer } from 'ws';
36
- // NodeStorage and AnsiLogger modules
37
8
  import { NodeStorageManager } from 'node-persist-manager';
38
- import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt, idn, or, hk, BLUE } from 'node-ansi-logger';
39
- // Matterbridge
40
- import { MatterbridgeDevice } from './matterbridgeDevice.js';
41
- import { WS_ID_LOG, WS_ID_REFRESH_NEEDED, WS_ID_RESTART_NEEDED, wsMessageHandler } from './matterbridgeWebsocket.js';
42
- import { logInterfaces, wait, waiter, createZip, copyDirectory, getParameter, getIntParameter, hasParameter } from './utils/utils.js';
9
+ import { AnsiLogger, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, BRIGHT, RESET, er, nf, rs, wr, RED, GREEN, zb, CYAN, nt } from 'node-ansi-logger';
10
+ import { logInterfaces, wait, waiter, copyDirectory, getParameter, getIntParameter, hasParameter } from './utils/utils.js';
43
11
  import { PluginManager } from './pluginManager.js';
44
12
  import { DeviceManager } from './deviceManager.js';
45
13
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
46
- // @matter
47
- import { DeviceTypeId, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageManager, EndpointServer, StorageService, Environment } from '@matter/main';
48
- import { BasicInformationCluster, BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster, FixedLabelCluster, GeneralCommissioning, PowerSourceCluster, SwitchCluster, ThreadNetworkDiagnosticsCluster, UserLabelCluster, } from '@matter/main/clusters';
49
- import { getClusterNameById, ManualPairingCodeCodec, QrCodeSchema } from '@matter/main/types';
50
- import { StorageBackendDisk, StorageBackendJsonFile } from '@matter/nodejs';
51
- // @project-chip
52
- import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter.js';
53
- import { Aggregator, DeviceTypes, NodeStateInformation } from '@project-chip/matter.js/device';
54
- import { aggregator } from './matterbridgeDeviceTypes.js';
55
- // Default colors
14
+ import { bridge } from './matterbridgeDeviceTypes.js';
15
+ import { DeviceTypeId, Endpoint as EndpointNode, Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, VendorId, StorageService, Environment, ServerNode } from '@matter/main';
16
+ import { FabricAction, PaseClient } from '@matter/main/protocol';
17
+ import { AggregatorEndpoint } from '@matter/main/endpoints';
18
+ import { Frontend } from './frontend.js';
56
19
  const plg = '\u001B[38;5;33m';
57
20
  const dev = '\u001B[38;5;79m';
58
21
  const typ = '\u001B[38;5;207m';
59
- /**
60
- * Represents the Matterbridge application.
61
- */
62
22
  export class Matterbridge extends EventEmitter {
63
23
  systemInformation = {
64
24
  interfaceName: '',
@@ -89,13 +49,11 @@ export class Matterbridge extends EventEmitter {
89
49
  matterbridgeFabricInformations: [],
90
50
  matterbridgeSessionInformations: [],
91
51
  matterbridgePaired: false,
92
- matterbridgeConnected: false,
93
52
  bridgeMode: '',
94
53
  restartMode: '',
95
- edge: hasParameter('edge'),
96
54
  readOnly: hasParameter('readonly'),
97
55
  profile: getParameter('profile'),
98
- loggerLevel: "info" /* LogLevel.INFO */,
56
+ loggerLevel: "info",
99
57
  fileLogger: false,
100
58
  matterLoggerLevel: MatterLogLevel.INFO,
101
59
  matterFileLogger: false,
@@ -120,21 +78,19 @@ export class Matterbridge extends EventEmitter {
120
78
  matterbridgeFabricInformations = [];
121
79
  matterbridgeSessionInformations = [];
122
80
  matterbridgePaired = false;
123
- matterbridgeConnected = false;
124
81
  bridgeMode = '';
125
82
  restartMode = '';
126
83
  profile = getParameter('profile');
127
- edge = hasParameter('edge');
84
+ edge = true;
128
85
  log;
129
86
  matterbrideLoggerFile = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log';
130
87
  matterLoggerFile = 'matter' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.log';
131
88
  plugins;
132
89
  devices;
90
+ frontend = new Frontend(this);
133
91
  nodeStorage;
134
92
  nodeContext;
135
- matterStorageName = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json';
136
93
  nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
137
- // Cleanup
138
94
  hasCleanupStarted = false;
139
95
  initialized = false;
140
96
  execRunningCount = 0;
@@ -146,62 +102,34 @@ export class Matterbridge extends EventEmitter {
146
102
  sigtermHandler;
147
103
  exceptionHandler;
148
104
  rejectionHandler;
149
- // Frontend
150
- expressApp;
151
- httpServer;
152
- httpsServer;
153
- webSocketServer;
154
- // Matter
155
- mdnsInterface; // matter server mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
156
- ipv4address; // matter commissioning server listeningAddressIpv4
157
- ipv6address; // matter commissioning server listeningAddressIpv6
158
- port = 5540; // first commissioning server port
159
- passcode; // first commissioning server passcode
160
- discriminator; // first commissioning server discriminator
161
- storageManager;
105
+ environment = Environment.default;
106
+ matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
107
+ matterStorageService;
108
+ matterStorageManager;
162
109
  matterbridgeContext;
163
110
  mattercontrollerContext;
164
- matterServer;
165
- matterAggregator;
166
- commissioningServer;
167
- commissioningController;
111
+ mdnsInterface;
112
+ ipv4address;
113
+ ipv6address;
114
+ port;
115
+ passcode;
116
+ discriminator;
117
+ serverNode;
118
+ aggregatorNode;
168
119
  aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
169
120
  aggregatorProductId = getIntParameter('productId') ?? 0x8000;
170
121
  static instance;
171
- // We load asyncronously so is private
172
122
  constructor() {
173
123
  super();
174
- // Bind the handler to the instance
175
- this.matterbridgeMessageHandler = wsMessageHandler.bind(this);
176
124
  }
177
- /**
178
- * Retrieves the list of Matterbridge devices.
179
- * @returns {MatterbridgeDevice[]} An array of MatterbridgeDevice objects.
180
- */
181
125
  getDevices() {
182
126
  return this.devices.array();
183
127
  }
184
- /**
185
- * Retrieves the list of registered plugins.
186
- * @returns {RegisteredPlugin[]} An array of RegisteredPlugin objects.
187
- */
188
128
  getPlugins() {
189
129
  return this.plugins.array();
190
130
  }
191
- matterbridgeMessageHandler;
192
- /** ***********************************************************************************************************************************/
193
- /** loadInstance() and cleanup() methods */
194
- /** ***********************************************************************************************************************************/
195
- /**
196
- * Loads an instance of the Matterbridge class.
197
- * If an instance already exists, return that instance.
198
- *
199
- * @param initialize - Whether to initialize the Matterbridge instance after loading.
200
- * @returns The loaded Matterbridge instance.
201
- */
202
131
  static async loadInstance(initialize = false) {
203
132
  if (!Matterbridge.instance) {
204
- // eslint-disable-next-line no-console
205
133
  if (hasParameter('debug'))
206
134
  console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
207
135
  Matterbridge.instance = new Matterbridge();
@@ -210,11 +138,6 @@ export class Matterbridge extends EventEmitter {
210
138
  }
211
139
  return Matterbridge.instance;
212
140
  }
213
- /**
214
- * Call cleanup().
215
- * @deprecated This method is deprecated and is only used for jest tests.
216
- *
217
- */
218
141
  async destroyInstance() {
219
142
  await this.cleanup('destroying instance...', false);
220
143
  await waiter('destroying instance...', () => {
@@ -222,60 +145,45 @@ export class Matterbridge extends EventEmitter {
222
145
  }, false, 60000, 100, false);
223
146
  await wait(1000, 'Wait for the global node_modules and matterbridge version', false);
224
147
  }
225
- /**
226
- * Initializes the Matterbridge application.
227
- *
228
- * @remarks
229
- * This method performs the necessary setup and initialization steps for the Matterbridge application.
230
- * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
231
- * node version, registers signal handlers, initializes storage, and parses the command line.
232
- *
233
- * @returns A Promise that resolves when the initialization is complete.
234
- */
235
148
  async initialize() {
236
- // Set the restart mode
237
149
  if (hasParameter('service'))
238
150
  this.restartMode = 'service';
239
151
  if (hasParameter('docker'))
240
152
  this.restartMode = 'docker';
241
- // Set the matterbridge directory
242
153
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
243
154
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
244
- // Create matterbridge logger
245
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: hasParameter('debug') ? "debug" /* LogLevel.DEBUG */ : "info" /* LogLevel.INFO */ });
246
- // Initialize nodeStorage and nodeContext
155
+ this.environment.vars.set('log.level', MatterLogLevel.INFO);
156
+ this.environment.vars.set('log.format', MatterLogFormat.ANSI);
157
+ this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, this.matterStorageName));
158
+ this.environment.vars.set('runtime.signals', false);
159
+ this.environment.vars.set('runtime.exitcode', false);
160
+ this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
161
+ this.registerProcessHandlers();
247
162
  try {
248
163
  this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
249
164
  this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
250
165
  this.log.debug('Creating node storage context for matterbridge');
251
166
  this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
252
- // TODO: Remove this code when node-persist-manager is updated
253
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
254
167
  const keys = (await this.nodeStorage?.storage.keys());
255
168
  for (const key of keys) {
256
169
  this.log.debug(`Checking node storage manager key: ${CYAN}${key}${db}`);
257
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
170
  await this.nodeStorage?.storage.get(key);
259
171
  }
260
172
  const storages = await this.nodeStorage.getStorageNames();
261
173
  for (const storage of storages) {
262
174
  this.log.debug(`Checking storage: ${CYAN}${storage}${db}`);
263
175
  const nodeContext = await this.nodeStorage?.createStorage(storage);
264
- // TODO: Remove this code when node-persist-manager is updated
265
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
266
176
  const keys = (await nodeContext?.storage.keys());
267
177
  keys.forEach(async (key) => {
268
178
  this.log.debug(`Checking key: ${CYAN}${storage}:${key}${db}`);
269
179
  await nodeContext?.get(key);
270
180
  });
271
181
  }
272
- // Creating a backup of the node storage since it is not corrupted
273
182
  this.log.debug('Creating node storage backup...');
274
183
  await copyDirectory(path.join(this.matterbridgeDirectory, this.nodeStorageName), path.join(this.matterbridgeDirectory, this.nodeStorageName + '.backup'));
275
184
  this.log.debug('Created node storage backup');
276
185
  }
277
186
  catch (error) {
278
- // Restoring the backup of the node storage since it is corrupted
279
187
  this.log.error(`Error creating node storage manager and context: ${error instanceof Error ? error.message : error}`);
280
188
  if (hasParameter('norestore')) {
281
189
  this.log.fatal(`The matterbridge node storage is corrupted. Parameter -norestore found: exiting...`);
@@ -290,51 +198,45 @@ export class Matterbridge extends EventEmitter {
290
198
  this.log.fatal('Fatal error creating node storage manager and context for matterbridge');
291
199
  throw new Error('Fatal error creating node storage manager and context for matterbridge');
292
200
  }
293
- // Set the first port to use for the commissioning server (will be incremented in childbridge mode)
294
201
  this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
295
- // Set the first passcode to use for the commissioning server (will be incremented in childbridge mode)
296
- this.passcode = this.passcode ?? getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode'));
297
- // Set the first discriminator to use for the commissioning server (will be incremented in childbridge mode)
298
- this.discriminator = this.discriminator ?? getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator'));
202
+ this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
203
+ this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
299
204
  this.log.debug(`Initializing commissioning server for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
300
- // Set matterbridge logger level (context: matterbridgeLogLevel)
301
205
  if (hasParameter('logger')) {
302
206
  const level = getParameter('logger');
303
207
  if (level === 'debug') {
304
- this.log.logLevel = "debug" /* LogLevel.DEBUG */;
208
+ this.log.logLevel = "debug";
305
209
  }
306
210
  else if (level === 'info') {
307
- this.log.logLevel = "info" /* LogLevel.INFO */;
211
+ this.log.logLevel = "info";
308
212
  }
309
213
  else if (level === 'notice') {
310
- this.log.logLevel = "notice" /* LogLevel.NOTICE */;
214
+ this.log.logLevel = "notice";
311
215
  }
312
216
  else if (level === 'warn') {
313
- this.log.logLevel = "warn" /* LogLevel.WARN */;
217
+ this.log.logLevel = "warn";
314
218
  }
315
219
  else if (level === 'error') {
316
- this.log.logLevel = "error" /* LogLevel.ERROR */;
220
+ this.log.logLevel = "error";
317
221
  }
318
222
  else if (level === 'fatal') {
319
- this.log.logLevel = "fatal" /* LogLevel.FATAL */;
223
+ this.log.logLevel = "fatal";
320
224
  }
321
225
  else {
322
226
  this.log.warn(`Invalid matterbridge logger level: ${level}. Using default level "info".`);
323
- this.log.logLevel = "info" /* LogLevel.INFO */;
227
+ this.log.logLevel = "info";
324
228
  }
325
229
  }
326
230
  else {
327
- this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', "info" /* LogLevel.INFO */);
231
+ this.log.logLevel = await this.nodeContext.get('matterbridgeLogLevel', "info");
328
232
  }
329
- MatterbridgeDevice.logLevel = this.log.logLevel;
330
- // Create the file logger for matterbridge (context: matterbridgeFileLog)
233
+ MatterbridgeEndpoint.logLevel = this.log.logLevel;
331
234
  if (hasParameter('filelogger') || (await this.nodeContext.get('matterbridgeFileLog', false))) {
332
235
  AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), this.log.logLevel, true);
333
236
  this.matterbridgeInformation.fileLogger = true;
334
237
  }
335
238
  this.log.notice('Matterbridge is starting...');
336
239
  this.log.debug(`Matterbridge logLevel: ${this.log.logLevel} fileLoger: ${this.matterbridgeInformation.fileLogger}.`);
337
- // Set matter.js logger level, format and logger (context: matterLogLevel)
338
240
  if (hasParameter('matterlogger')) {
339
241
  const level = getParameter('matterlogger');
340
242
  if (level === 'debug') {
@@ -365,7 +267,6 @@ export class Matterbridge extends EventEmitter {
365
267
  }
366
268
  Logger.format = MatterLogFormat.ANSI;
367
269
  Logger.setLogger('default', this.createMatterLogger());
368
- // Create the file logger for matter.js (context: matterFileLog)
369
270
  if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
370
271
  this.matterbridgeInformation.matterFileLogger = true;
371
272
  Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
@@ -374,25 +275,35 @@ export class Matterbridge extends EventEmitter {
374
275
  });
375
276
  }
376
277
  this.log.debug(`Matter logLevel: ${Logger.defaultLogLevel} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
377
- // Set the interface to use for the matter server mdnsInterface
378
278
  if (hasParameter('mdnsinterface')) {
379
279
  this.mdnsInterface = getParameter('mdnsinterface');
380
280
  }
381
281
  else {
382
- this.mdnsInterface = await this.nodeContext?.get('mattermdnsinterface', undefined);
282
+ this.mdnsInterface = await this.nodeContext.get('mattermdnsinterface', undefined);
383
283
  if (this.mdnsInterface === '')
384
284
  this.mdnsInterface = undefined;
385
285
  }
386
- // Set the listeningAddressIpv4 for the matter commissioning server
286
+ if (this.mdnsInterface) {
287
+ const networkInterfaces = os.networkInterfaces();
288
+ const availableInterfaces = Object.keys(networkInterfaces);
289
+ if (!availableInterfaces.includes(this.mdnsInterface)) {
290
+ this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
291
+ this.mdnsInterface = undefined;
292
+ }
293
+ else {
294
+ this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
295
+ }
296
+ }
297
+ if (this.mdnsInterface)
298
+ this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
387
299
  if (hasParameter('ipv4address')) {
388
300
  this.ipv4address = getParameter('ipv4address');
389
301
  }
390
302
  else {
391
- this.ipv4address = await this.nodeContext?.get('matteripv4address', undefined);
303
+ this.ipv4address = await this.nodeContext.get('matteripv4address', undefined);
392
304
  if (this.ipv4address === '')
393
305
  this.ipv4address = undefined;
394
306
  }
395
- // Set the listeningAddressIpv6 for the matter commissioning server
396
307
  if (hasParameter('ipv6address')) {
397
308
  this.ipv6address = getParameter('ipv6address');
398
309
  }
@@ -401,28 +312,22 @@ export class Matterbridge extends EventEmitter {
401
312
  if (this.ipv6address === '')
402
313
  this.ipv6address = undefined;
403
314
  }
404
- // Initialize PluginManager
405
315
  this.plugins = new PluginManager(this);
406
316
  await this.plugins.loadFromStorage();
407
- // Initialize DeviceManager
408
317
  this.devices = new DeviceManager(this, this.nodeContext);
409
- // Get the plugins from node storage and create the plugins node storage contexts
410
318
  for (const plugin of this.plugins) {
411
319
  const packageJson = await this.plugins.parse(plugin);
412
- if (packageJson === null && !hasParameter('add')) {
413
- // Try to reinstall the plugin from npm (for Docker pull and external plugins)
414
- // We don't do this when the add parameter is set because we shut down the process after adding the plugin
320
+ if (packageJson === null && !hasParameter('add') && !hasParameter('remove') && !hasParameter('enable') && !hasParameter('disable') && !hasParameter('reset') && !hasParameter('factoryreset')) {
415
321
  this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
416
322
  try {
417
323
  await this.spawnCommand('npm', ['install', '-g', plugin.name, '--omit=dev', '--verbose']);
418
324
  this.log.info(`Plugin ${plg}${plugin.name}${nf} reinstalled.`);
419
325
  plugin.error = false;
420
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
421
326
  }
422
327
  catch (error) {
423
328
  plugin.error = true;
424
329
  plugin.enabled = false;
425
- this.log.error(`Error installing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`);
330
+ this.log.error(`Error installing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`, error instanceof Error ? error.message : error);
426
331
  }
427
332
  }
428
333
  this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
@@ -434,7 +339,6 @@ export class Matterbridge extends EventEmitter {
434
339
  await plugin.nodeContext.set('description', plugin.description);
435
340
  await plugin.nodeContext.set('author', plugin.author);
436
341
  }
437
- // Log system info and create .matterbridge directory
438
342
  await this.logNodeAndSystemInfo();
439
343
  this.log.notice(`Matterbridge version ${this.matterbridgeVersion} ` +
440
344
  `${hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge') ? 'mode bridge ' : ''}` +
@@ -442,7 +346,6 @@ export class Matterbridge extends EventEmitter {
442
346
  `${hasParameter('controller') ? 'mode controller ' : ''}` +
443
347
  `${this.restartMode !== '' ? 'restart mode ' + this.restartMode + ' ' : ''}` +
444
348
  `running on ${this.systemInformation.osType} (v.${this.systemInformation.osRelease}) platform ${this.systemInformation.osPlatform} arch ${this.systemInformation.osArch}`);
445
- // Check node version and throw error
446
349
  const minNodeVersion = 18;
447
350
  const nodeVersion = process.versions.node;
448
351
  const versionMajor = parseInt(nodeVersion.split('.')[0]);
@@ -450,17 +353,9 @@ export class Matterbridge extends EventEmitter {
450
353
  this.log.error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
451
354
  throw new Error(`Node version ${versionMajor} is not supported. Please upgrade to ${minNodeVersion} or above.`);
452
355
  }
453
- // Register process handlers
454
- this.registerProcessHandlers();
455
- // Parse command line
456
356
  await this.parseCommandLine();
457
357
  this.initialized = true;
458
358
  }
459
- /**
460
- * Parses the command line arguments and performs the corresponding actions.
461
- * @private
462
- * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
463
- */
464
359
  async parseCommandLine() {
465
360
  if (hasParameter('help')) {
466
361
  this.log.info(`\nUsage: matterbridge [options]\n
@@ -567,75 +462,32 @@ export class Matterbridge extends EventEmitter {
567
462
  return;
568
463
  }
569
464
  if (hasParameter('factoryreset')) {
570
- try {
571
- // Delete old matter storage file
572
- const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
573
- this.log.info(`Unlinking old matter storage file: ${file}`);
574
- await fs.unlink(file);
575
- }
576
- catch (err) {
577
- if (err instanceof Error && err.code !== 'ENOENT') {
578
- this.log.error(`Error unlinking old matter storage file: ${err}`);
579
- }
580
- }
581
- try {
582
- // Delete matter node storage directory with its subdirectories
583
- const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
584
- this.log.info(`Removing matter node storage directory: ${dir}`);
585
- await fs.rm(dir, { recursive: true });
586
- }
587
- catch (err) {
588
- if (err instanceof Error && err.code !== 'ENOENT') {
589
- this.log.error(`Error removing matter storage directory: ${err}`);
590
- }
591
- }
592
- try {
593
- // Delete node storage directory with its subdirectories
594
- const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
595
- this.log.info(`Removing storage directory: ${dir}`);
596
- await fs.rm(dir, { recursive: true });
597
- }
598
- catch (err) {
599
- if (err instanceof Error && err.code !== 'ENOENT') {
600
- this.log.error(`Error removing storage directory: ${err}`);
601
- }
602
- }
603
- this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
604
- this.nodeContext = undefined;
605
- this.nodeStorage = undefined;
606
- this.plugins.clear();
607
- this.devices.clear();
608
- this.emit('shutdown');
465
+ await this.shutdownProcessAndFactoryReset();
609
466
  return;
610
467
  }
611
- // Start the matter storage and create the matterbridge context
612
468
  try {
613
- await this.startMatterStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
469
+ await this.startMatterStorage();
614
470
  }
615
471
  catch (error) {
616
472
  this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
617
473
  throw new Error(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
618
474
  }
619
- // Clear the matterbridge context if the reset parameter is set
620
- if (!this.edge && hasParameter('reset') && getParameter('reset') === undefined) {
621
- this.log.info('Resetting Matterbridge commissioning information...');
622
- await this.matterbridgeContext?.clearAll();
623
- await this.stopMatterStorage();
624
- this.log.info('Reset done! Remove the device from the controller.');
625
- this.emit('shutdown');
475
+ if (hasParameter('reset') && getParameter('reset') === undefined) {
476
+ await this.shutdownProcessAndReset();
626
477
  return;
627
478
  }
628
- // Clear matterbridge plugin context if the reset parameter is set
629
- if (!this.edge && hasParameter('reset') && getParameter('reset') !== undefined) {
479
+ if (hasParameter('reset') && getParameter('reset') !== undefined) {
630
480
  this.log.debug(`Reset plugin ${getParameter('reset')}`);
631
481
  const plugin = this.plugins.get(getParameter('reset'));
632
482
  if (plugin) {
633
- if (!this.storageManager)
483
+ const matterStorageManager = await this.matterStorageService?.open(plugin.name);
484
+ if (!matterStorageManager)
634
485
  this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
635
- const context = this.storageManager?.createContext(plugin.name);
636
- if (!context)
637
- this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
638
- await context?.clearAll();
486
+ await matterStorageManager?.createContext('events')?.clearAll();
487
+ await matterStorageManager?.createContext('fabrics')?.clearAll();
488
+ await matterStorageManager?.createContext('root')?.clearAll();
489
+ await matterStorageManager?.createContext('sessions')?.clearAll();
490
+ await matterStorageManager?.createContext('persist')?.clearAll();
639
491
  this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
640
492
  }
641
493
  else {
@@ -645,74 +497,55 @@ export class Matterbridge extends EventEmitter {
645
497
  this.emit('shutdown');
646
498
  return;
647
499
  }
648
- // Initialize frontend
649
500
  if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
650
- await this.initializeFrontend(getIntParameter('frontend'));
651
- // Check each 60 minutes the latest versions
501
+ await this.frontend.start(getIntParameter('frontend'));
652
502
  this.checkUpdateInterval = setInterval(() => {
653
503
  this.getMatterbridgeLatestVersion();
654
504
  for (const plugin of this.plugins) {
655
505
  this.getPluginLatestVersion(plugin);
656
506
  }
507
+ this.frontend.wssSendRefreshRequired();
657
508
  }, 60 * 60 * 1000);
658
- // Start the matterbridge in mode test
659
509
  if (hasParameter('test')) {
660
510
  this.bridgeMode = 'bridge';
661
- MatterbridgeDevice.bridgeMode = 'bridge';
511
+ MatterbridgeEndpoint.bridgeMode = 'bridge';
662
512
  return;
663
513
  }
664
- // Start the matterbridge in mode controller
665
514
  if (hasParameter('controller')) {
666
515
  this.bridgeMode = 'controller';
667
516
  await this.startController();
668
517
  return;
669
518
  }
670
- // Check if the bridge mode is set and start matterbridge in bridge mode if not set
671
519
  if (!hasParameter('bridge') && !hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === '') {
672
520
  this.log.info('Setting default matterbridge start mode to bridge');
673
521
  await this.nodeContext?.set('bridgeMode', 'bridge');
674
522
  }
675
- // Start matterbridge in bridge mode
676
523
  if (hasParameter('bridge') || (!hasParameter('childbridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'bridge')) {
677
524
  this.bridgeMode = 'bridge';
678
- MatterbridgeDevice.bridgeMode = 'bridge';
679
525
  MatterbridgeEndpoint.bridgeMode = 'bridge';
680
526
  this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
681
527
  await this.startBridge();
682
528
  return;
683
529
  }
684
- // Start matterbridge in childbridge mode
685
530
  if (hasParameter('childbridge') || (!hasParameter('bridge') && (await this.nodeContext?.get('bridgeMode', '')) === 'childbridge')) {
686
531
  this.bridgeMode = 'childbridge';
687
- MatterbridgeDevice.bridgeMode = 'childbridge';
688
532
  MatterbridgeEndpoint.bridgeMode = 'childbridge';
689
533
  this.log.debug(`Starting matterbridge in mode ${this.bridgeMode}`);
690
534
  await this.startChildbridge();
691
535
  return;
692
536
  }
693
537
  }
694
- /**
695
- * Asynchronously loads and starts the registered plugins.
696
- *
697
- * This method is responsible for initializing and staarting all enabled plugins.
698
- * It ensures that each plugin is properly loaded and started before the ridge starts.
699
- *
700
- * @returns {Promise<void>} A promise that resolves when all plugins have been loaded and started.
701
- */
702
538
  async startPlugins() {
703
- // Check, load and start the plugins
704
539
  for (const plugin of this.plugins) {
705
540
  plugin.configJson = await this.plugins.loadConfig(plugin);
706
541
  plugin.schemaJson = await this.plugins.loadSchema(plugin);
707
- // Check if the plugin is available
708
542
  if (!(await this.plugins.resolve(plugin.path))) {
709
543
  this.log.error(`Plugin ${plg}${plugin.name}${er} not found or not validated. Disabling it.`);
710
544
  plugin.enabled = false;
711
545
  plugin.error = true;
712
546
  continue;
713
547
  }
714
- // Check if the plugin has a new version
715
- this.getPluginLatestVersion(plugin); // No await do it asyncronously
548
+ this.getPluginLatestVersion(plugin);
716
549
  if (!plugin.enabled) {
717
550
  this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
718
551
  continue;
@@ -722,31 +555,24 @@ export class Matterbridge extends EventEmitter {
722
555
  plugin.loaded = false;
723
556
  plugin.started = false;
724
557
  plugin.configured = false;
725
- plugin.connected = undefined;
726
558
  plugin.registeredDevices = undefined;
727
559
  plugin.addedDevices = undefined;
728
560
  plugin.qrPairingCode = undefined;
729
561
  plugin.manualPairingCode = undefined;
730
- this.plugins.load(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
562
+ this.plugins.load(plugin, true, 'Matterbridge is starting');
731
563
  }
732
- this.wssSendRefreshRequired();
564
+ this.frontend.wssSendRefreshRequired();
733
565
  }
734
- /**
735
- * Registers the process handlers for uncaughtException, unhandledRejection, SIGINT and SIGTERM.
736
- * When either of these signals are received, the cleanup method is called with an appropriate message.
737
- */
738
566
  registerProcessHandlers() {
739
567
  this.log.debug(`Registering uncaughtException and unhandledRejection handlers...`);
740
568
  process.removeAllListeners('uncaughtException');
741
569
  process.removeAllListeners('unhandledRejection');
742
570
  this.exceptionHandler = async (error) => {
743
571
  this.log.fatal('Unhandled Exception detected at:', error.stack || error, rs);
744
- // await this.cleanup('Unhandled Exception detected, cleaning up...');
745
572
  };
746
573
  process.on('uncaughtException', this.exceptionHandler);
747
574
  this.rejectionHandler = async (reason, promise) => {
748
575
  this.log.fatal('Unhandled Rejection detected at:', promise, 'reason:', reason instanceof Error ? reason.stack : reason, rs);
749
- // await this.cleanup('Unhandled Rejection detected, cleaning up...');
750
576
  };
751
577
  process.on('unhandledRejection', this.rejectionHandler);
752
578
  this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
@@ -759,9 +585,6 @@ export class Matterbridge extends EventEmitter {
759
585
  };
760
586
  process.on('SIGTERM', this.sigtermHandler);
761
587
  }
762
- /**
763
- * Deregisters the process uncaughtException, unhandledRejection, SIGINT and SIGTERM signal handlers.
764
- */
765
588
  deregisterProcesslHandlers() {
766
589
  this.log.debug(`Deregistering uncaughtException and unhandledRejection handlers...`);
767
590
  if (this.exceptionHandler)
@@ -778,15 +601,14 @@ export class Matterbridge extends EventEmitter {
778
601
  process.off('SIGTERM', this.sigtermHandler);
779
602
  this.sigtermHandler = undefined;
780
603
  }
781
- /**
782
- * Logs the node and system information.
783
- */
784
604
  async logNodeAndSystemInfo() {
785
- // IP address information
786
605
  const networkInterfaces = os.networkInterfaces();
606
+ this.systemInformation.interfaceName = '';
787
607
  this.systemInformation.ipv4Address = '';
788
608
  this.systemInformation.ipv6Address = '';
789
609
  for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
610
+ if (this.mdnsInterface && interfaceName !== this.mdnsInterface)
611
+ continue;
790
612
  if (!interfaceDetails) {
791
613
  break;
792
614
  }
@@ -802,7 +624,7 @@ export class Matterbridge extends EventEmitter {
802
624
  this.systemInformation.macAddress = detail.mac;
803
625
  }
804
626
  }
805
- if (this.systemInformation.ipv4Address !== '' /* && this.systemInformation.ipv6Address !== ''*/) {
627
+ if (this.systemInformation.ipv4Address !== '' || this.systemInformation.ipv6Address !== '') {
806
628
  this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
807
629
  this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
808
630
  this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
@@ -810,22 +632,19 @@ export class Matterbridge extends EventEmitter {
810
632
  break;
811
633
  }
812
634
  }
813
- // Node information
814
635
  this.systemInformation.nodeVersion = process.versions.node;
815
636
  const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
816
637
  const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
817
638
  const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
818
- // Host system information
819
639
  this.systemInformation.hostname = os.hostname();
820
640
  this.systemInformation.user = os.userInfo().username;
821
- this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
822
- this.systemInformation.osRelease = os.release(); // Kernel version
823
- this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
824
- this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
825
- this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
826
- this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
827
- this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
828
- // Log the system information
641
+ this.systemInformation.osType = os.type();
642
+ this.systemInformation.osRelease = os.release();
643
+ this.systemInformation.osPlatform = os.platform();
644
+ this.systemInformation.osArch = os.arch();
645
+ this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
646
+ this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB';
647
+ this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours';
829
648
  this.log.debug('Host System Information:');
830
649
  this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
831
650
  this.log.debug(`- User: ${this.systemInformation.user}`);
@@ -841,19 +660,15 @@ export class Matterbridge extends EventEmitter {
841
660
  this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
842
661
  this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
843
662
  this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
844
- // Home directory
845
663
  this.homeDirectory = getParameter('homedir') ?? os.homedir();
846
664
  this.matterbridgeInformation.homeDirectory = this.homeDirectory;
847
665
  this.log.debug(`Home Directory: ${this.homeDirectory}`);
848
- // Package root directory
849
666
  const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
850
667
  this.rootDirectory = path.resolve(currentFileDirectory, '../');
851
668
  this.matterbridgeInformation.rootDirectory = this.rootDirectory;
852
669
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
853
- // Global node_modules directory
854
670
  if (this.nodeContext)
855
671
  this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
856
- // First run of Matterbridge so the node storage is empty
857
672
  if (this.globalModulesDirectory === '') {
858
673
  try {
859
674
  this.globalModulesDirectory = await this.getGlobalNodeModules();
@@ -877,7 +692,6 @@ export class Matterbridge extends EventEmitter {
877
692
  this.log.error(`Error getting global node_modules directory: ${error}`);
878
693
  });
879
694
  }
880
- // Create the data directory .matterbridge in the home directory
881
695
  this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
882
696
  this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
883
697
  try {
@@ -901,7 +715,6 @@ export class Matterbridge extends EventEmitter {
901
715
  }
902
716
  }
903
717
  this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
904
- // Create the plugin directory Matterbridge in the home directory
905
718
  this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
906
719
  this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
907
720
  try {
@@ -925,28 +738,19 @@ export class Matterbridge extends EventEmitter {
925
738
  }
926
739
  }
927
740
  this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
928
- // Matterbridge version
929
741
  const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
930
742
  this.matterbridgeVersion = packageJson.version;
931
743
  this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
932
744
  this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
933
- // Matterbridge latest version
934
745
  if (this.nodeContext)
935
746
  this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
936
747
  this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
937
748
  this.getMatterbridgeLatestVersion();
938
- // Current working directory
939
749
  const currentDir = process.cwd();
940
750
  this.log.debug(`Current Working Directory: ${currentDir}`);
941
- // Command line arguments (excluding 'node' and the script name)
942
751
  const cmdArgs = process.argv.slice(2).join(' ');
943
752
  this.log.debug(`Command Line Arguments: ${cmdArgs}`);
944
753
  }
945
- /**
946
- * Retrieves the latest version of a package from the npm registry.
947
- * @param packageName - The name of the package.
948
- * @returns A Promise that resolves to the latest version of the package.
949
- */
950
754
  async getLatestVersion(packageName) {
951
755
  return new Promise((resolve, reject) => {
952
756
  this.execRunningCount++;
@@ -961,10 +765,6 @@ export class Matterbridge extends EventEmitter {
961
765
  });
962
766
  });
963
767
  }
964
- /**
965
- * Retrieves the path to the global Node.js modules directory.
966
- * @returns A promise that resolves to the path of the global Node.js modules directory.
967
- */
968
768
  async getGlobalNodeModules() {
969
769
  return new Promise((resolve, reject) => {
970
770
  this.execRunningCount++;
@@ -979,11 +779,6 @@ export class Matterbridge extends EventEmitter {
979
779
  });
980
780
  });
981
781
  }
982
- /**
983
- * Retrieves the latest version of Matterbridge and performs necessary actions based on the version comparison.
984
- * @private
985
- * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
986
- */
987
782
  async getMatterbridgeLatestVersion() {
988
783
  this.getLatestVersion('matterbridge')
989
784
  .then(async (matterbridgeLatestVersion) => {
@@ -1000,19 +795,8 @@ export class Matterbridge extends EventEmitter {
1000
795
  })
1001
796
  .catch((error) => {
1002
797
  this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
1003
- // error.stack && this.log.debug(error.stack);
1004
798
  });
1005
799
  }
1006
- /**
1007
- * Retrieves the latest version of a plugin and updates the plugin's latestVersion property.
1008
- * If the plugin's version is different from the latest version, logs a warning message.
1009
- * If the plugin's version is the same as the latest version, logs an info message.
1010
- * If there is an error retrieving the latest version, logs an error message.
1011
- *
1012
- * @private
1013
- * @param {RegisteredPlugin} plugin - The plugin for which to retrieve the latest version.
1014
- * @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
1015
- */
1016
800
  async getPluginLatestVersion(plugin) {
1017
801
  this.getLatestVersion(plugin.name)
1018
802
  .then(async (latestVersion) => {
@@ -1024,54 +808,40 @@ export class Matterbridge extends EventEmitter {
1024
808
  })
1025
809
  .catch((error) => {
1026
810
  this.log.error(`Error getting ${plg}${plugin.name}${er} latest version: ${error.message}`);
1027
- // error.stack && this.log.debug(error.stack);
1028
811
  });
1029
812
  }
1030
- /**
1031
- * Creates a MatterLogger function to show the matter.js log messages in AnsiLogger (for the frontend).
1032
- *
1033
- * @returns {Function} The MatterLogger function.
1034
- */
1035
813
  createMatterLogger() {
1036
- const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
814
+ const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4, logLevel: "debug" });
1037
815
  return (_level, formattedLog) => {
1038
816
  const logger = formattedLog.slice(44, 44 + 20).trim();
1039
817
  const message = formattedLog.slice(65);
1040
818
  matterLogger.logName = logger;
1041
819
  switch (_level) {
1042
820
  case MatterLogLevel.DEBUG:
1043
- matterLogger.log("debug" /* LogLevel.DEBUG */, message);
821
+ matterLogger.log("debug", message);
1044
822
  break;
1045
823
  case MatterLogLevel.INFO:
1046
- matterLogger.log("info" /* LogLevel.INFO */, message);
824
+ matterLogger.log("info", message);
1047
825
  break;
1048
826
  case MatterLogLevel.NOTICE:
1049
- matterLogger.log("notice" /* LogLevel.NOTICE */, message);
827
+ matterLogger.log("notice", message);
1050
828
  break;
1051
829
  case MatterLogLevel.WARN:
1052
- matterLogger.log("warn" /* LogLevel.WARN */, message);
830
+ matterLogger.log("warn", message);
1053
831
  break;
1054
832
  case MatterLogLevel.ERROR:
1055
- matterLogger.log("error" /* LogLevel.ERROR */, message);
833
+ matterLogger.log("error", message);
1056
834
  break;
1057
835
  case MatterLogLevel.FATAL:
1058
- matterLogger.log("fatal" /* LogLevel.FATAL */, message);
836
+ matterLogger.log("fatal", message);
1059
837
  break;
1060
838
  default:
1061
- matterLogger.log("debug" /* LogLevel.DEBUG */, message);
839
+ matterLogger.log("debug", message);
1062
840
  break;
1063
841
  }
1064
842
  };
1065
843
  }
1066
- /**
1067
- * Creates a Matter File Logger.
1068
- *
1069
- * @param {string} filePath - The path to the log file.
1070
- * @param {boolean} [unlink=false] - Whether to unlink the log file before creating a new one.
1071
- * @returns {Function} - A function that logs formatted messages to the log file.
1072
- */
1073
844
  async createMatterFileLogger(filePath, unlink = false) {
1074
- // 2024-08-21 08:55:19.488 DEBUG InteractionMessenger Sending DataReport chunk with 28 attributes and 0 events: 1004 bytes
1075
845
  let fileSize = 0;
1076
846
  if (unlink) {
1077
847
  try {
@@ -1120,86 +890,113 @@ export class Matterbridge extends EventEmitter {
1120
890
  }
1121
891
  };
1122
892
  }
1123
- /**
1124
- * Update matterbridge and cleanup.
1125
- */
1126
- async updateProcess() {
1127
- await this.cleanup('updating...', false);
1128
- }
1129
- /**
1130
- * Restarts the process by spawning a new process and exiting the current process.
1131
- */
1132
893
  async restartProcess() {
1133
894
  await this.cleanup('restarting...', true);
1134
895
  }
1135
- /**
1136
- * Shut down the process by exiting the current process.
1137
- */
1138
896
  async shutdownProcess() {
1139
897
  await this.cleanup('shutting down...', false);
1140
898
  }
1141
- /**
1142
- * Shut down the process and reset.
1143
- */
899
+ async updateProcess() {
900
+ this.log.info('Updating matterbridge...');
901
+ try {
902
+ await this.spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
903
+ this.log.info('Matterbridge has been updated. Full restart required.');
904
+ }
905
+ catch (error) {
906
+ this.log.error('Error updating matterbridge:', error instanceof Error ? error.message : error);
907
+ }
908
+ this.frontend.wssSendRestartRequired();
909
+ await this.cleanup('updating...', false);
910
+ }
1144
911
  async unregisterAndShutdownProcess() {
1145
912
  this.log.info('Unregistering all devices and shutting down...');
1146
- for (const plugin of this.plugins /* .filter((plugin) => plugin.enabled && !plugin.error))*/) {
1147
- if (this.edge)
1148
- await this.removeAllBridgedEndpoints(plugin.name);
1149
- else
1150
- await this.removeAllBridgedDevices(plugin.name);
913
+ for (const plugin of this.plugins) {
914
+ await this.removeAllBridgedEndpoints(plugin.name);
1151
915
  }
1152
916
  await this.cleanup('unregistered all devices and shutting down...', false);
1153
917
  }
1154
- /**
1155
- * Shut down the process and reset.
1156
- */
1157
918
  async shutdownProcessAndReset() {
919
+ this.log.info('Resetting Matterbridge commissioning information...');
920
+ await this.matterStorageManager?.createContext('events')?.clearAll();
921
+ await this.matterStorageManager?.createContext('fabrics')?.clearAll();
922
+ await this.matterStorageManager?.createContext('root')?.clearAll();
923
+ await this.matterStorageManager?.createContext('sessions')?.clearAll();
924
+ await this.matterbridgeContext?.clearAll();
925
+ await this.stopMatterStorage();
926
+ this.log.info('Matter storage reset done! Remove the bridge from the controller.');
1158
927
  await this.cleanup('shutting down with reset...', false);
1159
928
  }
1160
- /**
1161
- * Shut down the process and factory reset.
1162
- */
1163
929
  async shutdownProcessAndFactoryReset() {
930
+ try {
931
+ const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
932
+ this.log.info(`Unlinking old matter storage file: ${file}`);
933
+ await fs.unlink(file);
934
+ const backup = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.backup.json');
935
+ this.log.info(`Unlinking old matter storage backup file: ${backup}`);
936
+ await fs.unlink(backup);
937
+ }
938
+ catch (err) {
939
+ if (err instanceof Error && err.code !== 'ENOENT') {
940
+ this.log.error(`Error unlinking old matter storage file: ${err}`);
941
+ }
942
+ }
943
+ try {
944
+ const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
945
+ this.log.info(`Removing matter node storage directory: ${dir}`);
946
+ await fs.rm(dir, { recursive: true });
947
+ const backup = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.backup');
948
+ this.log.info(`Removing matter node storage backup directory: ${backup}`);
949
+ await fs.rm(backup, { recursive: true });
950
+ }
951
+ catch (err) {
952
+ if (err instanceof Error && err.code !== 'ENOENT') {
953
+ this.log.error(`Error removing matter storage directory: ${err}`);
954
+ }
955
+ }
956
+ try {
957
+ const dir = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
958
+ this.log.info(`Removing storage directory: ${dir}`);
959
+ await fs.rm(dir, { recursive: true });
960
+ const backup = path.join(this.matterbridgeDirectory, 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.backup');
961
+ this.log.info(`Removing storage backup directory: ${backup}`);
962
+ await fs.rm(backup, { recursive: true });
963
+ }
964
+ catch (err) {
965
+ if (err instanceof Error && err.code !== 'ENOENT') {
966
+ this.log.error(`Error removing storage directory: ${err}`);
967
+ }
968
+ }
969
+ this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
970
+ this.nodeContext = undefined;
971
+ this.nodeStorage = undefined;
972
+ this.plugins.clear();
973
+ this.devices.clear();
1164
974
  await this.cleanup('shutting down with factory reset...', false);
1165
975
  }
1166
- /**
1167
- * Cleans up the Matterbridge instance.
1168
- * @param message - The cleanup message.
1169
- * @param restart - Indicates whether to restart the instance after cleanup. Default is `false`.
1170
- * @returns A promise that resolves when the cleanup is completed.
1171
- */
1172
976
  async cleanup(message, restart = false) {
1173
977
  if (this.initialized && !this.hasCleanupStarted) {
1174
978
  this.hasCleanupStarted = true;
1175
979
  this.log.info(message);
1176
- // Deregisters the process handlers
1177
- this.deregisterProcesslHandlers();
1178
- // Clear the start matter interval
1179
980
  if (this.startMatterInterval) {
1180
981
  clearInterval(this.startMatterInterval);
1181
982
  this.startMatterInterval = undefined;
1182
983
  this.log.debug('Start matter interval cleared');
1183
984
  }
1184
- // Clear the check update interval
1185
985
  if (this.checkUpdateInterval) {
1186
986
  clearInterval(this.checkUpdateInterval);
1187
987
  this.checkUpdateInterval = undefined;
1188
988
  this.log.debug('Check update interval cleared');
1189
989
  }
1190
- // Clear the configure timeout
1191
990
  if (this.configureTimeout) {
1192
991
  clearTimeout(this.configureTimeout);
1193
992
  this.configureTimeout = undefined;
1194
993
  this.log.debug('Matterbridge configure timeout cleared');
1195
994
  }
1196
- // Clear the reachability timeout
1197
995
  if (this.reachabilityTimeout) {
1198
996
  clearTimeout(this.reachabilityTimeout);
1199
997
  this.reachabilityTimeout = undefined;
1200
998
  this.log.debug('Matterbridge reachability timeout cleared');
1201
999
  }
1202
- // Calling the shutdown method of each plugin and clear the reachability timeout
1203
1000
  for (const plugin of this.plugins) {
1204
1001
  if (!plugin.enabled || plugin.error)
1205
1002
  continue;
@@ -1210,86 +1007,42 @@ export class Matterbridge extends EventEmitter {
1210
1007
  this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
1211
1008
  }
1212
1009
  }
1213
- // Convert the matter storage to the new format
1214
- if (!hasParameter('nostorageconversion') && this.edge === false && this.matterbridgeContext && ['updating...', 'restarting...', 'shutting down...'].includes(message)) {
1215
- if (this.bridgeMode === 'bridge') {
1216
- await this.convertStorage(this.matterbridgeContext, 'Matterbridge');
1217
- }
1218
- else if (this.bridgeMode === 'childbridge') {
1219
- for (const plugin of this.plugins) {
1220
- if (plugin.storageContext) {
1221
- await this.convertStorage(plugin.storageContext, plugin.name);
1222
- }
1223
- }
1010
+ this.frontend.stop();
1011
+ this.log.info(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
1012
+ if (this.bridgeMode === 'bridge') {
1013
+ if (this.serverNode) {
1014
+ await this.stopServerNode(this.serverNode);
1015
+ this.log.info(`Stopped matter server node for Matterbridge`);
1224
1016
  }
1225
1017
  }
1226
- // Close the http server
1227
- if (this.httpServer) {
1228
- this.httpServer.close();
1229
- this.httpServer.removeAllListeners();
1230
- this.httpServer = undefined;
1231
- this.log.debug('Frontend http server closed successfully');
1232
- }
1233
- // Close the https server
1234
- if (this.httpsServer) {
1235
- this.httpsServer.close();
1236
- this.httpsServer.removeAllListeners();
1237
- this.httpsServer = undefined;
1238
- this.log.debug('Frontend https server closed successfully');
1239
- }
1240
- // Remove listeners from the express app
1241
- if (this.expressApp) {
1242
- this.expressApp.removeAllListeners();
1243
- this.expressApp = undefined;
1244
- this.log.debug('Frontend app closed successfully');
1245
- }
1246
- // Close the WebSocket server
1247
- if (this.webSocketServer) {
1248
- // Close all active connections
1249
- this.webSocketServer.clients.forEach((client) => {
1250
- if (client.readyState === WebSocket.OPEN) {
1251
- client.close();
1252
- }
1253
- });
1254
- this.webSocketServer.close((error) => {
1255
- if (error) {
1256
- this.log.error(`Error closing WebSocket server: ${error}`);
1257
- }
1258
- else {
1259
- this.log.debug('WebSocket server closed successfully');
1018
+ if (this.bridgeMode === 'childbridge') {
1019
+ for (const plugin of this.plugins.array()) {
1020
+ if (plugin.serverNode) {
1021
+ await this.stopServerNode(plugin.serverNode);
1022
+ this.log.info(`Stopped matter server node for ${plugin.name}`);
1260
1023
  }
1261
- });
1262
- this.webSocketServer = undefined;
1024
+ }
1263
1025
  }
1264
- // Closing matter
1265
- await this.stopMatterServer();
1266
- // Closing matter storage
1026
+ this.log.info('Stopped matter server nodes');
1267
1027
  await this.stopMatterStorage();
1268
- // Remove the matterfilelogger
1269
1028
  try {
1270
1029
  Logger.removeLogger('matterfilelogger');
1271
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1272
1030
  }
1273
1031
  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}`);
1275
1032
  }
1276
- // Serialize registeredDevices
1277
1033
  if (this.nodeStorage && this.nodeContext) {
1278
1034
  this.log.info('Saving registered devices...');
1279
1035
  const serializedRegisteredDevices = [];
1280
1036
  this.devices.forEach(async (device) => {
1281
1037
  const serializedMatterbridgeDevice = device.serialize();
1282
- // this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
1283
1038
  if (serializedMatterbridgeDevice)
1284
1039
  serializedRegisteredDevices.push(serializedMatterbridgeDevice);
1285
1040
  });
1286
1041
  await this.nodeContext.set('devices', serializedRegisteredDevices);
1287
1042
  this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
1288
- // Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
1289
1043
  this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
1290
1044
  await this.nodeContext.close();
1291
1045
  this.nodeContext = undefined;
1292
- // Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
1293
1046
  for (const plugin of this.plugins) {
1294
1047
  if (plugin.nodeContext) {
1295
1048
  this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
@@ -1306,6 +1059,7 @@ export class Matterbridge extends EventEmitter {
1306
1059
  }
1307
1060
  this.plugins.clear();
1308
1061
  this.devices.clear();
1062
+ this.deregisterProcesslHandlers();
1309
1063
  if (restart) {
1310
1064
  if (message === 'updating...') {
1311
1065
  this.log.info('Cleanup completed. Updating...');
@@ -1319,38 +1073,6 @@ export class Matterbridge extends EventEmitter {
1319
1073
  }
1320
1074
  }
1321
1075
  else {
1322
- if (message === 'shutting down with reset...' || message === 'shutting down with factory reset...') {
1323
- try {
1324
- // Delete old matter storage file
1325
- const file = path.join(this.matterbridgeDirectory, 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json');
1326
- this.log.info(`Unlinking old matter storage file: ${file}`);
1327
- await fs.unlink(file);
1328
- }
1329
- catch (error) {
1330
- this.log.debug(`Error resetting old matter storage file: ${error}`);
1331
- }
1332
- try {
1333
- // Delete matter node storage directory with its subdirectories
1334
- const dir = path.join(this.matterbridgeDirectory, 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : ''));
1335
- this.log.info(`Removing matter node storage directory: ${dir}`);
1336
- await fs.rm(dir, { recursive: true });
1337
- }
1338
- catch (error) {
1339
- this.log.debug(`Error resetting matter node storage file: ${error}`);
1340
- }
1341
- this.log.info('Reset done! Remove all paired fabrics from the controllers.');
1342
- }
1343
- if (message === 'shutting down with factory reset...') {
1344
- try {
1345
- // Delete node storage directory with its subdirectories
1346
- this.log.info('Resetting Matterbridge storage...');
1347
- await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
1348
- }
1349
- catch (error) {
1350
- this.log.debug(`Error resetting Matterbridge storage: ${error}`);
1351
- }
1352
- this.log.info('Factory reset done! Remove all paired fabrics from the controllers.');
1353
- }
1354
1076
  this.log.notice('Cleanup completed. Shutting down...');
1355
1077
  Matterbridge.instance = undefined;
1356
1078
  this.emit('shutdown');
@@ -1359,190 +1081,41 @@ export class Matterbridge extends EventEmitter {
1359
1081
  this.initialized = false;
1360
1082
  }
1361
1083
  }
1362
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1363
- async addBridgedEndpoint(pluginName, device) {
1364
- // Nothing to do here
1365
- }
1366
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1367
- async removeBridgedEndpoint(pluginName, device) {
1368
- // Nothing to do here
1369
- }
1370
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1371
- async removeAllBridgedEndpoints(pluginName) {
1372
- // Nothing to do here
1373
- }
1374
- /**
1375
- * Adds a bridged device to the Matterbridge.
1376
- * @param pluginName - The name of the plugin.
1377
- * @param device - The bridged device to add.
1378
- * @returns {Promise<void>} - A promise that resolves when the device is added.
1379
- */
1380
- async addBridgedDevice(pluginName, device) {
1381
- this.log.debug(`Adding bridged device ${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1382
- // Check if the plugin is registered
1383
- const plugin = this.plugins.get(pluginName);
1384
- if (!plugin) {
1385
- this.log.error(`Error adding bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) plugin ${plg}${pluginName}${er} not found`);
1386
- return;
1387
- }
1388
- // Register and add the device to matterbridge aggregator in bridge mode
1389
- if (this.bridgeMode === 'bridge') {
1390
- if (!this.matterAggregator) {
1391
- this.log.error(`Adding bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er} error: matterAggregator not found`);
1392
- return;
1393
- }
1394
- this.matterAggregator.addBridgedDevice(device);
1395
- }
1396
- // The first time create the commissioning server and the aggregator for DynamicPlatform
1397
- // Register and add the device in childbridge mode
1398
- if (this.bridgeMode === 'childbridge') {
1399
- if (plugin.type === 'AccessoryPlatform') {
1400
- // Check if the plugin is locked with the commissioning server
1401
- if (!plugin.locked) {
1402
- plugin.locked = true;
1403
- plugin.storageContext = await this.importCommissioningServerContext(plugin.name, device);
1404
- this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1405
- plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1406
- this.log.debug(`Adding device ${dev}${device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
1407
- plugin.commissioningServer.addDevice(device);
1408
- plugin.device = device;
1409
- this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db}`);
1410
- await this.matterServer?.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
1411
- }
1412
- }
1413
- if (plugin.type === 'DynamicPlatform') {
1414
- // Check if the plugin is locked with the commissioning server and the aggregator
1415
- if (!plugin.locked) {
1416
- plugin.locked = true;
1417
- this.log.debug(`Creating commissioning server context for ${plg}${plugin.name}${db}`);
1418
- plugin.storageContext = await this.createCommissioningServerContext(plugin.name, 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, plugin.description);
1419
- this.log.debug(`Creating commissioning server for ${plg}${plugin.name}${db}`);
1420
- plugin.commissioningServer = await this.createCommisioningServer(plugin.storageContext, plugin.name);
1421
- this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
1422
- plugin.aggregator = await this.createMatterAggregator(plugin.storageContext, plugin.name);
1423
- this.log.debug(`Adding matter aggregator to commissioning server for plugin ${plg}${plugin.name}${db}`);
1424
- plugin.commissioningServer.addDevice(plugin.aggregator);
1425
- this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db}`);
1426
- await this.matterServer?.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
1427
- }
1428
- plugin.aggregator?.addBridgedDevice(device);
1429
- }
1084
+ async createAccessoryPlugin(plugin, device, start = false) {
1085
+ if (!plugin.locked && device.deviceName && device.vendorId && device.productId && device.vendorName && device.productName) {
1086
+ plugin.locked = true;
1087
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), device.vendorId, device.vendorName, device.productId, device.productName);
1088
+ plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1089
+ this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node`);
1090
+ await plugin.serverNode.add(device);
1091
+ if (start)
1092
+ await this.startServerNode(plugin.serverNode);
1430
1093
  }
1431
- if (plugin.registeredDevices !== undefined)
1432
- plugin.registeredDevices++;
1433
- if (plugin.addedDevices !== undefined)
1434
- plugin.addedDevices++;
1435
- // Add the device to the DeviceManager
1436
- this.devices.set(device);
1437
- this.log.info(`Added and registered bridged device (${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${dev}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1438
1094
  }
1439
- /**
1440
- * Removes a bridged device from the Matterbridge.
1441
- * @param pluginName - The name of the plugin.
1442
- * @param device - The device to be removed.
1443
- * @returns A Promise that resolves when the device is successfully removed.
1444
- */
1445
- async removeBridgedDevice(pluginName, device) {
1446
- this.log.debug(`Removing bridged device ${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1447
- // Check if the plugin is registered
1448
- const plugin = this.plugins.get(pluginName);
1449
- if (!plugin) {
1450
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1451
- return;
1452
- }
1453
- // Remove the device from matterbridge aggregator in bridge mode
1454
- if (this.bridgeMode === 'bridge') {
1455
- if (!this.matterAggregator) {
1456
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: matterAggregator not found`);
1457
- return;
1458
- }
1459
- if (device.number !== undefined) {
1460
- device.setBridgedDeviceReachability(false);
1461
- device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
1462
- }
1463
- // device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerShutDownEvent({});
1464
- // device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerLeaveEvent({});
1465
- this.matterAggregator?.removeBridgedDevice(device);
1466
- this.log.info(`Removed bridged device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1467
- if (plugin.registeredDevices !== undefined)
1468
- plugin.registeredDevices--;
1469
- if (plugin.addedDevices !== undefined)
1470
- plugin.addedDevices--;
1471
- }
1472
- // Remove the device in childbridge mode
1473
- if (this.bridgeMode === 'childbridge') {
1474
- if (plugin.type === 'AccessoryPlatform') {
1475
- if (!plugin.commissioningServer) {
1476
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: commissioning server not found`);
1477
- return;
1478
- }
1479
- }
1480
- else if (plugin.type === 'DynamicPlatform') {
1481
- if (!plugin.aggregator) {
1482
- this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator not found`);
1483
- return;
1484
- }
1485
- if (device.number !== undefined) {
1486
- device.setBridgedDeviceReachability(false);
1487
- device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
1488
- }
1489
- plugin.aggregator.removeBridgedDevice(device);
1490
- }
1491
- this.log.info(`Removed bridged device(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1492
- if (plugin.registeredDevices !== undefined)
1493
- plugin.registeredDevices--;
1494
- if (plugin.addedDevices !== undefined)
1495
- plugin.addedDevices--;
1496
- // Remove the commissioning server
1497
- if (plugin.registeredDevices === 0 && plugin.addedDevices === 0 && plugin.commissioningServer) {
1498
- this.matterServer?.removeCommissioningServer(plugin.commissioningServer);
1499
- plugin.commissioningServer = undefined;
1500
- this.log.info(`Removed commissioning server for plugin ${plg}${pluginName}${nf}`);
1501
- }
1095
+ async createDynamicPlugin(plugin, start = false) {
1096
+ if (!plugin.locked) {
1097
+ plugin.locked = true;
1098
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, plugin.description);
1099
+ plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1100
+ plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
1101
+ await plugin.serverNode.add(plugin.aggregatorNode);
1102
+ if (start)
1103
+ await this.startServerNode(plugin.serverNode);
1502
1104
  }
1503
- // Remove the device from the DeviceManager
1504
- this.devices.remove(device);
1505
- }
1506
- /**
1507
- * Removes all bridged devices associated with a specific plugin.
1508
- *
1509
- * @param pluginName - The name of the plugin.
1510
- * @returns A promise that resolves when all devices have been removed.
1511
- */
1512
- async removeAllBridgedDevices(pluginName) {
1513
- this.log.debug(`Removing all bridged devices for plugin ${plg}${pluginName}${db}`);
1514
- this.devices.forEach(async (device) => {
1515
- if (device.plugin === pluginName) {
1516
- await this.removeBridgedDevice(pluginName, device);
1517
- }
1518
- });
1519
1105
  }
1520
- /**
1521
- * Starts the Matterbridge in bridge mode.
1522
- * @private
1523
- * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1524
- */
1525
1106
  async startBridge() {
1526
- // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1527
- if (!this.storageManager)
1107
+ if (!this.matterStorageManager)
1528
1108
  throw new Error('No storage manager initialized');
1529
1109
  if (!this.matterbridgeContext)
1530
1110
  throw new Error('No storage context initialized');
1531
- this.matterServer = await this.createMatterServer(this.storageManager);
1532
- this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
1533
- this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
1534
- this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
1535
- this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
1536
- this.log.debug('Adding matterbridge aggregator to commissioning server');
1537
- this.commissioningServer.addDevice(this.matterAggregator);
1538
- this.log.debug('Adding matterbridge commissioning server to matter server');
1539
- await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
1111
+ this.serverNode = await this.createServerNode(this.matterbridgeContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1112
+ this.aggregatorNode = await this.createAggregatorNode(this.matterbridgeContext);
1113
+ await this.serverNode.add(this.aggregatorNode);
1540
1114
  await this.startPlugins();
1541
1115
  this.log.debug('Starting start matter interval in bridge mode');
1542
1116
  let failCount = 0;
1543
1117
  this.startMatterInterval = setInterval(async () => {
1544
1118
  for (const plugin of this.plugins) {
1545
- // new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1546
1119
  if (!plugin.enabled)
1547
1120
  continue;
1548
1121
  if (plugin.error) {
@@ -1567,54 +1140,51 @@ export class Matterbridge extends EventEmitter {
1567
1140
  clearInterval(this.startMatterInterval);
1568
1141
  this.startMatterInterval = undefined;
1569
1142
  this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1570
- // Start the Matter server
1571
- await this.startMatterServer();
1572
- this.log.notice('Matter server started');
1573
- // Show the QR code for commissioning or log the already commissioned message
1574
- await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
1575
- // Configure the plugins
1143
+ this.startServerNode(this.serverNode);
1576
1144
  this.configureTimeout = setTimeout(async () => {
1577
1145
  for (const plugin of this.plugins) {
1578
1146
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1579
1147
  continue;
1580
1148
  try {
1581
- await this.plugins.configure(plugin); // TODO No await do it in parallel
1149
+ await this.plugins.configure(plugin);
1582
1150
  }
1583
1151
  catch (error) {
1584
1152
  plugin.error = true;
1585
1153
  this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1586
1154
  }
1587
1155
  }
1588
- this.wssSendRefreshRequired();
1156
+ this.frontend.wssSendRefreshRequired();
1589
1157
  }, 30 * 1000);
1590
- // Setting reachability to true
1591
1158
  this.reachabilityTimeout = setTimeout(() => {
1592
1159
  this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
1593
- if (this.commissioningServer)
1594
- this.setCommissioningServerReachability(this.commissioningServer, true);
1595
- if (this.matterAggregator)
1596
- this.setAggregatorReachability(this.matterAggregator, true);
1160
+ if (this.serverNode)
1161
+ this.setServerNodeReachability(this.serverNode, true);
1162
+ if (this.aggregatorNode)
1163
+ this.setAggregatorReachability(this.aggregatorNode, true);
1164
+ this.frontend.wssSendRefreshRequired();
1597
1165
  }, 60 * 1000);
1598
1166
  }, 1000);
1599
1167
  }
1600
- /**
1601
- * Starts the Matterbridge in childbridge mode.
1602
- * @private
1603
- * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1604
- */
1605
1168
  async startChildbridge() {
1606
- // Matterbridge.addBridgedDevice creates the commissionig servers and add the devices to the the commissioning server or to the aggregator
1607
- // Plugins are configured by a timer when matter server is started and plugin.configured is set to true
1608
- if (!this.storageManager)
1169
+ if (!this.matterStorageManager)
1609
1170
  throw new Error('No storage manager initialized');
1610
- this.matterServer = await this.createMatterServer(this.storageManager);
1171
+ for (const plugin of this.plugins) {
1172
+ if (!plugin.enabled)
1173
+ continue;
1174
+ if (plugin.type === 'DynamicPlatform') {
1175
+ plugin.locked = true;
1176
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, plugin.description);
1177
+ plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1178
+ plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
1179
+ await plugin.serverNode.add(plugin.aggregatorNode);
1180
+ }
1181
+ }
1611
1182
  await this.startPlugins();
1612
1183
  this.log.debug('Starting start matter interval in childbridge mode...');
1613
1184
  let failCount = 0;
1614
1185
  this.startMatterInterval = setInterval(async () => {
1615
1186
  let allStarted = true;
1616
1187
  for (const plugin of this.plugins) {
1617
- // Prevents to start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
1618
1188
  if (!plugin.enabled)
1619
1189
  continue;
1620
1190
  if (plugin.error) {
@@ -1642,23 +1212,19 @@ export class Matterbridge extends EventEmitter {
1642
1212
  clearInterval(this.startMatterInterval);
1643
1213
  this.startMatterInterval = undefined;
1644
1214
  this.log.debug('Cleared startMatterInterval interval in childbridge mode');
1645
- // Start the Matter server
1646
- await this.startMatterServer();
1647
- this.log.notice('Matter server started');
1648
- // Configure the plugins
1649
1215
  this.configureTimeout = setTimeout(async () => {
1650
1216
  for (const plugin of this.plugins) {
1651
1217
  if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
1652
1218
  continue;
1653
1219
  try {
1654
- await this.plugins.configure(plugin); // TODO No await do it in parallel
1220
+ await this.plugins.configure(plugin);
1655
1221
  }
1656
1222
  catch (error) {
1657
1223
  plugin.error = true;
1658
1224
  this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
1659
1225
  }
1660
1226
  }
1661
- this.wssSendRefreshRequired();
1227
+ this.frontend.wssSendRefreshRequired();
1662
1228
  }, 30 * 1000);
1663
1229
  for (const plugin of this.plugins) {
1664
1230
  if (!plugin.enabled || plugin.error)
@@ -1667,8 +1233,8 @@ export class Matterbridge extends EventEmitter {
1667
1233
  this.log.error(`Plugin ${plg}${plugin.name}${er} didn't add any devices to Matterbridge. Verify the plugin configuration.`);
1668
1234
  continue;
1669
1235
  }
1670
- if (!plugin.commissioningServer) {
1671
- this.log.error(`Commissioning server not found for plugin ${plg}${plugin.name}${er}`);
1236
+ if (!plugin.serverNode) {
1237
+ this.log.error(`Server node not found for plugin ${plg}${plugin.name}${er}`);
1672
1238
  continue;
1673
1239
  }
1674
1240
  if (!plugin.storageContext) {
@@ -1679,919 +1245,338 @@ export class Matterbridge extends EventEmitter {
1679
1245
  this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
1680
1246
  continue;
1681
1247
  }
1682
- await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
1683
- // Setting reachability to true
1248
+ this.startServerNode(plugin.serverNode);
1684
1249
  plugin.reachabilityTimeout = setTimeout(() => {
1685
1250
  this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
1686
- if (plugin.commissioningServer)
1687
- this.setCommissioningServerReachability(plugin.commissioningServer, true);
1251
+ if (plugin.serverNode)
1252
+ this.setServerNodeReachability(plugin.serverNode, true);
1688
1253
  if (plugin.type === 'AccessoryPlatform' && plugin.device)
1689
1254
  this.setDeviceReachability(plugin.device, true);
1690
- if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
1691
- this.setAggregatorReachability(plugin.aggregator, true);
1255
+ if (plugin.type === 'DynamicPlatform' && plugin.aggregatorNode)
1256
+ this.setAggregatorReachability(plugin.aggregatorNode, true);
1257
+ this.frontend.wssSendRefreshRequired();
1692
1258
  }, 60 * 1000);
1693
1259
  }
1694
1260
  }, 1000);
1695
1261
  }
1696
- /**
1697
- * Starts the Matterbridge controller.
1698
- * @private
1699
- * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
1700
- */
1701
1262
  async startController() {
1702
- if (!this.storageManager) {
1703
- this.log.error('No storage manager initialized');
1704
- await this.cleanup('No storage manager initialized');
1705
- return;
1706
- }
1707
- this.log.info('Creating context: mattercontrollerContext');
1708
- this.mattercontrollerContext = this.storageManager.createContext('mattercontrollerContext');
1709
- if (!this.mattercontrollerContext) {
1710
- this.log.error('No storage context mattercontrollerContext initialized');
1711
- await this.cleanup('No storage context mattercontrollerContext initialized');
1712
- return;
1713
- }
1714
- this.log.debug('Starting matterbridge in mode', this.bridgeMode);
1715
- this.matterServer = await this.createMatterServer(this.storageManager);
1716
- this.log.info('Creating matter commissioning controller');
1717
- this.commissioningController = new CommissioningController({
1718
- autoConnect: false,
1263
+ }
1264
+ async startMatterStorage() {
1265
+ this.log.info(`Starting matter node storage...`);
1266
+ this.matterStorageService = this.environment.get(StorageService);
1267
+ this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
1268
+ this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
1269
+ this.log.info('Matter node storage manager "Matterbridge" created');
1270
+ this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, 'Matterbridge', this.aggregatorProductId, 'Matterbridge aggregator');
1271
+ this.log.info('Matter node storage started');
1272
+ await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
1273
+ }
1274
+ async backupMatterStorage(storageName, backupName) {
1275
+ this.log.info('Creating matter node storage backup...');
1276
+ await copyDirectory(storageName, backupName);
1277
+ this.log.info('Created matter node storage backup');
1278
+ }
1279
+ async stopMatterStorage() {
1280
+ this.log.info('Closing matter node storage...');
1281
+ this.matterStorageManager?.close();
1282
+ this.matterStorageService = undefined;
1283
+ this.matterStorageManager = undefined;
1284
+ this.matterbridgeContext = undefined;
1285
+ this.log.info('Matter node storage closed');
1286
+ }
1287
+ async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
1288
+ if (!this.matterStorageService)
1289
+ throw new Error('No storage service initialized');
1290
+ this.log.info(`Creating server node storage context "${pluginName}.persist" for ${pluginName}...`);
1291
+ const storageManager = await this.matterStorageService.open(pluginName);
1292
+ const storageContext = storageManager.createContext('persist');
1293
+ const random = randomBytes(8).toString('hex');
1294
+ await storageContext.set('storeId', pluginName);
1295
+ await storageContext.set('deviceName', deviceName);
1296
+ await storageContext.set('deviceType', deviceType);
1297
+ await storageContext.set('vendorId', vendorId);
1298
+ await storageContext.set('vendorName', vendorName.slice(0, 32));
1299
+ await storageContext.set('productId', productId);
1300
+ await storageContext.set('productName', productName.slice(0, 32));
1301
+ await storageContext.set('nodeLabel', productName.slice(0, 32));
1302
+ await storageContext.set('productLabel', productName.slice(0, 32));
1303
+ await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
1304
+ await storageContext.set('uniqueId', await storageContext.get('uniqueId', 'UI' + random));
1305
+ await storageContext.set('softwareVersion', this.matterbridgeVersion !== '' && this.matterbridgeVersion.includes('.') ? parseInt(this.matterbridgeVersion.split('.')[0], 10) : 1);
1306
+ await storageContext.set('softwareVersionString', this.matterbridgeVersion !== '' ? this.matterbridgeVersion : '1.0.0');
1307
+ await storageContext.set('hardwareVersion', this.systemInformation.osRelease !== '' && this.systemInformation.osRelease.includes('.') ? parseInt(this.systemInformation.osRelease.split('.')[0], 10) : 1);
1308
+ await storageContext.set('hardwareVersionString', this.systemInformation.osRelease !== '' ? this.systemInformation.osRelease : '1.0.0');
1309
+ this.log.debug(`Created server node storage context "${pluginName}.persist" for ${pluginName}:`);
1310
+ this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
1311
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
1312
+ this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1313
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
1314
+ this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
1315
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1316
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1317
+ return storageContext;
1318
+ }
1319
+ async createServerNode(storageContext, port = 5540, passcode = 20242025, discriminator = 3850) {
1320
+ const storeId = await storageContext.get('storeId');
1321
+ this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
1322
+ this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
1323
+ this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1324
+ this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
1325
+ this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
1326
+ this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
1327
+ this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1328
+ const serverNode = await ServerNode.create({
1329
+ id: storeId,
1330
+ network: {
1331
+ listeningAddressIpv4: this.ipv4address,
1332
+ listeningAddressIpv6: this.ipv6address,
1333
+ port,
1334
+ },
1335
+ commissioning: {
1336
+ passcode,
1337
+ discriminator,
1338
+ },
1339
+ productDescription: {
1340
+ name: await storageContext.get('deviceName'),
1341
+ deviceType: DeviceTypeId(await storageContext.get('deviceType')),
1342
+ },
1343
+ basicInformation: {
1344
+ vendorId: VendorId(await storageContext.get('vendorId')),
1345
+ vendorName: await storageContext.get('vendorName'),
1346
+ productId: await storageContext.get('productId'),
1347
+ productName: await storageContext.get('productName'),
1348
+ productLabel: await storageContext.get('productName'),
1349
+ nodeLabel: await storageContext.get('productName'),
1350
+ serialNumber: await storageContext.get('serialNumber'),
1351
+ uniqueId: await storageContext.get('uniqueId'),
1352
+ softwareVersion: await storageContext.get('softwareVersion'),
1353
+ softwareVersionString: await storageContext.get('softwareVersionString'),
1354
+ hardwareVersion: await storageContext.get('hardwareVersion'),
1355
+ hardwareVersionString: await storageContext.get('hardwareVersionString'),
1356
+ },
1719
1357
  });
1720
- this.log.info('Adding matter commissioning controller to matter server');
1721
- await this.matterServer.addCommissioningController(this.commissioningController);
1722
- this.log.info('Starting matter server');
1723
- await this.matterServer.start();
1724
- this.log.info('Matter server started');
1725
- if (hasParameter('pairingcode')) {
1726
- this.log.info('Pairing device with pairingcode:', getParameter('pairingcode'));
1727
- const pairingCode = getParameter('pairingcode');
1728
- const ip = this.mattercontrollerContext.has('ip') ? this.mattercontrollerContext.get('ip') : undefined;
1729
- const port = this.mattercontrollerContext.has('port') ? this.mattercontrollerContext.get('port') : undefined;
1730
- let longDiscriminator, setupPin, shortDiscriminator;
1731
- if (pairingCode !== undefined) {
1732
- const pairingCodeCodec = ManualPairingCodeCodec.decode(pairingCode);
1733
- shortDiscriminator = pairingCodeCodec.shortDiscriminator;
1734
- longDiscriminator = undefined;
1735
- setupPin = pairingCodeCodec.passcode;
1736
- this.log.info(`Data extracted from pairing code: ${Logger.toJSON(pairingCodeCodec)}`);
1358
+ const sanitizeFabrics = (fabrics) => {
1359
+ const sanitizedFabrics = this.sanitizeFabricInformations(Array.from(Object.values(fabrics)));
1360
+ this.log.info(`Fabrics: ${debugStringify(sanitizedFabrics)}`);
1361
+ if (this.bridgeMode === 'bridge') {
1362
+ this.matterbridgeFabricInformations = sanitizedFabrics;
1363
+ this.matterbridgePaired = true;
1364
+ }
1365
+ if (this.bridgeMode === 'childbridge') {
1366
+ const plugin = this.plugins.get(storeId);
1367
+ if (plugin) {
1368
+ plugin.fabricInformations = sanitizedFabrics;
1369
+ plugin.paired = true;
1370
+ }
1371
+ }
1372
+ };
1373
+ serverNode.lifecycle.commissioned.on(() => this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`));
1374
+ serverNode.lifecycle.decommissioned.on(() => this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`));
1375
+ serverNode.lifecycle.online.on(() => {
1376
+ this.log.notice(`Server node for ${storeId} is online`);
1377
+ if (!serverNode.lifecycle.isCommissioned) {
1378
+ this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
1379
+ const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
1380
+ if (this.bridgeMode === 'bridge') {
1381
+ this.matterbridgeQrPairingCode = qrPairingCode;
1382
+ this.matterbridgeManualPairingCode = manualPairingCode;
1383
+ this.matterbridgeFabricInformations = [];
1384
+ this.matterbridgeSessionInformations = [];
1385
+ this.matterbridgePaired = false;
1386
+ this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
1387
+ this.log.notice(`Manual pairing code: ${manualPairingCode}`);
1388
+ }
1389
+ if (this.bridgeMode === 'childbridge') {
1390
+ const plugin = this.plugins.get(storeId);
1391
+ if (plugin) {
1392
+ plugin.qrPairingCode = qrPairingCode;
1393
+ plugin.manualPairingCode = manualPairingCode;
1394
+ plugin.fabricInformations = [];
1395
+ plugin.sessionInformations = [];
1396
+ plugin.paired = false;
1397
+ this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
1398
+ this.log.notice(`Manual pairing code: ${manualPairingCode}`);
1399
+ }
1400
+ }
1737
1401
  }
1738
1402
  else {
1739
- longDiscriminator = await this.mattercontrollerContext.get('longDiscriminator', 3840);
1740
- if (longDiscriminator > 4095)
1741
- throw new Error('Discriminator value must be less than 4096');
1742
- setupPin = this.mattercontrollerContext.get('pin', 20202021);
1403
+ this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
1404
+ sanitizeFabrics(serverNode.state.commissioning.fabrics);
1743
1405
  }
1744
- if ((shortDiscriminator === undefined && longDiscriminator === undefined) || setupPin === undefined) {
1745
- throw new Error('Please specify the longDiscriminator of the device to commission with -longDiscriminator or provide a valid passcode with -passcode');
1406
+ this.frontend.wssSendRefreshRequired();
1407
+ });
1408
+ serverNode.lifecycle.offline.on(() => {
1409
+ this.log.notice(`Server node for ${storeId} is offline`);
1410
+ if (this.bridgeMode === 'bridge') {
1411
+ this.matterbridgeQrPairingCode = undefined;
1412
+ this.matterbridgeManualPairingCode = undefined;
1413
+ this.matterbridgeFabricInformations = [];
1414
+ this.matterbridgeSessionInformations = [];
1415
+ this.matterbridgePaired = false;
1746
1416
  }
1747
- const commissioningOptions = {
1748
- regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.IndoorOutdoor,
1749
- regulatoryCountryCode: 'XX',
1750
- };
1751
- const options = {
1752
- commissioning: commissioningOptions,
1753
- discovery: {
1754
- knownAddress: ip !== undefined && port !== undefined ? { ip, port, type: 'udp' } : undefined,
1755
- identifierData: longDiscriminator !== undefined ? { longDiscriminator } : shortDiscriminator !== undefined ? { shortDiscriminator } : {},
1756
- },
1757
- passcode: setupPin,
1758
- };
1759
- this.log.info('Commissioning with options:', options);
1760
- const nodeId = await this.commissioningController.commissionNode(options);
1761
- this.log.info(`Commissioning successfully done with nodeId: ${nodeId}`);
1762
- this.log.info('ActiveSessionInformation:', this.commissioningController.getActiveSessionInformation());
1763
- } // (hasParameter('pairingcode'))
1764
- if (hasParameter('unpairall')) {
1765
- this.log.info('***Commissioning controller unpairing all nodes...');
1766
- const nodeIds = this.commissioningController.getCommissionedNodes();
1767
- for (const nodeId of nodeIds) {
1768
- this.log.info('***Commissioning controller unpairing node:', nodeId);
1769
- await this.commissioningController.removeNode(nodeId);
1417
+ if (this.bridgeMode === 'childbridge') {
1418
+ const plugin = this.plugins.get(storeId);
1419
+ if (plugin) {
1420
+ plugin.qrPairingCode = undefined;
1421
+ plugin.manualPairingCode = undefined;
1422
+ plugin.fabricInformations = [];
1423
+ plugin.sessionInformations = [];
1424
+ plugin.paired = false;
1425
+ }
1426
+ }
1427
+ this.frontend.wssSendRefreshRequired();
1428
+ });
1429
+ serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
1430
+ let action = '';
1431
+ switch (fabricAction) {
1432
+ case FabricAction.Added:
1433
+ action = 'added';
1434
+ break;
1435
+ case FabricAction.Removed:
1436
+ action = 'removed';
1437
+ break;
1438
+ case FabricAction.Updated:
1439
+ action = 'updated';
1440
+ break;
1441
+ }
1442
+ this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
1443
+ sanitizeFabrics(serverNode.state.commissioning.fabrics);
1444
+ this.frontend.wssSendRefreshRequired();
1445
+ });
1446
+ const sanitizeSessions = (sessions) => {
1447
+ const sanitizedSessions = this.sanitizeSessionInformation(sessions.map((session) => ({
1448
+ ...session,
1449
+ secure: session.name.startsWith('secure'),
1450
+ })));
1451
+ this.log.debug(`Sessions: ${debugStringify(sanitizedSessions)}`);
1452
+ if (this.bridgeMode === 'bridge') {
1453
+ this.matterbridgeSessionInformations = sanitizedSessions;
1454
+ }
1455
+ if (this.bridgeMode === 'childbridge') {
1456
+ const plugin = this.plugins.get(storeId);
1457
+ if (plugin) {
1458
+ plugin.sessionInformations = sanitizedSessions;
1459
+ }
1770
1460
  }
1461
+ };
1462
+ serverNode.events.sessions.opened.on((session) => {
1463
+ this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
1464
+ sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1465
+ this.frontend.wssSendRefreshRequired();
1466
+ });
1467
+ serverNode.events.sessions.closed.on((session) => {
1468
+ this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
1469
+ sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1470
+ this.frontend.wssSendRefreshRequired();
1471
+ });
1472
+ serverNode.events.sessions.subscriptionsChanged.on((session) => {
1473
+ this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
1474
+ sanitizeSessions(Object.values(serverNode.state.sessions.sessions));
1475
+ this.frontend.wssSendRefreshRequired();
1476
+ });
1477
+ this.log.info(`Created server node for ${storeId}`);
1478
+ return serverNode;
1479
+ }
1480
+ async startServerNode(matterServerNode) {
1481
+ if (!matterServerNode)
1771
1482
  return;
1772
- }
1773
- if (hasParameter('discover')) {
1774
- // const discover = await this.commissioningController.discoverCommissionableDevices({ productId: 0x8000, deviceType: 0xfff1 });
1775
- // console.log(discover);
1776
- }
1777
- if (!this.commissioningController.isCommissioned()) {
1778
- this.log.info('***Commissioning controller is not commissioned: use matterbridge -controller -pairingcode [pairingcode] to commission a device');
1483
+ this.log.notice(`Starting ${matterServerNode.id} server node`);
1484
+ await matterServerNode.start();
1485
+ }
1486
+ async stopServerNode(matterServerNode) {
1487
+ if (!matterServerNode)
1779
1488
  return;
1780
- }
1781
- const nodeIds = this.commissioningController.getCommissionedNodes();
1782
- this.log.info(`***Commissioning controller is commissioned ${this.commissioningController.isCommissioned()} and has ${nodeIds.length} nodes commisioned: `);
1783
- for (const nodeId of nodeIds) {
1784
- this.log.info(`***Connecting to commissioned node: ${nodeId}`);
1785
- const node = await this.commissioningController.connectNode(nodeId, {
1786
- autoSubscribe: false,
1787
- attributeChangedCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, attributeName }, value }) => this.log.info(`***Commissioning controller attributeChangedCallback ${peerNodeId}: attribute ${nodeId}/${endpointId}/${clusterId}/${attributeName} changed to ${Logger.toJSON(value)}`),
1788
- eventTriggeredCallback: (peerNodeId, { path: { nodeId, clusterId, endpointId, eventName }, events }) => this.log.info(`***Commissioning controller eventTriggeredCallback ${peerNodeId}: Event ${nodeId}/${endpointId}/${clusterId}/${eventName} triggered with ${Logger.toJSON(events)}`),
1789
- stateInformationCallback: (peerNodeId, info) => {
1790
- switch (info) {
1791
- case NodeStateInformation.Connected:
1792
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} connected`);
1793
- break;
1794
- case NodeStateInformation.Disconnected:
1795
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} disconnected`);
1796
- break;
1797
- case NodeStateInformation.Reconnecting:
1798
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} reconnecting`);
1799
- break;
1800
- case NodeStateInformation.WaitingForDeviceDiscovery:
1801
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} waiting for device discovery`);
1802
- break;
1803
- case NodeStateInformation.StructureChanged:
1804
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} structure changed`);
1805
- break;
1806
- case NodeStateInformation.Decommissioned:
1807
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} decommissioned`);
1808
- break;
1809
- default:
1810
- this.log.info(`***Commissioning controller stateInformationCallback ${peerNodeId}: Node ${nodeId} NodeStateInformation.${info}`);
1811
- break;
1812
- }
1813
- },
1814
- });
1815
- node.logStructure();
1816
- // Get the interaction client
1817
- this.log.info('Getting the interaction client');
1818
- const interactionClient = await node.getInteractionClient();
1819
- let cluster;
1820
- let attributes;
1821
- // Log BasicInformationCluster
1822
- cluster = BasicInformationCluster;
1823
- attributes = await interactionClient.getMultipleAttributes({
1824
- attributes: [{ clusterId: cluster.id }],
1825
- });
1826
- if (attributes.length > 0)
1827
- this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1828
- attributes.forEach((attribute) => {
1829
- this.log.info(`- 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}`);
1830
- });
1831
- // Log PowerSourceCluster
1832
- cluster = PowerSourceCluster;
1833
- attributes = await interactionClient.getMultipleAttributes({
1834
- attributes: [{ clusterId: cluster.id }],
1835
- });
1836
- if (attributes.length > 0)
1837
- this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1838
- attributes.forEach((attribute) => {
1839
- this.log.info(`- 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}`);
1840
- });
1841
- // Log ThreadNetworkDiagnostics
1842
- cluster = ThreadNetworkDiagnosticsCluster;
1843
- attributes = await interactionClient.getMultipleAttributes({
1844
- attributes: [{ clusterId: cluster.id }],
1845
- });
1846
- if (attributes.length > 0)
1847
- this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1848
- attributes.forEach((attribute) => {
1849
- this.log.info(`- 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}`);
1850
- });
1851
- // Log SwitchCluster
1852
- cluster = SwitchCluster;
1853
- attributes = await interactionClient.getMultipleAttributes({
1854
- attributes: [{ clusterId: cluster.id }],
1855
- });
1856
- if (attributes.length > 0)
1857
- this.log.info(`Cluster: ${idn}${cluster.name}${rs}${nf} attributes:`);
1858
- attributes.forEach((attribute) => {
1859
- this.log.info(`- 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}`);
1860
- });
1861
- this.log.info('Subscribing to all attributes and events');
1862
- await node.subscribeAllAttributesAndEvents({
1863
- ignoreInitialTriggers: false,
1864
- attributeChangedCallback: ({ path: { nodeId, clusterId, endpointId, attributeName }, version, value }) => this.log.info(`***${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}`),
1865
- eventTriggeredCallback: ({ path: { nodeId, clusterId, endpointId, eventName }, events }) => {
1866
- this.log.info(`***${db}Commissioning controller eventTriggeredCallback: event ${BLUE}${nodeId}${db}/${or}${endpointId}${db}/${hk}${getClusterNameById(clusterId)}${db}/${zb}${eventName}${db} triggered with ${debugStringify(events ?? { none: true })}`);
1867
- },
1868
- });
1869
- this.log.info('Subscribed to all attributes and events');
1870
- }
1489
+ this.log.notice(`Closing ${matterServerNode.id} server node`);
1490
+ await matterServerNode.close();
1871
1491
  }
1872
- /** ***********************************************************************************************************************************/
1873
- /** Matter.js methods */
1874
- /** ***********************************************************************************************************************************/
1875
- /**
1876
- * Starts the matter storage process based on the specified storage type and name.
1877
- * @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
1878
- * @param {string} storageName - The name of the storage file.
1879
- * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1880
- */
1881
- async startMatterStorage(storageType, storageName) {
1882
- this.log.debug(`Starting matter ${storageType} storage ${CYAN}${storageName}${db}`);
1883
- if (storageType === 'disk') {
1884
- const storageDisk = new StorageBackendDisk(storageName);
1885
- this.storageManager = new StorageManager(storageDisk);
1886
- }
1887
- else if (storageType === 'json') {
1888
- if (!storageName.endsWith('.json'))
1889
- storageName += '.json';
1890
- const storageJson = new StorageBackendJsonFile(storageName);
1891
- this.storageManager = new StorageManager(storageJson);
1892
- }
1893
- else {
1894
- this.log.error(`Unsupported matter storage type ${storageType}`);
1895
- await this.cleanup('Unsupported matter storage type');
1492
+ async createAggregatorNode(storageContext) {
1493
+ this.log.notice(`Creating ${await storageContext.get('storeId')} aggregator `);
1494
+ const aggregatorNode = new EndpointNode(AggregatorEndpoint, { id: `${await storageContext.get('storeId')}` });
1495
+ return aggregatorNode;
1496
+ }
1497
+ async addBridgedEndpoint(pluginName, device) {
1498
+ const plugin = this.plugins.get(pluginName);
1499
+ if (!plugin) {
1500
+ this.log.error(`Error adding bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.id}${er}) plugin ${plg}${pluginName}${er} not found`);
1896
1501
  return;
1897
1502
  }
1898
- try {
1899
- await this.storageManager.initialize();
1900
- this.log.debug('Matter storage initialized');
1901
- if (storageType === 'json') {
1902
- await this.backupMatterStorage(storageName, storageName.replace('.json', '') + '.backup.json');
1903
- }
1503
+ if (this.bridgeMode === 'bridge') {
1504
+ this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to Matterbridge aggregator node`);
1505
+ if (!this.aggregatorNode)
1506
+ this.log.error('Aggregator node not found for Matterbridge');
1507
+ await this.aggregatorNode?.add(device);
1904
1508
  }
1905
- catch (error) {
1906
- this.log.error(`Matter storage initialize error! The file .matterbridge/${storageName} may be corrupted: ${error instanceof Error ? error.message : error}`);
1907
- if (hasParameter('norestore')) {
1908
- this.log.fatal(`Please delete it and rename ${storageName.replace('.json', '.backup.json')} to ${storageName} and try to restart Matterbridge.`);
1909
- await this.cleanup('Matter storage initialize error and -norestore parameter found!');
1910
- return;
1911
- }
1912
- await this.restoreMatterStorage(storageName.replace('.json', '') + '.backup.json', storageName);
1913
- try {
1914
- await this.storageManager.initialize();
1915
- this.log.notice('Matter storage initialized from the backup file');
1509
+ else if (this.bridgeMode === 'childbridge') {
1510
+ if (plugin.type === 'AccessoryPlatform') {
1511
+ this.createAccessoryPlugin(plugin, device);
1916
1512
  }
1917
- catch (error) {
1918
- this.log.error(`Matter storage initialize error! The backup file for .matterbridge/${storageName} may be corrupted too:`, error instanceof Error ? error.message : error);
1919
- await this.cleanup('Matter storage initialize error from backup!');
1920
- return;
1513
+ if (plugin.type === 'DynamicPlatform') {
1514
+ plugin.locked = true;
1515
+ this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} aggregator node`);
1516
+ if (!plugin.aggregatorNode)
1517
+ this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${db}`);
1518
+ await plugin.aggregatorNode?.add(device);
1921
1519
  }
1922
1520
  }
1923
- this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
1924
- this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', aggregator.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
1925
- await this.matterbridgeContext.set('port', this.port);
1926
- await this.matterbridgeContext.set('passcode', this.passcode);
1927
- await this.matterbridgeContext.set('discriminator', this.discriminator);
1521
+ if (plugin.registeredDevices !== undefined)
1522
+ plugin.registeredDevices++;
1523
+ if (plugin.addedDevices !== undefined)
1524
+ plugin.addedDevices++;
1525
+ this.devices.set(device);
1526
+ 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}`);
1928
1527
  }
1929
- /**
1930
- * Convert the old API matter storage to the new API format.
1931
- * @param {StorageContext} context - The context of Matterbridge or of the plugin.
1932
- * @param {string} pluginName - The name of the plugin or Matterbridge.
1933
- * @returns {Promise<void>} - A promise that resolves when the storage process is started.
1934
- */
1935
- async convertStorage(context, pluginName) {
1936
- if (this.edge !== false)
1528
+ async removeBridgedEndpoint(pluginName, device) {
1529
+ this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
1530
+ const plugin = this.plugins.get(pluginName);
1531
+ if (!plugin) {
1532
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
1937
1533
  return;
1938
- try {
1939
- const storageService = Environment.default.get(StorageService);
1940
- Environment.default.vars.set('path.root', path.join(this.matterbridgeDirectory, 'matterstorage' + (this.profile ? '.' + this.profile : '')));
1941
- const nodeStorage = await storageService.open(pluginName);
1942
- if ((await nodeStorage.createContext('root').createContext('generalDiagnostics').get('rebootCount', -1)) >= 0) {
1943
- this.log.info(`Matter node storage already converted to Matterbridge edge for ${plg}${pluginName}${nf}`);
1944
- return;
1945
- }
1946
- else {
1947
- this.log.notice(`Converting matter node storage to Matterbridge edge for ${plg}${pluginName}${nt}...`);
1948
- }
1949
- // Read FabricManager from the old storage and get FabricManager.fabrics and FabricManager.nextFabricIndex
1950
- const fabricManagerContext = context.createContext('FabricManager');
1951
- const fabrics = (await fabricManagerContext.get('fabrics', []));
1952
- const nextFabricIndex = await fabricManagerContext.get('nextFabricIndex', 0);
1953
- // Read EventHandler from the old storage
1954
- const eventHandlerContext = context.createContext('EventHandler');
1955
- // Read SessionManager from the old storage
1956
- const sessionManagerContext = context.createContext('SessionManager');
1957
- // Read EndpointStructure from the old storage
1958
- const endpointStructureContext = context.createContext('EndpointStructure');
1959
- // Read generalCommissioning from the old storage
1960
- const generalCommissioningContext = context.createContext('Cluster-0-48');
1961
- // Read basicInformation from the old storage
1962
- const basicInformationContext = context.createContext('Cluster-0-40');
1963
- const fabricInfo = {};
1964
- const fabricInfoArray = [];
1965
- const nocArray = [];
1966
- const trcArray = [];
1967
- const aclArray = [];
1968
- this.log.info(`Found ${CYAN}${fabrics.length}${nf} fabrics (nextFabricIndex ${CYAN}${nextFabricIndex}${nf}) for ${plg}${pluginName}${nf}:`);
1969
- if (fabrics.length === 0 || nextFabricIndex === 0) {
1970
- this.log.notice(`If you want to try out matterbridge edge add -edge to the command line and pair it to your controller(s).`);
1534
+ }
1535
+ if (this.bridgeMode === 'bridge') {
1536
+ if (!this.aggregatorNode) {
1537
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
1971
1538
  return;
1972
1539
  }
1973
- for (const fabric of fabrics) {
1974
- this.log.info(`- fabricIndex ${CYAN}${fabric.fabricIndex}${nf} fabricId ${CYAN}${fabric.fabricId}${nf} nodeId ${CYAN}${fabric.nodeId}${nf} rootNodeId ${CYAN}${fabric.rootNodeId}${nf} rootVendorId ${CYAN}${fabric.rootVendorId}${nf} label ${CYAN}${fabric.label}${nf}`);
1975
- fabricInfo[fabric.fabricIndex] = {
1976
- fabricIndex: fabric.fabricIndex,
1977
- fabricId: fabric.fabricId,
1978
- nodeId: fabric.nodeId,
1979
- rootNodeId: fabric.rootNodeId,
1980
- rootVendorId: fabric.rootVendorId,
1981
- label: fabric.label,
1982
- };
1983
- fabricInfoArray.push({
1984
- fabricIndex: fabric.fabricIndex,
1985
- fabricId: fabric.fabricId,
1986
- nodeId: fabric.nodeId,
1987
- vendorId: fabric.rootVendorId,
1988
- rootPublicKey: fabric.rootPublicKey,
1989
- label: fabric.label,
1990
- });
1991
- nocArray.push({ noc: fabric.operationalCert, icac: null, fabricIndex: fabric.fabricIndex });
1992
- trcArray.push(fabric.rootCert);
1993
- this.log.info(`- updating ACL for fabricIndex ${fabric.fabricIndex}:`, fabric.scopedClusterData);
1994
- const acl = fabric.scopedClusterData.get(0x1f)?.get('acl');
1995
- if (acl && acl.value.length > 0) {
1996
- aclArray.push(acl.value[0]);
1997
- this.log.info(`- ACL updated to ${debugStringify(acl.value)}${nf} for fabricIndex ${CYAN}${fabric.fabricIndex}${nf}`);
1998
- }
1999
- else {
2000
- const defaultAcl = { fabricIndex: fabric.fabricIndex, privilege: 5, authMode: 2, subjects: [fabric.rootNodeId], targets: null };
2001
- aclArray.push(defaultAcl);
2002
- this.log.info(`- ACL updated to default ${debugStringify(defaultAcl)}${nf} for fabricIndex ${CYAN}${fabric.fabricIndex}${nf}`);
2003
- }
2004
- }
2005
- await nodeStorage.createContext('fabrics').set('fabrics', fabrics);
2006
- await nodeStorage.createContext('fabrics').set('nextFabricIndex', nextFabricIndex);
2007
- await nodeStorage.createContext('sessions').set('resumptionRecords', await sessionManagerContext.get('resumptionRecords', []));
2008
- await nodeStorage.createContext('events').set('lastEventNumber', await eventHandlerContext.get('lastEventNumber', 1));
2009
- await nodeStorage.createContext('root').set('__number__', 0);
2010
- await nodeStorage.createContext('root').createContext('commissioning').set('enabled', true);
2011
- await nodeStorage.createContext('root').createContext('commissioning').set('commissioned', true);
2012
- await nodeStorage.createContext('root').createContext('commissioning').set('fabrics', fabricInfo);
2013
- await nodeStorage.createContext('root').createContext('operationalCredentials').set('commissionedFabrics', fabricInfoArray.length);
2014
- await nodeStorage.createContext('root').createContext('operationalCredentials').set('fabrics', fabricInfoArray);
2015
- // operationalCredentials.nocs ==>> [{noc: fabric.operationalCert, icac: null, fabricIndex: fabric.fabricIndex }]
2016
- await nodeStorage.createContext('root').createContext('operationalCredentials').set('nocs', nocArray);
2017
- // operationalCredentials.trustedRootCertificates ==>> ["{\"__object__\":\"Uint8Array\",\"__value__\":\"" + fabric.rootCert + "\"}"]
2018
- await nodeStorage.createContext('root').createContext('operationalCredentials').set('trustedRootCertificates', trcArray);
2019
- // ACL updated, updating ACL manager { fabricIndex: 3, privilege: 5, authMode: 2, subjects: [ 18446744060825763897 ], targets: null }
2020
- // From fabric.rootNodeId if fabric.scopedClusterData.get(0x1f).get('acl') is empty
2021
- // [{"fabricIndex":3,"privilege":5,"authMode":2,"subjects":["{\"__object__\":\"BigInt\",\"__value__\":\"18446744060825763897\"}"],"targets":null}]
2022
- await nodeStorage.createContext('root').createContext('accessControl').set('acl', aclArray);
2023
- await nodeStorage
2024
- .createContext('root')
2025
- .createContext('generalCommissioning')
2026
- .set('breadcrumb', await generalCommissioningContext.get('breadcrumb', BigInt(0)));
2027
- await nodeStorage
2028
- .createContext('root')
2029
- .createContext('basicInformation')
2030
- .set('location', await basicInformationContext.get('location', 'XX'));
2031
- await nodeStorage.createContext('root').createContext('network').set('ble', false);
2032
- await nodeStorage
2033
- .createContext('root')
2034
- .createContext('network')
2035
- .set('operationalPort', await context.get('port', 5540));
2036
- await nodeStorage
2037
- .createContext('root')
2038
- .createContext('productDescription')
2039
- .set('productId', await context.get('productId', 0x8000));
2040
- await nodeStorage
2041
- .createContext('root')
2042
- .createContext('productDescription')
2043
- .set('vendorId', await context.get('vendorId', 0xfff1));
2044
- /*
2045
- "Matterbridge.EndpointStructure": {
2046
- "unique_d60ca095a002f160-index_0": 1,
2047
- "unique_d60ca095a002f160-index_0-custom_Switch0": 2,
2048
- "unique_d60ca095a002f160-index_0-custom_Outlet0": 3,
2049
- "unique_d60ca095a002f160-index_0-custom_Light0": 4,
2050
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa": 2,
2051
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_PowerSource": 3,
2052
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_light:0": 4,
2053
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_light:1": 5,
2054
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_light:2": 6,
2055
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_light:3": 7,
2056
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_meter:0": 8,
2057
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_meter:1": 9,
2058
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_meter:2": 10,
2059
- "unique_d60ca095a002f160-index_0-unique_7ddb4752b982ee108d5928e934f0fcfa-custom_meter:3": 11,
2060
- "nextEndpointId": 5
2061
- },
2062
- */
2063
- const rootDeviceName = (await context.get('deviceName', '')).replace(/[ .]/g, '');
2064
- this.log.info(`Converting ${pluginName}.EndpointStructure to root.parts.${rootDeviceName}...`);
2065
- for (const key of await endpointStructureContext.keys()) {
2066
- if (key === 'nextEndpointId') {
2067
- await nodeStorage.createContext('root').set('__nextNumber__', await endpointStructureContext.get(key));
2068
- continue;
2069
- }
2070
- const parts = key.split('-');
2071
- const number = await endpointStructureContext.get(key);
2072
- if (parts.length === 2) {
2073
- this.log.debug(`Converting bridge Matterbridge.EndpointStructure:${key}:${number} to root.parts.${rootDeviceName}.__number__:${CYAN}${number}${db}`);
2074
- await nodeStorage.createContext('root').createContext('parts').createContext(rootDeviceName).set('__number__', number);
2075
- }
2076
- else if (parts.length === 3 && parts[2].startsWith('unique_')) {
2077
- const device = this.devices.get(parts[2].replace('unique_', ''));
2078
- if (device && device.deviceName && device.maybeNumber) {
2079
- this.log.debug(`Converting unique Matterbridge.EndpointStructure:${key}:${number} to root.parts.${rootDeviceName}.parts.${device.deviceName.replace(/[ .]/g, '')}.__number__:${CYAN}${device.maybeNumber}${db}`);
2080
- await nodeStorage.createContext('root').createContext('parts').createContext(rootDeviceName).createContext('parts').createContext(device.deviceName.replace(/[ .]/g, '')).set('__number__', device.maybeNumber);
2081
- }
2082
- }
2083
- else if (parts.length === 4 && parts[2].startsWith('unique_') && parts[3].startsWith('custom_')) {
2084
- const device = this.devices.get(parts[2].replace('unique_', ''));
2085
- if (device && device.deviceName && device.maybeNumber) {
2086
- const childEndpointName = parts[3].replace('custom_', '');
2087
- const childEndpoint = device.getChildEndpointByName(childEndpointName);
2088
- this.log.debug(`Converting unique Matterbridge.EndpointStructure:${key}:${number} to root.parts.${rootDeviceName}.parts.${device.deviceName.replace(/[ .]/g, '')}.parts.${parts[3].replace('custom_', '').replace(/[ .]/g, '')}.__number__:${CYAN}${childEndpoint?.number}${db}`);
2089
- await nodeStorage
2090
- .createContext('root')
2091
- .createContext('parts')
2092
- .createContext(rootDeviceName)
2093
- .createContext('parts')
2094
- .createContext(device.deviceName.replace(/[ .]/g, ''))
2095
- .createContext('parts')
2096
- .createContext(parts[3].replace('custom_', '').replace(/[ .]/g, ''))
2097
- .set('__number__', childEndpoint?.number);
2098
- }
2099
- }
2100
- else if (parts.length === 3 && parts[2].startsWith('custom_')) {
2101
- this.log.debug(`Converting custom Matterbridge.EndpointStructure:${key}:${number} to root.parts.${rootDeviceName}.parts.${parts[2].replace('custom_', '').replace(/[ .]/g, '')}.__number__:${CYAN}${number}${db}`);
2102
- await nodeStorage.createContext('root').createContext('parts').createContext(rootDeviceName).createContext('parts').createContext(parts[2].replace('custom_', '').replace(/[ .]/g, '')).set('__number__', number);
2103
- }
2104
- else if (parts.length === 4 && parts[2].startsWith('custom_') && parts[3].startsWith('custom_')) {
2105
- this.log.debug(`Converting custom Matterbridge.EndpointStructure:${key}:${number} to root.parts.${rootDeviceName}.parts.${parts[2].replace('custom_', '').replace(/[ .]/g, '')}.parts.${parts[3].replace('custom_', '').replace(/[ .]/g, '')}.__number__:${CYAN}${number}${db}`);
2106
- await nodeStorage
2107
- .createContext('root')
2108
- .createContext('parts')
2109
- .createContext(rootDeviceName)
2110
- .createContext('parts')
2111
- .createContext(parts[2].replace('custom_', '').replace(/[ .]/g, ''))
2112
- .createContext('parts')
2113
- .createContext(parts[3].replace('custom_', '').replace(/[ .]/g, ''))
2114
- .set('__number__', number);
2115
- }
2116
- }
2117
- await nodeStorage.createContext('persist').set('converted', true);
2118
- await nodeStorage.createContext('persist').set('deviceName', await context.get('deviceName'));
2119
- await nodeStorage.createContext('persist').set('deviceType', await context.get('deviceType'));
2120
- await nodeStorage.createContext('persist').set('vendorId', await context.get('vendorId'));
2121
- await nodeStorage.createContext('persist').set('vendorName', await context.get('vendorName'));
2122
- await nodeStorage.createContext('persist').set('productId', await context.get('productId'));
2123
- await nodeStorage.createContext('persist').set('productName', await context.get('productName'));
2124
- await nodeStorage.createContext('persist').set('nodeLabel', await context.get('nodeLabel'));
2125
- await nodeStorage.createContext('persist').set('productLabel', await context.get('productLabel'));
2126
- await nodeStorage.createContext('persist').set('serialNumber', 'SN' + (await context.get('serialNumber')));
2127
- await nodeStorage.createContext('persist').set('uniqueId', await context.get('uniqueId'));
2128
- await nodeStorage.createContext('persist').set('softwareVersion', await context.get('softwareVersion'));
2129
- await nodeStorage.createContext('persist').set('softwareVersionString', await context.get('softwareVersionString'));
2130
- await nodeStorage.createContext('persist').set('hardwareVersion', await context.get('hardwareVersion'));
2131
- await nodeStorage.createContext('persist').set('hardwareVersionString', await context.get('hardwareVersionString'));
2132
- await context.set('converted', true);
2133
- this.log.notice(`Matter storage converted to Matterbridge edge for ${plg}${pluginName}${nt}`);
2134
- this.log.notice(`If you want to try out matterbridge edge add -edge to the command line.`);
2135
- this.log.notice(`All fabrics have been converted to the new storage format.`);
2136
- }
2137
- catch (error) {
2138
- this.log.error(`convertStorage error converting matter storage to Matterbridge edge for ${plg}${pluginName}${er}:`, error);
2139
- }
2140
- }
2141
- /**
2142
- * Makes a backup copy of the specified matter JSON storage file.
2143
- *
2144
- * @param storageName - The name of the JSON storage file to be backed up.
2145
- * @param backupName - The name of the backup file to be created.
2146
- */
2147
- async backupMatterStorage(storageName, backupName) {
2148
- try {
2149
- this.log.debug(`Making backup copy of ${storageName}`);
2150
- await fs.copyFile(storageName, backupName);
2151
- this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
1540
+ await device.delete();
1541
+ this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1542
+ if (plugin.registeredDevices !== undefined)
1543
+ plugin.registeredDevices--;
1544
+ if (plugin.addedDevices !== undefined)
1545
+ plugin.addedDevices--;
2152
1546
  }
2153
- catch (err) {
2154
- if (err instanceof Error && 'code' in err) {
2155
- if (err.code === 'ENOENT') {
2156
- this.log.debug(`No existing file to back up for ${storageName}. This is expected on the first run.`);
2157
- }
2158
- else {
2159
- this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
2160
- }
2161
- }
2162
- else {
2163
- this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
1547
+ else if (this.bridgeMode === 'childbridge') {
1548
+ if (plugin.type === 'AccessoryPlatform') {
2164
1549
  }
2165
- }
2166
- }
2167
- /**
2168
- * Restore the specified matter JSON storage file.
2169
- *
2170
- * @param backupName - The name of the backup file to restore from.
2171
- * @param storageName - The name of the JSON storage file to restored.
2172
- */
2173
- async restoreMatterStorage(backupName, storageName) {
2174
- try {
2175
- this.log.notice(`Restoring the backup copy of ${storageName}`);
2176
- await fs.copyFile(backupName, storageName);
2177
- this.log.notice(`Successfully restored ${backupName} to ${storageName}`);
2178
- }
2179
- catch (err) {
2180
- if (err instanceof Error && 'code' in err) {
2181
- if (err.code === 'ENOENT') {
2182
- this.log.info(`No existing file to restore: ${backupName}.`);
2183
- }
2184
- else {
2185
- this.log.error(`Error restoring ${backupName}: ${err.message}`);
1550
+ else if (plugin.type === 'DynamicPlatform') {
1551
+ if (!plugin.aggregatorNode) {
1552
+ this.log.error(`Error removing bridged endpoint ${dev}${device.deviceName}${er} (${zb}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator node not found`);
1553
+ return;
2186
1554
  }
1555
+ await device.delete();
2187
1556
  }
2188
- else {
2189
- this.log.error(`An unexpected error occurred during the restore of ${backupName}: ${String(err)}`);
2190
- }
2191
- }
2192
- }
2193
- /**
2194
- * Stops the matter storage.
2195
- * @returns {Promise<void>} A promise that resolves when the storage is stopped.
2196
- */
2197
- async stopMatterStorage() {
2198
- this.log.debug('Stopping storage');
2199
- await this.storageManager?.close();
2200
- this.log.debug('Storage closed');
2201
- this.storageManager = undefined;
2202
- this.matterbridgeContext = undefined;
2203
- this.mattercontrollerContext = undefined;
2204
- }
2205
- /**
2206
- * Creates a Matter server using the provided storage manager and the provided mdnsInterface.
2207
- * @param storageManager The storage manager to be used by the Matter server.
2208
- *
2209
- */
2210
- async createMatterServer(storageManager) {
2211
- this.log.debug('Creating matter server');
2212
- // Validate mdnsInterface
2213
- if (this.mdnsInterface) {
2214
- const networkInterfaces = os.networkInterfaces();
2215
- const availableInterfaces = Object.keys(networkInterfaces);
2216
- if (!availableInterfaces.includes(this.mdnsInterface)) {
2217
- this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
2218
- this.mdnsInterface = undefined;
2219
- }
2220
- else {
2221
- this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
2222
- }
2223
- }
2224
- const matterServer = new MatterServer(storageManager, { mdnsInterface: this.mdnsInterface });
2225
- this.log.debug(`Created matter server with mdnsInterface: ${this.mdnsInterface ?? 'all available interfaces'}`);
2226
- return matterServer;
2227
- }
2228
- /**
2229
- * Starts the Matter server.
2230
- * If the Matter server is not initialized, it logs an error and performs cleanup.
2231
- */
2232
- async startMatterServer() {
2233
- if (!this.matterServer) {
2234
- this.log.error('No matter server initialized');
2235
- await this.cleanup('No matter server initialized');
2236
- return;
2237
- }
2238
- this.log.debug('Starting matter server...');
2239
- await this.matterServer.start();
2240
- this.log.debug('Started matter server');
2241
- // this.commissioningServer?.getRootEndpoint() && logEndpoint(this.commissioningServer?.getRootEndpoint());
2242
- }
2243
- /**
2244
- * Stops the Matter server, commissioningServer and commissioningController.
2245
- */
2246
- async stopMatterServer() {
2247
- this.log.debug('Stopping matter commissioningServer');
2248
- await this.commissioningServer?.close();
2249
- this.log.debug('Stopping matter commissioningController');
2250
- await this.commissioningController?.close();
2251
- this.log.debug('Stopping matter server');
2252
- await this.matterServer?.close();
2253
- this.log.debug('Matter server closed');
2254
- this.commissioningController = undefined;
2255
- this.commissioningServer = undefined;
2256
- this.matterAggregator = undefined;
2257
- this.matterServer = undefined;
2258
- }
2259
- /**
2260
- * Creates a Matter Aggregator.
2261
- * @param {StorageContext} context - The storage context.
2262
- * @returns {Aggregator} - The created Matter Aggregator.
2263
- */
2264
- async createMatterAggregator(context, pluginName) {
2265
- this.log.debug(`Creating matter aggregator for ${plg}${pluginName}${db}`);
2266
- const matterAggregator = new Aggregator();
2267
- return matterAggregator;
2268
- }
2269
- /**
2270
- * Creates a matter commissioning server.
2271
- *
2272
- * @param {StorageContext} context - The storage context.
2273
- * @param {string} pluginName - The name of the commissioning server.
2274
- * @returns {CommissioningServer} The created commissioning server.
2275
- */
2276
- async createCommisioningServer(context, pluginName) {
2277
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
2278
- const deviceName = await context.get('deviceName');
2279
- const deviceType = await context.get('deviceType');
2280
- const vendorId = await context.get('vendorId');
2281
- const vendorName = await context.get('vendorName'); // Home app = Manufacturer
2282
- const productId = await context.get('productId');
2283
- const productName = await context.get('productName'); // Home app = Model
2284
- const serialNumber = await context.get('serialNumber');
2285
- const uniqueId = await context.get('uniqueId');
2286
- const softwareVersion = await context.get('softwareVersion', 1);
2287
- const softwareVersionString = await context.get('softwareVersionString', '1.0.0'); // Home app = Firmware Revision
2288
- const hardwareVersion = await context.get('hardwareVersion', 1);
2289
- const hardwareVersionString = await context.get('hardwareVersionString', '1.0.0');
2290
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with deviceName '${deviceName}' deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')})`);
2291
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with uniqueId ${uniqueId} serialNumber ${serialNumber}`);
2292
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with softwareVersion ${softwareVersion} softwareVersionString ${softwareVersionString}`);
2293
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with hardwareVersion ${hardwareVersion} hardwareVersionString ${hardwareVersionString}`);
2294
- this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with nodeLabel '${productName}' port ${CYAN}${this.port}${db} discriminator ${CYAN}${this.discriminator}${db} passcode ${CYAN}${this.passcode}${db} `);
2295
- // Validate ipv4address
2296
- if (this.ipv4address) {
2297
- const networkInterfaces = os.networkInterfaces();
2298
- const availableAddresses = Object.values(networkInterfaces)
2299
- .flat()
2300
- .filter((iface) => iface !== undefined && iface.family === 'IPv4' && !iface.internal)
2301
- .map((iface) => iface.address);
2302
- if (!availableAddresses.includes(this.ipv4address)) {
2303
- this.log.error(`Invalid ipv4address: ${this.ipv4address}. Available addresses are: ${availableAddresses.join(', ')}. Using all available addresses.`);
2304
- this.mdnsInterface = undefined;
2305
- }
2306
- else {
2307
- this.log.info(`Using ipv4address '${this.ipv4address}' for the Matter commissioning server.`);
2308
- }
2309
- }
2310
- // Validate ipv6address
2311
- if (this.ipv6address) {
2312
- const networkInterfaces = os.networkInterfaces();
2313
- const availableAddresses = Object.values(networkInterfaces)
2314
- .flat()
2315
- .filter((iface) => iface !== undefined && iface.family === 'IPv6' && !iface.internal)
2316
- .map((iface) => iface.address);
2317
- if (!availableAddresses.includes(this.ipv6address)) {
2318
- this.log.error(`Invalid ipv6address: ${this.ipv6address}. Available addresses are: ${availableAddresses.join(', ')}. Using all available addresses.`);
2319
- this.mdnsInterface = undefined;
2320
- }
2321
- else {
2322
- this.log.info(`Using ipv6address '${this.ipv6address}' for the Matter commissioning server.`);
2323
- }
2324
- }
2325
- const commissioningServer = new CommissioningServer({
2326
- port: this.port++,
2327
- listeningAddressIpv4: this.ipv4address,
2328
- listeningAddressIpv6: this.ipv6address,
2329
- passcode: this.passcode,
2330
- discriminator: this.discriminator,
2331
- deviceName,
2332
- deviceType,
2333
- basicInformation: {
2334
- vendorId: VendorId(vendorId),
2335
- vendorName,
2336
- productId,
2337
- productName,
2338
- nodeLabel: productName,
2339
- productLabel: productName,
2340
- softwareVersion,
2341
- softwareVersionString, // Home app = Firmware Revision
2342
- hardwareVersion,
2343
- hardwareVersionString,
2344
- uniqueId,
2345
- serialNumber,
2346
- reachable: true,
2347
- },
2348
- activeSessionsChangedCallback: (fabricIndex) => {
2349
- const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
2350
- let connected = false;
2351
- sessionInformations.forEach((session) => {
2352
- this.log.info(`Active session changed on fabric ${zb}${fabricIndex}${nf} id ${zb}${session.fabric?.fabricId}${nf} vendor ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
2353
- if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
2354
- this.log.notice(`Controller ${zb}${session.fabric?.rootVendorId}${nt} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} connected to ${plg}${pluginName}${nt} on session ${session.name}`);
2355
- connected = true;
2356
- }
2357
- });
2358
- if (connected) {
2359
- if (this.bridgeMode === 'bridge') {
2360
- this.matterbridgePaired = true;
2361
- this.matterbridgeConnected = true;
2362
- this.matterbridgeSessionInformations = this.sanitizeSessionInformation(sessionInformations);
2363
- }
2364
- if (this.bridgeMode === 'childbridge') {
2365
- const plugin = this.plugins.get(pluginName);
2366
- if (plugin) {
2367
- plugin.paired = true;
2368
- plugin.connected = true;
2369
- plugin.sessionInformations = this.sanitizeSessionInformation(sessionInformations);
2370
- }
2371
- }
2372
- }
2373
- else {
2374
- if (this.bridgeMode === 'bridge') {
2375
- this.matterbridgeSessionInformations = [];
2376
- }
2377
- if (this.bridgeMode === 'childbridge') {
2378
- const plugin = this.plugins.get(pluginName);
2379
- if (plugin) {
2380
- plugin.sessionInformations = [];
2381
- }
2382
- }
2383
- }
2384
- this.wssSendRefreshRequired();
2385
- },
2386
- commissioningChangedCallback: async (fabricIndex) => {
2387
- const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
2388
- this.log.debug(`Commissioning changed on fabric ${zb}${fabricIndex}${db} for ${plg}${pluginName}${db}`, debugStringify(fabricInfo));
2389
- if (commissioningServer.getCommissionedFabricInformation().length === 0) {
2390
- this.log.warn(`Commissioning removed from fabric ${zb}${fabricIndex}${wr} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
2391
- await commissioningServer.factoryReset();
2392
- if (pluginName === 'Matterbridge') {
2393
- await this.matterbridgeContext?.clearAll();
2394
- this.matterbridgeFabricInformations = [];
2395
- this.matterbridgeSessionInformations = [];
2396
- this.matterbridgePaired = false;
2397
- this.matterbridgeConnected = false;
2398
- }
2399
- else {
2400
- for (const plugin of this.plugins) {
2401
- if (plugin.name === pluginName) {
2402
- await plugin.platform?.onShutdown('Commissioning removed by the controller');
2403
- plugin.fabricInformations = [];
2404
- plugin.sessionInformations = [];
2405
- plugin.paired = false;
2406
- plugin.connected = false;
2407
- await plugin.storageContext?.clearAll();
2408
- }
2409
- }
2410
- }
2411
- this.log.warn(`Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
2412
- }
2413
- else {
2414
- const fabricInfo = commissioningServer.getCommissionedFabricInformation();
2415
- if (pluginName === 'Matterbridge') {
2416
- this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
2417
- this.matterbridgePaired = true;
2418
- }
2419
- else {
2420
- const plugin = this.plugins.get(pluginName);
2421
- if (plugin) {
2422
- plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
2423
- plugin.paired = true;
2424
- }
2425
- }
2426
- }
2427
- this.wssSendRefreshRequired();
2428
- },
2429
- });
2430
- if (this.passcode !== undefined)
2431
- this.passcode++;
2432
- if (this.discriminator !== undefined)
2433
- this.discriminator++;
2434
- commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
2435
- return commissioningServer;
2436
- }
2437
- /**
2438
- * Creates a commissioning server storage context.
2439
- *
2440
- * @param pluginName - The name of the plugin.
2441
- * @param deviceName - The name of the device.
2442
- * @param deviceType - The type of the device.
2443
- * @param vendorId - The vendor ID.
2444
- * @param vendorName - The vendor name.
2445
- * @param productId - The product ID.
2446
- * @param productName - The product name.
2447
- * @param serialNumber - The serial number of the device (optional).
2448
- * @param uniqueId - The unique ID of the device (optional).
2449
- * @param softwareVersion - The software version of the device (optional).
2450
- * @param softwareVersionString - The software version string of the device (optional).
2451
- * @param hardwareVersion - The hardware version of the device (optional).
2452
- * @param hardwareVersionString - The hardware version string of the device (optional).
2453
- * @returns The storage context for the commissioning server.
2454
- */
2455
- async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName) {
2456
- if (!this.storageManager)
2457
- throw new Error('No storage manager initialized');
2458
- this.log.debug(`Creating commissioning server storage context for ${plg}${pluginName}${db}`);
2459
- const random = randomBytes(8).toString('hex');
2460
- const storageContext = this.storageManager.createContext(pluginName);
2461
- await storageContext.set('deviceName', deviceName);
2462
- await storageContext.set('deviceType', deviceType);
2463
- await storageContext.set('vendorId', vendorId);
2464
- await storageContext.set('vendorName', vendorName.slice(0, 32));
2465
- await storageContext.set('productId', productId);
2466
- await storageContext.set('productName', productName.slice(0, 32));
2467
- await storageContext.set('nodeLabel', productName.slice(0, 32));
2468
- await storageContext.set('productLabel', productName.slice(0, 32));
2469
- await storageContext.set('serialNumber', await storageContext.get('serialNumber', random));
2470
- await storageContext.set('uniqueId', await storageContext.get('uniqueId', random));
2471
- await storageContext.set('softwareVersion', this.matterbridgeVersion && this.matterbridgeVersion.includes('.') ? parseInt(this.matterbridgeVersion.split('.')[0], 10) : 1);
2472
- await storageContext.set('softwareVersionString', this.matterbridgeVersion ?? '1.0.0');
2473
- await storageContext.set('hardwareVersion', this.systemInformation.osRelease && this.systemInformation.osRelease.includes('.') ? parseInt(this.systemInformation.osRelease.split('.')[0], 10) : 1);
2474
- await storageContext.set('hardwareVersionString', this.systemInformation.osRelease ?? '1.0.0');
2475
- this.log.debug(`Created commissioning server storage context for ${plg}${pluginName}${db}`);
2476
- this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
2477
- this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
2478
- this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
2479
- this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
2480
- return storageContext;
2481
- }
2482
- /**
2483
- * Imports the commissioning server context for a specific plugin and device.
2484
- * @param pluginName - The name of the plugin.
2485
- * @param device - The MatterbridgeDevice object representing the device.
2486
- * @returns The commissioning server context.
2487
- * @throws Error if the BasicInformationCluster is not found.
2488
- */
2489
- async importCommissioningServerContext(pluginName, device) {
2490
- this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
2491
- const basic = device.getClusterServer(BasicInformationCluster);
2492
- if (!basic) {
2493
- this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
2494
- process.exit(1);
2495
- }
2496
- if (!this.storageManager) {
2497
- this.log.error('importCommissioningServerContext error: no storage manager initialized');
2498
- process.exit(1);
2499
- }
2500
- this.log.debug(`Importing commissioning server storage context for ${plg}${pluginName}${db}`);
2501
- const storageContext = this.storageManager.createContext(pluginName);
2502
- await storageContext.set('deviceName', basic.getNodeLabelAttribute());
2503
- await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
2504
- await storageContext.set('vendorId', basic.getVendorIdAttribute());
2505
- await storageContext.set('vendorName', basic.getVendorNameAttribute());
2506
- await storageContext.set('productId', basic.getProductIdAttribute());
2507
- await storageContext.set('productName', basic.getProductNameAttribute());
2508
- await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
2509
- await storageContext.set('productLabel', basic.getNodeLabelAttribute());
2510
- await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
2511
- await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
2512
- await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
2513
- await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
2514
- await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
2515
- await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
2516
- this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
2517
- this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
2518
- this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
2519
- this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
2520
- this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
2521
- return storageContext;
2522
- }
2523
- /**
2524
- * Shows the commissioning server QR code for a given plugin.
2525
- * @param {CommissioningServer} commissioningServer - The commissioning server instance.
2526
- * @param {StorageContext} storageContext - The storage context instance.
2527
- * @param {NodeStorage} nodeContext - The node storage instance.
2528
- * @param {string} pluginName - The name of the plugin of Matterbridge in bridge mode.
2529
- * @returns {Promise<void>} - A promise that resolves when the QR code is shown.
2530
- */
2531
- async showCommissioningQRCode(commissioningServer, storageContext, nodeContext, pluginName) {
2532
- if (!commissioningServer || !storageContext || !nodeContext || !pluginName) {
2533
- this.log.error(`showCommissioningQRCode error: commissioningServer: ${!commissioningServer} storageContext: ${!storageContext} nodeContext: ${!nodeContext} pluginName: ${pluginName}`);
2534
- await this.cleanup('No storage initialized in showCommissioningQRCode');
2535
- return;
2536
- }
2537
- if (!commissioningServer.isCommissioned()) {
2538
- const { qrPairingCode, manualPairingCode } = commissioningServer.getPairingCode();
2539
- const QrCode = new QrCodeSchema();
2540
- this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code:\n\n`);
2541
- // eslint-disable-next-line no-console
2542
- if (this.log.logLevel === "debug" /* LogLevel.DEBUG */ || this.log.logLevel === "info" /* LogLevel.INFO */)
2543
- console.log(`${QrCode.encode(qrPairingCode)}\n`);
2544
- this.log.info(`${plg}${pluginName}${nf} \n\nqrPairingCode: ${qrPairingCode} \n\nManual pairing code: ${manualPairingCode}\n`);
2545
- if (pluginName === 'Matterbridge') {
2546
- this.matterbridgeQrPairingCode = qrPairingCode;
2547
- this.matterbridgeManualPairingCode = manualPairingCode;
2548
- this.matterbridgeFabricInformations = [];
2549
- this.matterbridgeSessionInformations = [];
2550
- this.matterbridgePaired = false;
2551
- this.matterbridgeConnected = false;
2552
- }
2553
- if (pluginName !== 'Matterbridge') {
2554
- const plugin = this.plugins.get(pluginName);
2555
- if (plugin) {
2556
- plugin.qrPairingCode = qrPairingCode;
2557
- plugin.manualPairingCode = manualPairingCode;
2558
- plugin.fabricInformations = [];
2559
- plugin.sessionInformations = [];
2560
- plugin.paired = false;
2561
- plugin.connected = false;
1557
+ this.log.info(`Removed bridged endpoint(${plugin.registeredDevices}/${plugin.addedDevices}) ${dev}${device.deviceName}${nf} (${zb}${device.name}${nf}) for plugin ${plg}${pluginName}${nf}`);
1558
+ if (plugin.registeredDevices !== undefined)
1559
+ plugin.registeredDevices--;
1560
+ if (plugin.addedDevices !== undefined)
1561
+ plugin.addedDevices--;
1562
+ if (plugin.registeredDevices === 0 && plugin.addedDevices === 0) {
1563
+ if (plugin.serverNode) {
1564
+ await this.stopServerNode(plugin.serverNode);
1565
+ plugin.locked = false;
1566
+ plugin.aggregatorNode = undefined;
1567
+ plugin.serverNode = undefined;
1568
+ this.log.info(`Stopped server node for plugin ${plg}${pluginName}${nf}`);
2562
1569
  }
2563
1570
  }
2564
1571
  }
2565
- else {
2566
- this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned. Waiting for controllers to connect ...`);
2567
- const fabricInfo = commissioningServer.getCommissionedFabricInformation();
2568
- if (fabricInfo.length > 0)
2569
- this.log.info('Commissioned fabric information:');
2570
- fabricInfo?.forEach((info) => {
2571
- this.log.info(`- fabric index ${zb}${info.fabricIndex}${nf} id ${zb}${info.fabricId}${nf} vendor ${zb}${info.rootVendorId}${nf} ${this.getVendorIdName(info.rootVendorId)} ${info.label}`);
2572
- });
2573
- if (pluginName === 'Matterbridge') {
2574
- this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
2575
- this.matterbridgeSessionInformations = [];
2576
- this.matterbridgePaired = true;
2577
- }
2578
- if (pluginName !== 'Matterbridge') {
2579
- const plugin = this.plugins.get(pluginName);
2580
- if (plugin) {
2581
- plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
2582
- plugin.sessionInformations = [];
2583
- plugin.paired = true;
2584
- }
2585
- }
1572
+ this.devices.remove(device);
1573
+ }
1574
+ async removeAllBridgedEndpoints(pluginName) {
1575
+ this.log.debug(`Removing all bridged endpoints for plugin ${plg}${pluginName}${db}`);
1576
+ for (const device of this.devices.array().filter((device) => device.plugin === pluginName)) {
1577
+ await this.removeBridgedEndpoint(pluginName, device);
2586
1578
  }
2587
- this.wssSendRefreshRequired();
2588
1579
  }
2589
- /**
2590
- * Sanitizes the fabric information by converting bigint properties to string cause res.json doesn't know bigint.
2591
- *
2592
- * @param fabricInfo - The array of exposed fabric information objects.
2593
- * @returns An array of sanitized exposed fabric information objects.
2594
- */
2595
1580
  sanitizeFabricInformations(fabricInfo) {
2596
1581
  return fabricInfo.map((info) => {
2597
1582
  return {
@@ -2605,12 +1590,6 @@ export class Matterbridge extends EventEmitter {
2605
1590
  };
2606
1591
  });
2607
1592
  }
2608
- /**
2609
- * Sanitizes the session information by converting bigint properties to string.
2610
- *
2611
- * @param sessionInfo - The array of session information objects.
2612
- * @returns An array of sanitized session information objects.
2613
- */
2614
1593
  sanitizeSessionInformation(sessionInfo) {
2615
1594
  return sessionInfo
2616
1595
  .filter((session) => session.isPeerActive)
@@ -2638,48 +1617,11 @@ export class Matterbridge extends EventEmitter {
2638
1617
  };
2639
1618
  });
2640
1619
  }
2641
- /**
2642
- * Sets the reachability of a commissioning server and trigger.
2643
- *
2644
- * @param {CommissioningServer} commissioningServer - The commissioning server to set the reachability for.
2645
- * @param {boolean} reachable - The new reachability status.
2646
- */
2647
- setCommissioningServerReachability(commissioningServer, reachable) {
2648
- const basicInformationCluster = commissioningServer?.getRootClusterServer(BasicInformationCluster);
2649
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2650
- basicInformationCluster.setReachableAttribute(reachable);
2651
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2652
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
1620
+ setServerNodeReachability(serverNode, reachable) {
2653
1621
  }
2654
- /**
2655
- * Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
2656
- * @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
2657
- * @param {boolean} reachable - A boolean indicating the reachability status to set.
2658
- */
2659
- setAggregatorReachability(matterAggregator, reachable) {
2660
- const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
2661
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2662
- basicInformationCluster.setReachableAttribute(reachable);
2663
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2664
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2665
- matterAggregator.getBridgedDevices().forEach((device) => {
2666
- this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
2667
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
2668
- device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
2669
- });
1622
+ setAggregatorReachability(aggregatorNode, reachable) {
2670
1623
  }
2671
- /**
2672
- * Sets the reachability of a device and trigger.
2673
- *
2674
- * @param {MatterbridgeDevice} device - The device to set the reachability for.
2675
- * @param {boolean} reachable - The new reachability status of the device.
2676
- */
2677
1624
  setDeviceReachability(device, reachable) {
2678
- const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
2679
- if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
2680
- basicInformationCluster.setReachableAttribute(reachable);
2681
- if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
2682
- basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
2683
1625
  }
2684
1626
  getVendorIdName = (vendorId) => {
2685
1627
  if (!vendorId)
@@ -2722,71 +1664,13 @@ export class Matterbridge extends EventEmitter {
2722
1664
  }
2723
1665
  return vendorName;
2724
1666
  };
2725
- /**
2726
- * Retrieves the base registered plugins sanitized for res.json().
2727
- * @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
2728
- */
2729
- async getBaseRegisteredPlugins() {
2730
- const baseRegisteredPlugins = [];
2731
- for (const plugin of this.plugins) {
2732
- baseRegisteredPlugins.push({
2733
- path: plugin.path,
2734
- type: plugin.type,
2735
- name: plugin.name,
2736
- version: plugin.version,
2737
- description: plugin.description,
2738
- author: plugin.author,
2739
- latestVersion: plugin.latestVersion,
2740
- locked: plugin.locked,
2741
- error: plugin.error,
2742
- enabled: plugin.enabled,
2743
- loaded: plugin.loaded,
2744
- started: plugin.started,
2745
- configured: plugin.configured,
2746
- paired: plugin.paired,
2747
- connected: plugin.connected,
2748
- fabricInformations: plugin.fabricInformations,
2749
- sessionInformations: plugin.sessionInformations,
2750
- registeredDevices: plugin.registeredDevices,
2751
- addedDevices: plugin.addedDevices,
2752
- qrPairingCode: plugin.qrPairingCode,
2753
- manualPairingCode: plugin.manualPairingCode,
2754
- configJson: plugin.configJson,
2755
- schemaJson: plugin.schemaJson,
2756
- });
2757
- }
2758
- return baseRegisteredPlugins;
2759
- }
2760
- /**
2761
- * Spawns a child process with the given command and arguments.
2762
- * @param {string} command - The command to execute.
2763
- * @param {string[]} args - The arguments to pass to the command (default: []).
2764
- * @returns {Promise<boolean>} A promise that resolves when the child process exits successfully, or rejects if there is an error.
2765
- */
2766
1667
  async spawnCommand(command, args = []) {
2767
- /*
2768
- npm > npm.cmd on windows
2769
- cmd.exe ['dir'] on windows
2770
- await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
2771
- process.on('unhandledRejection', (reason, promise) => {
2772
- this.log.error('Unhandled Rejection at:', promise, 'reason:', reason);
2773
- });
2774
-
2775
- spawn - [14:27:21.125] [Matterbridge:spawn]: changed 38 packages in 4s
2776
- spawn - [14:27:21.125] [Matterbridge:spawn]: 10 packages are looking for funding run `npm fund` for details
2777
- debug - [14:27:21.131] [Matterbridge]: Child process exited with code 0 and signal null
2778
- debug - [14:27:21.131] [Matterbridge]: Child process stdio streams have closed with code 0
2779
- */
2780
1668
  const cmdLine = command + ' ' + args.join(' ');
2781
1669
  if (process.platform === 'win32' && command === 'npm') {
2782
- // Must be spawn('cmd.exe', ['/c', 'npm -g install <package>']);
2783
1670
  const argstring = 'npm ' + args.join(' ');
2784
1671
  args.splice(0, args.length, '/c', argstring);
2785
1672
  command = 'cmd.exe';
2786
1673
  }
2787
- // Decide when using sudo on linux
2788
- // When you need sudo: Spawn stderr: npm error Error: EACCES: permission denied
2789
- // When you don't need sudo: Failed to start child process "npm install -g matterbridge-eve-door": spawn sudo ENOENT
2790
1674
  if (hasParameter('sudo') || (process.platform === 'linux' && command === 'npm' && !hasParameter('docker') && !hasParameter('nosudo'))) {
2791
1675
  args.unshift(command);
2792
1676
  command = 'sudo';
@@ -2801,7 +1685,7 @@ export class Matterbridge extends EventEmitter {
2801
1685
  reject(err);
2802
1686
  });
2803
1687
  childProcess.on('close', (code, signal) => {
2804
- this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', `child process closed with code ${code} and signal ${signal}`);
1688
+ this.frontend.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', `child process closed with code ${code} and signal ${signal}`);
2805
1689
  if (code === 0) {
2806
1690
  if (cmdLine.startsWith('npm install -g'))
2807
1691
  this.log.notice(`Package ${cmdLine.replace('npm install -g ', '').replace('--verbose', '').replace('--omit=dev', '')} installed correctly`);
@@ -2814,7 +1698,7 @@ export class Matterbridge extends EventEmitter {
2814
1698
  }
2815
1699
  });
2816
1700
  childProcess.on('exit', (code, signal) => {
2817
- this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', `child process exited with code ${code} and signal ${signal}`);
1701
+ this.frontend.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', `child process exited with code ${code} and signal ${signal}`);
2818
1702
  if (code === 0) {
2819
1703
  this.log.debug(`Child process "${cmdLine}" exited with code ${code} and signal ${signal}`);
2820
1704
  resolve(true);
@@ -2832,1060 +1716,16 @@ export class Matterbridge extends EventEmitter {
2832
1716
  childProcess.stdout.on('data', (data) => {
2833
1717
  const message = data.toString().trim();
2834
1718
  this.log.debug(`Spawn output (stdout): ${message}`);
2835
- this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
1719
+ this.frontend.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
2836
1720
  });
2837
1721
  }
2838
1722
  if (childProcess.stderr) {
2839
1723
  childProcess.stderr.on('data', (data) => {
2840
1724
  const message = data.toString().trim();
2841
1725
  this.log.debug(`Spawn verbose (stderr): ${message}`);
2842
- this.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
2843
- });
2844
- }
2845
- });
2846
- }
2847
- /**
2848
- * Sends a WebSocket message to all connected clients.
2849
- *
2850
- * @param {string} level - The logger level of the message: debug info notice warn error fatal...
2851
- * @param {string} time - The time string of the message
2852
- * @param {string} name - The logger name of the message
2853
- * @param {string} message - The content of the message.
2854
- */
2855
- wssSendMessage(level, time, name, message) {
2856
- if (!level || !time || !name || !message)
2857
- return;
2858
- // Remove ANSI escape codes from the message
2859
- // eslint-disable-next-line no-control-regex
2860
- message = message.replace(/\x1B\[[0-9;]*[m|s|u|K]/g, '');
2861
- // Remove leading asterisks from the message
2862
- message = message.replace(/^\*+/, '');
2863
- // Replace all occurrences of \t and \n
2864
- message = message.replace(/[\t\n]/g, '');
2865
- // Remove non-printable characters
2866
- // eslint-disable-next-line no-control-regex
2867
- message = message.replace(/[\x00-\x1F\x7F]/g, '');
2868
- // Replace all occurrences of \" with "
2869
- message = message.replace(/\\"/g, '"');
2870
- // Define the maximum allowed length for continuous characters without a space
2871
- const maxContinuousLength = 100;
2872
- const keepStartLength = 20;
2873
- const keepEndLength = 20;
2874
- // Split the message into words
2875
- message = message
2876
- .split(' ')
2877
- .map((word) => {
2878
- // If the word length exceeds the max continuous length, insert spaces and truncate
2879
- if (word.length > maxContinuousLength) {
2880
- return word.slice(0, keepStartLength) + ' ... ' + word.slice(-keepEndLength);
2881
- }
2882
- return word;
2883
- })
2884
- .join(' ');
2885
- // Send the message to all connected clients
2886
- this.webSocketServer?.clients.forEach((client) => {
2887
- if (client.readyState === WebSocket.OPEN) {
2888
- client.send(JSON.stringify({ id: WS_ID_LOG, src: 'Matterbridge', level, time, name, message }));
2889
- }
2890
- });
2891
- }
2892
- /**
2893
- * Sends a need to refresh WebSocket message to all connected clients.
2894
- *
2895
- */
2896
- wssSendRefreshRequired() {
2897
- this.matterbridgeInformation.refreshRequired = true;
2898
- // Send the message to all connected clients
2899
- this.webSocketServer?.clients.forEach((client) => {
2900
- if (client.readyState === WebSocket.OPEN) {
2901
- client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: {} }));
2902
- }
2903
- });
2904
- }
2905
- /**
2906
- * Sends a need to restart WebSocket message to all connected clients.
2907
- *
2908
- */
2909
- wssSendRestartRequired() {
2910
- this.matterbridgeInformation.restartRequired = true;
2911
- // Send the message to all connected clients
2912
- this.webSocketServer?.clients.forEach((client) => {
2913
- if (client.readyState === WebSocket.OPEN) {
2914
- client.send(JSON.stringify({ id: WS_ID_RESTART_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'restart_required', params: {} }));
2915
- }
2916
- });
2917
- }
2918
- /**
2919
- * Initializes the frontend of Matterbridge.
2920
- *
2921
- * @param port The port number to run the frontend server on. Default is 8283.
2922
- */
2923
- async initializeFrontend(port = 8283) {
2924
- let initializeError = false;
2925
- this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${port}${db}`);
2926
- // Create the express app that serves the frontend
2927
- this.expressApp = express();
2928
- // Log all requests to the server for debugging
2929
- /*
2930
- if (hasParameter('homedir')) {
2931
- this.expressApp.use((req, res, next) => {
2932
- this.log.debug(`Received request on expressApp: ${req.method} ${req.url}`);
2933
- next();
2934
- });
2935
- }
2936
- */
2937
- // Serve static files from '/static' endpoint
2938
- this.expressApp.use(express.static(path.join(this.rootDirectory, 'frontend/build')));
2939
- if (!hasParameter('ssl')) {
2940
- // Create an HTTP server and attach the express app
2941
- this.httpServer = createServer(this.expressApp);
2942
- // Listen on the specified port
2943
- if (hasParameter('ingress')) {
2944
- this.httpServer.listen(port, '0.0.0.0', () => {
2945
- this.log.info(`The frontend http server is listening on ${UNDERLINE}http://0.0.0.0:${port}${UNDERLINEOFF}${rs}`);
2946
- });
2947
- }
2948
- else {
2949
- this.httpServer.listen(port, () => {
2950
- if (this.systemInformation.ipv4Address !== '')
2951
- this.log.info(`The frontend http server is listening on ${UNDERLINE}http://${this.systemInformation.ipv4Address}:${port}${UNDERLINEOFF}${rs}`);
2952
- if (this.systemInformation.ipv6Address !== '')
2953
- this.log.info(`The frontend http server is listening on ${UNDERLINE}http://[${this.systemInformation.ipv6Address}]:${port}${UNDERLINEOFF}${rs}`);
2954
- });
2955
- }
2956
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2957
- this.httpServer.on('error', (error) => {
2958
- this.log.error(`Frontend http server error listening on ${port}`);
2959
- switch (error.code) {
2960
- case 'EACCES':
2961
- this.log.error(`Port ${port} requires elevated privileges`);
2962
- break;
2963
- case 'EADDRINUSE':
2964
- this.log.error(`Port ${port} is already in use`);
2965
- break;
2966
- }
2967
- initializeError = true;
2968
- return;
2969
- });
2970
- }
2971
- else {
2972
- // Load the SSL certificate, the private key and optionally the CA certificate
2973
- let cert;
2974
- try {
2975
- cert = await fs.readFile(path.join(this.matterbridgeDirectory, 'certs/cert.pem'), 'utf8');
2976
- this.log.info(`Loaded certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}`);
2977
- }
2978
- catch (error) {
2979
- this.log.error(`Error reading certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
2980
- return;
2981
- }
2982
- let key;
2983
- try {
2984
- key = await fs.readFile(path.join(this.matterbridgeDirectory, 'certs/key.pem'), 'utf8');
2985
- this.log.info(`Loaded key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}`);
2986
- }
2987
- catch (error) {
2988
- this.log.error(`Error reading key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
2989
- return;
2990
- }
2991
- let ca;
2992
- try {
2993
- ca = await fs.readFile(path.join(this.matterbridgeDirectory, 'certs/ca.pem'), 'utf8');
2994
- this.log.info(`Loaded CA certificate file ${path.join(this.matterbridgeDirectory, 'certs/ca.pem')}`);
2995
- }
2996
- catch (error) {
2997
- this.log.info(`CA certificate file ${path.join(this.matterbridgeDirectory, 'certs/ca.pem')} not loaded: ${error}`);
2998
- }
2999
- const serverOptions = { cert, key, ca };
3000
- // Create an HTTPS server with the SSL certificate and private key (ca is optional) and attach the express app
3001
- this.httpsServer = https.createServer(serverOptions, this.expressApp);
3002
- // Listen on the specified port
3003
- if (hasParameter('ingress')) {
3004
- this.httpsServer.listen(port, '0.0.0.0', () => {
3005
- this.log.info(`The frontend https server is listening on ${UNDERLINE}https://0.0.0.0:${port}${UNDERLINEOFF}${rs}`);
3006
- });
3007
- }
3008
- else {
3009
- this.httpsServer.listen(port, () => {
3010
- if (this.systemInformation.ipv4Address !== '')
3011
- this.log.info(`The frontend https server is listening on ${UNDERLINE}https://${this.systemInformation.ipv4Address}:${port}${UNDERLINEOFF}${rs}`);
3012
- if (this.systemInformation.ipv6Address !== '')
3013
- this.log.info(`The frontend https server is listening on ${UNDERLINE}https://[${this.systemInformation.ipv6Address}]:${port}${UNDERLINEOFF}${rs}`);
3014
- });
3015
- }
3016
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3017
- this.httpsServer.on('error', (error) => {
3018
- this.log.error(`Frontend https server error listening on ${port}`);
3019
- switch (error.code) {
3020
- case 'EACCES':
3021
- this.log.error(`Port ${port} requires elevated privileges`);
3022
- break;
3023
- case 'EADDRINUSE':
3024
- this.log.error(`Port ${port} is already in use`);
3025
- break;
3026
- }
3027
- initializeError = true;
3028
- return;
3029
- });
3030
- }
3031
- if (initializeError)
3032
- return;
3033
- // Createe a WebSocket server and attach it to the http or https server
3034
- const wssPort = port;
3035
- const wssHost = hasParameter('ssl') ? `wss://${this.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.systemInformation.ipv4Address}:${wssPort}`;
3036
- this.webSocketServer = new WebSocketServer(hasParameter('ssl') ? { server: this.httpsServer } : { server: this.httpServer });
3037
- this.webSocketServer.on('connection', (ws, request) => {
3038
- const clientIp = request.socket.remoteAddress;
3039
- AnsiLogger.setGlobalCallback(this.wssSendMessage.bind(this), "debug" /* LogLevel.DEBUG */);
3040
- this.log.info(`WebSocketServer client "${clientIp}" connected to Matterbridge`);
3041
- ws.on('message', (message) => {
3042
- this.log.debug(`WebSocket client message: ${message}`);
3043
- this.matterbridgeMessageHandler(ws, message);
3044
- });
3045
- ws.on('ping', () => {
3046
- this.log.debug('WebSocket client ping');
3047
- ws.pong();
3048
- });
3049
- ws.on('pong', () => {
3050
- this.log.debug('WebSocket client pong');
3051
- });
3052
- ws.on('close', () => {
3053
- this.log.info('WebSocket client disconnected');
3054
- if (this.webSocketServer?.clients.size === 0) {
3055
- AnsiLogger.setGlobalCallback(undefined);
3056
- this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
3057
- }
3058
- });
3059
- ws.on('error', (error) => {
3060
- this.log.error(`WebSocket client error: ${error}`);
3061
- });
3062
- });
3063
- this.webSocketServer.on('close', () => {
3064
- this.log.debug(`WebSocketServer closed`);
3065
- });
3066
- this.webSocketServer.on('listening', () => {
3067
- this.log.info(`The WebSocketServer is listening on ${UNDERLINE}${wssHost}${UNDERLINEOFF}${rs}`);
3068
- });
3069
- this.webSocketServer.on('error', (ws, error) => {
3070
- this.log.error(`WebSocketServer error: ${error}`);
3071
- });
3072
- // Endpoint to validate login code
3073
- this.expressApp.post('/api/login', express.json(), async (req, res) => {
3074
- const { password } = req.body;
3075
- this.log.debug('The frontend sent /api/login', password);
3076
- if (!this.nodeContext) {
3077
- this.log.error('/api/login nodeContext not found');
3078
- res.json({ valid: false });
3079
- return;
3080
- }
3081
- try {
3082
- const storedPassword = await this.nodeContext.get('password', '');
3083
- if (storedPassword === '' || password === storedPassword) {
3084
- this.log.debug('/api/login password valid');
3085
- res.json({ valid: true });
3086
- }
3087
- else {
3088
- this.log.warn('/api/login error wrong password');
3089
- res.json({ valid: false });
3090
- }
3091
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3092
- }
3093
- catch (error) {
3094
- this.log.error('/api/login error getting password');
3095
- res.json({ valid: false });
3096
- }
3097
- });
3098
- // Endpoint to provide health check
3099
- this.expressApp.get('/health', (req, res) => {
3100
- this.log.debug('Express received /health');
3101
- const healthStatus = {
3102
- status: 'ok', // Indicate service is healthy
3103
- uptime: process.uptime(), // Server uptime in seconds
3104
- timestamp: new Date().toISOString(), // Current timestamp
3105
- };
3106
- res.status(200).json(healthStatus);
3107
- });
3108
- // Endpoint to provide settings
3109
- this.expressApp.get('/api/settings', express.json(), async (req, res) => {
3110
- this.log.debug('The frontend sent /api/settings');
3111
- this.matterbridgeInformation.bridgeMode = this.bridgeMode;
3112
- this.matterbridgeInformation.restartMode = this.restartMode;
3113
- this.matterbridgeInformation.loggerLevel = this.log.logLevel;
3114
- this.matterbridgeInformation.matterLoggerLevel = Logger.defaultLogLevel;
3115
- this.matterbridgeInformation.mattermdnsinterface = (await this.nodeContext?.get('mattermdnsinterface', '')) || '';
3116
- this.matterbridgeInformation.matteripv4address = (await this.nodeContext?.get('matteripv4address', '')) || '';
3117
- this.matterbridgeInformation.matteripv6address = (await this.nodeContext?.get('matteripv6address', '')) || '';
3118
- this.matterbridgeInformation.matterPort = (await this.nodeContext?.get('matterport', 5540)) ?? 5540;
3119
- this.matterbridgeInformation.matterDiscriminator = await this.nodeContext?.get('matterdiscriminator');
3120
- this.matterbridgeInformation.matterPasscode = await this.nodeContext?.get('matterpasscode');
3121
- this.matterbridgeInformation.matterbridgePaired = this.matterbridgePaired;
3122
- this.matterbridgeInformation.matterbridgeConnected = this.matterbridgeConnected;
3123
- this.matterbridgeInformation.matterbridgeQrPairingCode = this.matterbridgeQrPairingCode;
3124
- this.matterbridgeInformation.matterbridgeManualPairingCode = this.matterbridgeManualPairingCode;
3125
- this.matterbridgeInformation.matterbridgeFabricInformations = this.matterbridgeFabricInformations;
3126
- this.matterbridgeInformation.matterbridgeSessionInformations = Array.from(this.matterbridgeSessionInformations.values());
3127
- this.matterbridgeInformation.profile = this.profile;
3128
- const response = { systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
3129
- // this.log.debug('Response:', debugStringify(response));
3130
- res.json(response);
3131
- });
3132
- // Endpoint to provide plugins
3133
- this.expressApp.get('/api/plugins', async (req, res) => {
3134
- this.log.debug('The frontend sent /api/plugins');
3135
- const response = await this.getBaseRegisteredPlugins();
3136
- // this.log.debug('Response:', debugStringify(response));
3137
- res.json(response);
3138
- });
3139
- // Endpoint to provide devices
3140
- this.expressApp.get('/api/devices', (req, res) => {
3141
- this.log.debug('The frontend sent /api/devices');
3142
- const devices = [];
3143
- this.devices.forEach(async (device) => {
3144
- const pluginName = device.plugin ?? 'Unknown';
3145
- if (this.edge)
3146
- device = EndpointServer.forEndpoint(device);
3147
- let name = device.getClusterServer(BasicInformationCluster)?.attributes.nodeLabel?.getLocal();
3148
- if (!name)
3149
- name = device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.nodeLabel?.getLocal() ?? 'Unknown';
3150
- let serial = device.getClusterServer(BasicInformationCluster)?.attributes.serialNumber?.getLocal();
3151
- if (!serial)
3152
- serial = device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.serialNumber?.getLocal() ?? 'Unknown';
3153
- let productUrl = device.getClusterServer(BasicInformationCluster)?.attributes.productUrl?.getLocal();
3154
- if (!productUrl)
3155
- productUrl = device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.productUrl?.getLocal() ?? 'Unknown';
3156
- let uniqueId = device.getClusterServer(BasicInformationCluster)?.attributes.uniqueId?.getLocal();
3157
- if (!uniqueId)
3158
- uniqueId = device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.uniqueId?.getLocal() ?? 'Unknown';
3159
- const cluster = this.getClusterTextFromDevice(device);
3160
- devices.push({
3161
- pluginName,
3162
- type: device.name + ' (0x' + device.deviceType.toString(16).padStart(4, '0') + ')',
3163
- endpoint: device.number,
3164
- name,
3165
- serial,
3166
- productUrl,
3167
- configUrl: device.configUrl,
3168
- uniqueId,
3169
- cluster: cluster,
1726
+ this.frontend.wssSendMessage('spawn', this.log.now(), 'Matterbridge:spawn', message);
3170
1727
  });
3171
- });
3172
- // this.log.debug('Response:', debugStringify(data));
3173
- res.json(devices);
3174
- });
3175
- // Endpoint to provide the cluster servers of the devices
3176
- this.expressApp.get('/api/devices_clusters/:selectedPluginName/:selectedDeviceEndpoint', (req, res) => {
3177
- const selectedPluginName = req.params.selectedPluginName;
3178
- const selectedDeviceEndpoint = parseInt(req.params.selectedDeviceEndpoint, 10);
3179
- this.log.debug(`The frontend sent /api/devices_clusters plugin:${selectedPluginName} endpoint:${selectedDeviceEndpoint}`);
3180
- if (selectedPluginName === 'none') {
3181
- res.json([]);
3182
- return;
3183
1728
  }
3184
- const data = [];
3185
- this.devices.forEach(async (device) => {
3186
- const pluginName = device.plugin;
3187
- if (this.edge)
3188
- device = EndpointServer.forEndpoint(device);
3189
- if (pluginName === selectedPluginName && device.number === selectedDeviceEndpoint) {
3190
- const clusterServers = device.getAllClusterServers();
3191
- clusterServers.forEach((clusterServer) => {
3192
- Object.entries(clusterServer.attributes).forEach(([key, value]) => {
3193
- if (clusterServer.name === 'EveHistory')
3194
- return;
3195
- // this.log.debug(`***--clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute:${key}(${value.id}) ${value.isFixed} ${value.isWritable} ${value.isWritable}`);
3196
- let attributeValue;
3197
- try {
3198
- if (typeof value.getLocal() === 'object')
3199
- attributeValue = stringify(value.getLocal());
3200
- else
3201
- attributeValue = value.getLocal().toString();
3202
- }
3203
- catch (error) {
3204
- attributeValue = 'Fabric-Scoped';
3205
- this.log.debug(`GetLocal value ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
3206
- // console.log(error);
3207
- }
3208
- data.push({
3209
- endpoint: device.number ? device.number.toString() : '...',
3210
- clusterName: clusterServer.name,
3211
- clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
3212
- attributeName: key,
3213
- attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
3214
- attributeValue,
3215
- });
3216
- });
3217
- });
3218
- device.getChildEndpoints().forEach((childEndpoint) => {
3219
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
3220
- const name = this.edge ? childEndpoint.endpoint?.id : childEndpoint.uniqueStorageKey;
3221
- const clusterServers = childEndpoint.getAllClusterServers();
3222
- clusterServers.forEach((clusterServer) => {
3223
- Object.entries(clusterServer.attributes).forEach(([key, value]) => {
3224
- if (clusterServer.name === 'EveHistory')
3225
- return;
3226
- // this.log.debug(`***--clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute:${key}(${value.id}) ${value.isFixed} ${value.isWritable} ${value.isWritable}`);
3227
- let attributeValue;
3228
- try {
3229
- if (typeof value.getLocal() === 'object')
3230
- attributeValue = stringify(value.getLocal());
3231
- else
3232
- attributeValue = value.getLocal().toString();
3233
- }
3234
- catch (error) {
3235
- attributeValue = 'Unavailable';
3236
- this.log.debug(`GetLocal error ${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
3237
- // console.log(error);
3238
- }
3239
- data.push({
3240
- endpoint: (childEndpoint.number ? childEndpoint.number.toString() : '...') + (name ? ' (' + name + ')' : ''),
3241
- clusterName: clusterServer.name,
3242
- clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
3243
- attributeName: key,
3244
- attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
3245
- attributeValue,
3246
- });
3247
- });
3248
- });
3249
- });
3250
- }
3251
- });
3252
- res.json(data);
3253
1729
  });
3254
- // Endpoint to view the log
3255
- this.expressApp.get('/api/view-log', async (req, res) => {
3256
- this.log.debug('The frontend sent /api/log');
3257
- try {
3258
- const data = await fs.readFile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), 'utf8');
3259
- res.type('text/plain');
3260
- res.send(data);
3261
- }
3262
- catch (error) {
3263
- this.log.error(`Error reading log file ${this.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
3264
- res.status(500).send('Error reading log file');
3265
- }
3266
- });
3267
- // Endpoint to download the matterbridge log
3268
- this.expressApp.get('/api/download-mblog', async (req, res) => {
3269
- this.log.debug('The frontend sent /api/download-mblog');
3270
- try {
3271
- await fs.access(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), fs.constants.F_OK);
3272
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3273
- }
3274
- catch (error) {
3275
- fs.appendFile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), 'Enable the log on file in the settings to enable the file logger');
3276
- }
3277
- res.download(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), 'matterbridge.log', (error) => {
3278
- if (error) {
3279
- this.log.error(`Error downloading log file ${this.matterbrideLoggerFile}: ${error instanceof Error ? error.message : error}`);
3280
- res.status(500).send('Error downloading the matterbridge log file');
3281
- }
3282
- });
3283
- });
3284
- // Endpoint to download the matter log
3285
- this.expressApp.get('/api/download-mjlog', async (req, res) => {
3286
- this.log.debug('The frontend sent /api/download-mjlog');
3287
- try {
3288
- await fs.access(path.join(this.matterbridgeDirectory, this.matterLoggerFile), fs.constants.F_OK);
3289
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3290
- }
3291
- catch (error) {
3292
- fs.appendFile(path.join(this.matterbridgeDirectory, this.matterLoggerFile), 'Enable the log on file in the settings to enable the file logger');
3293
- }
3294
- res.download(path.join(this.matterbridgeDirectory, this.matterLoggerFile), 'matter.log', (error) => {
3295
- if (error) {
3296
- this.log.error(`Error downloading log file ${this.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
3297
- res.status(500).send('Error downloading the matter log file');
3298
- }
3299
- });
3300
- });
3301
- // Endpoint to download the matter storage file
3302
- this.expressApp.get('/api/download-mjstorage', (req, res) => {
3303
- this.log.debug('The frontend sent /api/download-mjstorage');
3304
- res.download(path.join(this.matterbridgeDirectory, this.matterStorageName), 'matterbridge.json', (error) => {
3305
- if (error) {
3306
- this.log.error(`Error downloading log file ${this.matterStorageName}: ${error instanceof Error ? error.message : error}`);
3307
- res.status(500).send('Error downloading the matter storage file');
3308
- }
3309
- });
3310
- });
3311
- // Endpoint to download the matterbridge storage directory
3312
- this.expressApp.get('/api/download-mbstorage', async (req, res) => {
3313
- this.log.debug('The frontend sent /api/download-mbstorage');
3314
- await createZip(path.join(os.tmpdir(), `matterbridge.${this.nodeStorageName}.zip`), path.join(this.matterbridgeDirectory, this.nodeStorageName));
3315
- res.download(path.join(os.tmpdir(), `matterbridge.${this.nodeStorageName}.zip`), `matterbridge.${this.nodeStorageName}.zip`, (error) => {
3316
- if (error) {
3317
- this.log.error(`Error downloading file ${`matterbridge.${this.nodeStorageName}.zip`}: ${error instanceof Error ? error.message : error}`);
3318
- res.status(500).send('Error downloading the matterbridge storage file');
3319
- }
3320
- });
3321
- });
3322
- // Endpoint to download the matterbridge plugin directory
3323
- this.expressApp.get('/api/download-pluginstorage', async (req, res) => {
3324
- this.log.debug('The frontend sent /api/download-pluginstorage');
3325
- await createZip(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), this.matterbridgePluginDirectory);
3326
- res.download(path.join(os.tmpdir(), `matterbridge.pluginstorage.zip`), `matterbridge.pluginstorage.zip`, (error) => {
3327
- if (error) {
3328
- this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
3329
- res.status(500).send('Error downloading the matterbridge plugin storage file');
3330
- }
3331
- });
3332
- });
3333
- // Endpoint to download the matterbridge plugin config files
3334
- this.expressApp.get('/api/download-pluginconfig', async (req, res) => {
3335
- this.log.debug('The frontend sent /api/download-pluginconfig');
3336
- await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridgeDirectory, '*.config.json')));
3337
- // await createZip(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), path.relative(process.cwd(), path.join(this.matterbridgeDirectory, 'certs', '*.*')), path.relative(process.cwd(), path.join(this.matterbridgeDirectory, '*.config.json')));
3338
- res.download(path.join(os.tmpdir(), `matterbridge.pluginconfig.zip`), `matterbridge.pluginconfig.zip`, (error) => {
3339
- if (error) {
3340
- this.log.error(`Error downloading file matterbridge.pluginstorage.zip: ${error instanceof Error ? error.message : error}`);
3341
- res.status(500).send('Error downloading the matterbridge plugin storage file');
3342
- }
3343
- });
3344
- });
3345
- // Endpoint to download the matterbridge plugin config files
3346
- this.expressApp.get('/api/download-backup', async (req, res) => {
3347
- this.log.debug('The frontend sent /api/download-backup');
3348
- res.download(path.join(os.tmpdir(), `matterbridge.backup.zip`), `matterbridge.backup.zip`, (error) => {
3349
- if (error) {
3350
- this.log.error(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
3351
- res.status(500).send(`Error downloading file matterbridge.backup.zip: ${error instanceof Error ? error.message : error}`);
3352
- }
3353
- });
3354
- });
3355
- // Endpoint to receive commands
3356
- this.expressApp.post('/api/command/:command/:param', express.json(), async (req, res) => {
3357
- const command = req.params.command;
3358
- let param = req.params.param;
3359
- this.log.debug(`The frontend sent /api/command/${command}/${param}`);
3360
- if (!command) {
3361
- res.status(400).json({ error: 'No command provided' });
3362
- return;
3363
- }
3364
- this.log.debug(`Received frontend command: ${command}:${param}`);
3365
- // Handle the command setpassword from Settings
3366
- if (command === 'setpassword') {
3367
- const password = param.slice(1, -1); // Remove the first and last characters
3368
- this.log.debug('setpassword', param, password);
3369
- await this.nodeContext?.set('password', password);
3370
- res.json({ message: 'Command received' });
3371
- return;
3372
- }
3373
- // Handle the command setbridgemode from Settings
3374
- if (command === 'setbridgemode') {
3375
- this.log.debug(`setbridgemode: ${param}`);
3376
- this.wssSendRestartRequired();
3377
- await this.nodeContext?.set('bridgeMode', param);
3378
- res.json({ message: 'Command received' });
3379
- return;
3380
- }
3381
- // Handle the command backup from Settings
3382
- if (command === 'backup') {
3383
- this.log.notice(`Prepairing the backup...`);
3384
- await createZip(path.join(os.tmpdir(), `matterbridge.backup.zip`), path.join(this.matterbridgeDirectory), path.join(this.matterbridgePluginDirectory));
3385
- this.log.notice(`Backup ready to be downloaded.`);
3386
- res.json({ message: 'Command received' });
3387
- return;
3388
- }
3389
- // Handle the command setmbloglevel from Settings
3390
- if (command === 'setmbloglevel') {
3391
- this.log.debug('Matterbridge log level:', param);
3392
- if (param === 'Debug') {
3393
- this.log.logLevel = "debug" /* LogLevel.DEBUG */;
3394
- }
3395
- else if (param === 'Info') {
3396
- this.log.logLevel = "info" /* LogLevel.INFO */;
3397
- }
3398
- else if (param === 'Notice') {
3399
- this.log.logLevel = "notice" /* LogLevel.NOTICE */;
3400
- }
3401
- else if (param === 'Warn') {
3402
- this.log.logLevel = "warn" /* LogLevel.WARN */;
3403
- }
3404
- else if (param === 'Error') {
3405
- this.log.logLevel = "error" /* LogLevel.ERROR */;
3406
- }
3407
- else if (param === 'Fatal') {
3408
- this.log.logLevel = "fatal" /* LogLevel.FATAL */;
3409
- }
3410
- await this.nodeContext?.set('matterbridgeLogLevel', this.log.logLevel);
3411
- MatterbridgeDevice.logLevel = this.log.logLevel;
3412
- this.plugins.logLevel = this.log.logLevel;
3413
- for (const plugin of this.plugins) {
3414
- if (!plugin.platform || !plugin.platform.config)
3415
- continue;
3416
- plugin.platform.log.logLevel = plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel;
3417
- await plugin.platform.onChangeLoggerLevel(plugin.platform.config.debug ? "debug" /* LogLevel.DEBUG */ : this.log.logLevel);
3418
- }
3419
- res.json({ message: 'Command received' });
3420
- return;
3421
- }
3422
- // Handle the command setmbloglevel from Settings
3423
- if (command === 'setmjloglevel') {
3424
- this.log.debug('Matter.js log level:', param);
3425
- if (param === 'Debug') {
3426
- Logger.defaultLogLevel = MatterLogLevel.DEBUG;
3427
- }
3428
- else if (param === 'Info') {
3429
- Logger.defaultLogLevel = MatterLogLevel.INFO;
3430
- }
3431
- else if (param === 'Notice') {
3432
- Logger.defaultLogLevel = MatterLogLevel.NOTICE;
3433
- }
3434
- else if (param === 'Warn') {
3435
- Logger.defaultLogLevel = MatterLogLevel.WARN;
3436
- }
3437
- else if (param === 'Error') {
3438
- Logger.defaultLogLevel = MatterLogLevel.ERROR;
3439
- }
3440
- else if (param === 'Fatal') {
3441
- Logger.defaultLogLevel = MatterLogLevel.FATAL;
3442
- }
3443
- await this.nodeContext?.set('matterLogLevel', Logger.defaultLogLevel);
3444
- res.json({ message: 'Command received' });
3445
- return;
3446
- }
3447
- // Handle the command setmdnsinterface from Settings
3448
- if (command === 'setmdnsinterface') {
3449
- param = param.slice(1, -1); // Remove the first and last characters *mdns*
3450
- this.matterbridgeInformation.mattermdnsinterface = param;
3451
- this.log.debug('Matter.js mdns interface:', param === '' ? 'All interfaces' : param);
3452
- await this.nodeContext?.set('mattermdnsinterface', param);
3453
- res.json({ message: 'Command received' });
3454
- return;
3455
- }
3456
- // Handle the command setipv4address from Settings
3457
- if (command === 'setipv4address') {
3458
- param = param.slice(1, -1); // Remove the first and last characters *ip*
3459
- this.matterbridgeInformation.matteripv4address = param;
3460
- this.log.debug('Matter.js ipv4 address:', param === '' ? 'All ipv4 addresses' : param);
3461
- await this.nodeContext?.set('matteripv4address', param);
3462
- res.json({ message: 'Command received' });
3463
- return;
3464
- }
3465
- // Handle the command setipv6address from Settings
3466
- if (command === 'setipv6address') {
3467
- param = param.slice(1, -1); // Remove the first and last characters *ip*
3468
- this.matterbridgeInformation.matteripv6address = param;
3469
- this.log.debug('Matter.js ipv6 address:', param === '' ? 'All ipv6 addresses' : param);
3470
- await this.nodeContext?.set('matteripv6address', param);
3471
- res.json({ message: 'Command received' });
3472
- return;
3473
- }
3474
- // Handle the command setmatterport from Settings
3475
- if (command === 'setmatterport') {
3476
- const port = Math.min(Math.max(parseInt(param), 5540), 5560);
3477
- this.matterbridgeInformation.matterPort = port;
3478
- this.log.debug(`Set matter commissioning port to ${CYAN}${port}${db}`);
3479
- await this.nodeContext?.set('matterport', port);
3480
- res.json({ message: 'Command received' });
3481
- return;
3482
- }
3483
- // Handle the command setmatterdiscriminator from Settings
3484
- if (command === 'setmatterdiscriminator') {
3485
- const discriminator = Math.min(Math.max(parseInt(param), 1000), 4095);
3486
- this.matterbridgeInformation.matterDiscriminator = discriminator;
3487
- this.log.debug(`Set matter commissioning discriminator to ${CYAN}${discriminator}${db}`);
3488
- await this.nodeContext?.set('matterdiscriminator', discriminator);
3489
- res.json({ message: 'Command received' });
3490
- return;
3491
- }
3492
- // Handle the command setmatterpasscode from Settings
3493
- if (command === 'setmatterpasscode') {
3494
- const passcode = Math.min(Math.max(parseInt(param), 10000000), 90000000);
3495
- this.matterbridgeInformation.matterPasscode = passcode;
3496
- this.log.debug(`Set matter commissioning passcode to ${CYAN}${passcode}${db}`);
3497
- await this.nodeContext?.set('matterpasscode', passcode);
3498
- res.json({ message: 'Command received' });
3499
- return;
3500
- }
3501
- // Handle the command setmbloglevel from Settings
3502
- if (command === 'setmblogfile') {
3503
- this.log.debug('Matterbridge file log:', param);
3504
- this.matterbridgeInformation.fileLogger = param === 'true';
3505
- await this.nodeContext?.set('matterbridgeFileLog', param === 'true');
3506
- // Create the file logger for matterbridge
3507
- if (param === 'true')
3508
- AnsiLogger.setGlobalLogfile(path.join(this.matterbridgeDirectory, this.matterbrideLoggerFile), "debug" /* LogLevel.DEBUG */, true);
3509
- else
3510
- AnsiLogger.setGlobalLogfile(undefined);
3511
- res.json({ message: 'Command received' });
3512
- return;
3513
- }
3514
- // Handle the command setmbloglevel from Settings
3515
- if (command === 'setmjlogfile') {
3516
- this.log.debug('Matter file log:', param);
3517
- this.matterbridgeInformation.matterFileLogger = param === 'true';
3518
- await this.nodeContext?.set('matterFileLog', param === 'true');
3519
- if (param === 'true') {
3520
- try {
3521
- Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
3522
- defaultLogLevel: MatterLogLevel.DEBUG,
3523
- logFormat: MatterLogFormat.PLAIN,
3524
- });
3525
- }
3526
- catch (error) {
3527
- this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
3528
- }
3529
- }
3530
- else {
3531
- try {
3532
- Logger.removeLogger('matterfilelogger');
3533
- }
3534
- catch (error) {
3535
- this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridgeDirectory, this.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
3536
- }
3537
- }
3538
- res.json({ message: 'Command received' });
3539
- return;
3540
- }
3541
- // Handle the command unregister from Settings
3542
- if (command === 'unregister') {
3543
- await this.unregisterAndShutdownProcess();
3544
- res.json({ message: 'Command received' });
3545
- return;
3546
- }
3547
- // Handle the command reset from Settings
3548
- if (command === 'reset') {
3549
- await this.shutdownProcessAndReset();
3550
- res.json({ message: 'Command received' });
3551
- return;
3552
- }
3553
- // Handle the command factoryreset from Settings
3554
- if (command === 'factoryreset') {
3555
- await this.shutdownProcessAndFactoryReset();
3556
- res.json({ message: 'Command received' });
3557
- return;
3558
- }
3559
- // Handle the command shutdown from Header
3560
- if (command === 'shutdown') {
3561
- await this.shutdownProcess();
3562
- res.json({ message: 'Command received' });
3563
- return;
3564
- }
3565
- // Handle the command restart from Header
3566
- if (command === 'restart') {
3567
- await this.restartProcess();
3568
- res.json({ message: 'Command received' });
3569
- return;
3570
- }
3571
- // Handle the command update from Header
3572
- if (command === 'update') {
3573
- this.log.info('Updating matterbridge...');
3574
- try {
3575
- await this.spawnCommand('npm', ['install', '-g', 'matterbridge', '--omit=dev', '--verbose']);
3576
- this.log.info('Matterbridge has been updated. Full restart required.');
3577
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3578
- }
3579
- catch (error) {
3580
- this.log.error('Error updating matterbridge');
3581
- }
3582
- await this.updateProcess();
3583
- this.wssSendRestartRequired();
3584
- res.json({ message: 'Command received' });
3585
- return;
3586
- }
3587
- // Handle the command saveconfig from Home
3588
- if (command === 'saveconfig') {
3589
- param = param.replace(/\*/g, '\\');
3590
- this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
3591
- // console.log('Req.body:', JSON.stringify(req.body, null, 2));
3592
- if (!this.plugins.has(param)) {
3593
- this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3594
- }
3595
- else {
3596
- const plugin = this.plugins.get(param);
3597
- if (!plugin)
3598
- return;
3599
- this.plugins.saveConfigFromJson(plugin, req.body);
3600
- }
3601
- this.wssSendRestartRequired();
3602
- res.json({ message: 'Command received' });
3603
- return;
3604
- }
3605
- // Handle the command installplugin from Home
3606
- if (command === 'installplugin') {
3607
- param = param.replace(/\*/g, '\\');
3608
- this.log.info(`Installing plugin ${plg}${param}${nf}...`);
3609
- try {
3610
- await this.spawnCommand('npm', ['install', '-g', param, '--omit=dev', '--verbose']);
3611
- this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
3612
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3613
- }
3614
- catch (error) {
3615
- this.log.error(`Error installing plugin ${plg}${param}${er}`);
3616
- }
3617
- this.wssSendRestartRequired();
3618
- param = param.split('@')[0];
3619
- // Also add the plugin to matterbridge so no return!
3620
- if (param === 'matterbridge') {
3621
- // If we used the command installplugin to install a dev or a specific version of matterbridge we don't want to add it to matterbridge
3622
- res.json({ message: 'Command received' });
3623
- return;
3624
- }
3625
- }
3626
- // Handle the command addplugin from Home
3627
- if (command === 'addplugin' || command === 'installplugin') {
3628
- param = param.replace(/\*/g, '\\');
3629
- const plugin = await this.plugins.add(param);
3630
- if (plugin) {
3631
- this.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
3632
- }
3633
- res.json({ message: 'Command received' });
3634
- this.wssSendRefreshRequired();
3635
- return;
3636
- }
3637
- // Handle the command removeplugin from Home
3638
- if (command === 'removeplugin') {
3639
- if (!this.plugins.has(param)) {
3640
- this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3641
- }
3642
- else {
3643
- const plugin = this.plugins.get(param);
3644
- await this.plugins.shutdown(plugin, 'The plugin has been removed.', true);
3645
- await this.plugins.remove(param);
3646
- }
3647
- res.json({ message: 'Command received' });
3648
- this.wssSendRefreshRequired();
3649
- return;
3650
- }
3651
- // Handle the command enableplugin from Home
3652
- if (command === 'enableplugin') {
3653
- if (!this.plugins.has(param)) {
3654
- this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3655
- }
3656
- else {
3657
- const plugin = this.plugins.get(param);
3658
- if (plugin && !plugin.enabled) {
3659
- plugin.locked = undefined;
3660
- plugin.error = undefined;
3661
- plugin.loaded = undefined;
3662
- plugin.started = undefined;
3663
- plugin.configured = undefined;
3664
- plugin.connected = undefined;
3665
- plugin.platform = undefined;
3666
- plugin.registeredDevices = undefined;
3667
- plugin.addedDevices = undefined;
3668
- await this.plugins.enable(param);
3669
- this.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background
3670
- }
3671
- }
3672
- res.json({ message: 'Command received' });
3673
- this.wssSendRefreshRequired();
3674
- return;
3675
- }
3676
- // Handle the command disableplugin from Home
3677
- if (command === 'disableplugin') {
3678
- if (!this.plugins.has(param)) {
3679
- this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
3680
- }
3681
- else {
3682
- const plugin = this.plugins.get(param);
3683
- if (plugin && plugin.enabled) {
3684
- await this.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
3685
- await this.plugins.disable(param);
3686
- }
3687
- }
3688
- res.json({ message: 'Command received' });
3689
- this.wssSendRefreshRequired();
3690
- return;
3691
- }
3692
- });
3693
- // Fallback for routing (must be the last route)
3694
- this.expressApp.get('*', (req, res) => {
3695
- this.log.debug('The frontend sent:', req.url);
3696
- this.log.debug('Response send file:', path.join(this.rootDirectory, 'frontend/build/index.html'));
3697
- res.sendFile(path.join(this.rootDirectory, 'frontend/build/index.html'));
3698
- });
3699
- this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${UNDERLINEOFF}${rs}`);
3700
- }
3701
- /**
3702
- * Retrieves the cluster text description from a given device.
3703
- * @param {MatterbridgeDevice} device - The MatterbridgeDevice object.
3704
- * @returns {string} The attributes description of the cluster servers in the device.
3705
- */
3706
- getClusterTextFromDevice(device) {
3707
- const stringifyUserLabel = (endpoint) => {
3708
- const labelList = endpoint.getClusterServer(UserLabelCluster)?.attributes.labelList.getLocal();
3709
- if (!labelList)
3710
- return;
3711
- const composed = labelList.find((entry) => entry.label === 'composed');
3712
- if (composed)
3713
- return 'Composed: ' + composed.value;
3714
- else
3715
- return '';
3716
- };
3717
- const stringifyFixedLabel = (endpoint) => {
3718
- const labelList = endpoint.getClusterServer(FixedLabelCluster)?.attributes.labelList.getLocal();
3719
- if (!labelList)
3720
- return;
3721
- const composed = labelList.find((entry) => entry.label === 'composed');
3722
- if (composed)
3723
- return 'Composed: ' + composed.value;
3724
- else
3725
- return '';
3726
- };
3727
- let attributes = '';
3728
- // this.log.debug(`***getClusterTextFromDevice: ${device.deviceName} (${device.name})`);
3729
- const clusterServers = device.getAllClusterServers();
3730
- clusterServers.forEach((clusterServer) => {
3731
- try {
3732
- // this.log.debug(`**--clusterServer: ${clusterServer.id} (${clusterServer.name})`);
3733
- if (clusterServer.name === 'OnOff')
3734
- attributes += `OnOff: ${clusterServer.attributes.onOff.getLocal()} `;
3735
- if (clusterServer.name === 'Switch')
3736
- attributes += `Position: ${clusterServer.attributes.currentPosition.getLocal()} `;
3737
- if (clusterServer.name === 'WindowCovering')
3738
- attributes += `Cover position: ${clusterServer.attributes.currentPositionLiftPercent100ths.getLocal() / 100}% `;
3739
- if (clusterServer.name === 'DoorLock')
3740
- attributes += `State: ${clusterServer.attributes.lockState.getLocal() === 1 ? 'Locked' : 'Not locked'} `;
3741
- if (clusterServer.name === 'Thermostat')
3742
- attributes += `Temperature: ${clusterServer.attributes.localTemperature.getLocal() / 100}°C `;
3743
- if (clusterServer.name === 'LevelControl')
3744
- attributes += `Level: ${clusterServer.attributes.currentLevel.getLocal()}% `;
3745
- if (clusterServer.name === 'ColorControl' && clusterServer.attributes.currentX)
3746
- attributes += `X: ${Math.round(clusterServer.attributes.currentX.getLocal())} Y: ${Math.round(clusterServer.attributes.currentY.getLocal())} `;
3747
- if (clusterServer.name === 'ColorControl' && clusterServer.attributes.currentHue)
3748
- attributes += `Hue: ${Math.round(clusterServer.attributes.currentHue.getLocal())} Saturation: ${Math.round(clusterServer.attributes.currentSaturation.getLocal())}% `;
3749
- if (clusterServer.name === 'ColorControl' && clusterServer.attributes.colorTemperatureMireds)
3750
- attributes += `ColorTemp: ${Math.round(clusterServer.attributes.colorTemperatureMireds.getLocal())} `;
3751
- if (clusterServer.name === 'BooleanState')
3752
- attributes += `Contact: ${clusterServer.attributes.stateValue.getLocal()} `;
3753
- if (clusterServer.name === 'BooleanStateConfiguration' && clusterServer.attributes.alarmsActive)
3754
- attributes += `Active alarms: ${stringify(clusterServer.attributes.alarmsActive.getLocal())} `;
3755
- if (clusterServer.name === 'SmokeCoAlarm' && clusterServer.attributes.smokeState)
3756
- attributes += `Smoke: ${clusterServer.attributes.smokeState.getLocal()} `;
3757
- if (clusterServer.name === 'SmokeCoAlarm' && clusterServer.attributes.coState)
3758
- attributes += `Co: ${clusterServer.attributes.coState.getLocal()} `;
3759
- if (clusterServer.name === 'FanControl')
3760
- attributes += `Mode: ${clusterServer.attributes.fanMode.getLocal()} Speed: ${clusterServer.attributes.percentCurrent.getLocal()} `;
3761
- if (clusterServer.name === 'FanControl' && clusterServer.attributes.speedCurrent)
3762
- attributes += `MultiSpeed: ${clusterServer.attributes.speedCurrent.getLocal()} `;
3763
- if (clusterServer.name === 'OccupancySensing')
3764
- attributes += `Occupancy: ${clusterServer.attributes.occupancy.getLocal().occupied} `;
3765
- if (clusterServer.name === 'IlluminanceMeasurement')
3766
- attributes += `Illuminance: ${clusterServer.attributes.measuredValue.getLocal()} `;
3767
- if (clusterServer.name === 'AirQuality')
3768
- attributes += `Air quality: ${clusterServer.attributes.airQuality.getLocal()} `;
3769
- if (clusterServer.name === 'TvocMeasurement')
3770
- attributes += `Voc: ${clusterServer.attributes.measuredValue.getLocal()} `;
3771
- if (clusterServer.name === 'TemperatureMeasurement')
3772
- attributes += `Temperature: ${clusterServer.attributes.measuredValue.getLocal() / 100}°C `;
3773
- if (clusterServer.name === 'RelativeHumidityMeasurement')
3774
- attributes += `Humidity: ${clusterServer.attributes.measuredValue.getLocal() / 100}% `;
3775
- if (clusterServer.name === 'PressureMeasurement')
3776
- attributes += `Pressure: ${clusterServer.attributes.measuredValue.getLocal()} `;
3777
- if (clusterServer.name === 'FlowMeasurement')
3778
- attributes += `Flow: ${clusterServer.attributes.measuredValue.getLocal()} `;
3779
- if (clusterServer.name === 'FixedLabel')
3780
- attributes += `${stringifyFixedLabel(device)} `;
3781
- if (clusterServer.name === 'UserLabel')
3782
- attributes += `${stringifyUserLabel(device)} `;
3783
- // this.log.debug(`*--clusterServer: ${clusterServer.id} (${clusterServer.name})`);
3784
- }
3785
- catch (error) {
3786
- this.log.error(`getClusterTextFromDevice with ${clusterServer.name} error: ${error}`);
3787
- }
3788
- });
3789
- // this.log.debug(`*getClusterTextFromDevice: ${device.deviceName} (${device.name})`);
3790
- return attributes;
3791
- }
3792
- /**
3793
- * Initializes the Matterbridge instance as extension for zigbee2mqtt.
3794
- * @deprecated This method is deprecated and will be removed in a future version.
3795
- *
3796
- * @returns A Promise that resolves when the initialization is complete.
3797
- */
3798
- async startExtension(dataPath, extensionVersion, port = 5540) {
3799
- // Set the bridge mode
3800
- this.bridgeMode = 'bridge';
3801
- // Set the first port to use
3802
- this.port = port;
3803
- // Set Matterbridge logger
3804
- this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "info" /* LogLevel.INFO */ });
3805
- this.log.debug('Matterbridge extension is starting...');
3806
- // Initialize NodeStorage
3807
- this.matterbridgeDirectory = dataPath;
3808
- this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
3809
- this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
3810
- this.log.debug('Creating node storage context for matterbridge: matterbridge');
3811
- this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
3812
- const plugin = {
3813
- path: '',
3814
- type: 'DynamicPlatform',
3815
- name: 'MatterbridgeExtension',
3816
- version: '1.0.0',
3817
- description: 'Matterbridge extension',
3818
- author: 'https://github.com/Luligu',
3819
- enabled: false,
3820
- registeredDevices: 0,
3821
- addedDevices: 0,
3822
- };
3823
- this.plugins.set(plugin);
3824
- this.plugins.saveToStorage();
3825
- // Log system info and create .matterbridge directory
3826
- await this.logNodeAndSystemInfo();
3827
- this.matterbridgeDirectory = dataPath;
3828
- // Set matter.js logger level and format
3829
- Logger.defaultLogLevel = MatterLogLevel.INFO;
3830
- Logger.format = MatterLogFormat.ANSI;
3831
- // Start the storage and create matterbridgeContext
3832
- await this.startMatterStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
3833
- if (!this.storageManager)
3834
- return false;
3835
- this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
3836
- if (!this.matterbridgeContext)
3837
- return false;
3838
- await this.matterbridgeContext.set('softwareVersion', 1);
3839
- await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
3840
- await this.matterbridgeContext.set('hardwareVersion', 1);
3841
- await this.matterbridgeContext.set('hardwareVersionString', extensionVersion); // Update with the extension version
3842
- this.matterServer = await this.createMatterServer(this.storageManager);
3843
- this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
3844
- this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
3845
- this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
3846
- this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
3847
- this.log.debug('Adding matterbridge aggregator to commissioning server');
3848
- this.commissioningServer.addDevice(this.matterAggregator);
3849
- this.log.debug('Adding matterbridge commissioning server to matter server');
3850
- await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
3851
- await this.startMatterServer();
3852
- this.log.info('Matter server started');
3853
- await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
3854
- // Set reachability to true and trigger event after 60 seconds
3855
- setTimeout(() => {
3856
- this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
3857
- if (this.commissioningServer)
3858
- this.setCommissioningServerReachability(this.commissioningServer, true);
3859
- if (this.matterAggregator)
3860
- this.setAggregatorReachability(this.matterAggregator, true);
3861
- }, 60 * 1000);
3862
- return this.commissioningServer.isCommissioned();
3863
- }
3864
- /**
3865
- * Close the Matterbridge instance as extension for zigbee2mqtt.
3866
- * @deprecated This method is deprecated and will be removed in a future version.
3867
- *
3868
- * @returns A Promise that resolves when the initialization is complete.
3869
- */
3870
- async stopExtension() {
3871
- // Closing matter
3872
- await this.stopMatterServer();
3873
- // Clearing the session manager
3874
- // this.matterbridgeContext?.createContext('SessionManager').clear();
3875
- // Closing storage
3876
- await this.stopMatterStorage();
3877
- this.log.info('Matter server stopped');
3878
- }
3879
- /**
3880
- * Checks if the extension is commissioned.
3881
- * @deprecated This method is deprecated and will be removed in a future version.
3882
- *
3883
- * @returns {boolean} Returns true if the extension is commissioned, false otherwise.
3884
- */
3885
- isExtensionCommissioned() {
3886
- if (!this.commissioningServer)
3887
- return false;
3888
- return this.commissioningServer.isCommissioned();
3889
1730
  }
3890
1731
  }
3891
- //# sourceMappingURL=matterbridge.js.map