matterbridge 1.0.6 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +202 -21
  3. package/README.md +90 -5
  4. package/Screenshot devices page.png +0 -0
  5. package/Screenshot home page.png +0 -0
  6. package/dist/AirQualityCluster.d.ts +22 -0
  7. package/dist/AirQualityCluster.d.ts.map +1 -1
  8. package/dist/AirQualityCluster.js +23 -1
  9. package/dist/AirQualityCluster.js.map +1 -1
  10. package/dist/ColorControlServer.d.ts +20 -3
  11. package/dist/ColorControlServer.d.ts.map +1 -1
  12. package/dist/ColorControlServer.js +20 -3
  13. package/dist/ColorControlServer.js.map +1 -1
  14. package/dist/TvocCluster.d.ts +262 -0
  15. package/dist/TvocCluster.d.ts.map +1 -0
  16. package/dist/TvocCluster.js +114 -0
  17. package/dist/TvocCluster.js.map +1 -0
  18. package/dist/index.d.ts +30 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +37 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/matterbridge.d.ts +187 -17
  23. package/dist/matterbridge.d.ts.map +1 -1
  24. package/dist/matterbridge.js +721 -218
  25. package/dist/matterbridge.js.map +1 -1
  26. package/dist/matterbridgeAccessoryPlatform.d.ts +50 -11
  27. package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
  28. package/dist/matterbridgeAccessoryPlatform.js +56 -41
  29. package/dist/matterbridgeAccessoryPlatform.js.map +1 -1
  30. package/dist/matterbridgeComposed.d.ts +43 -0
  31. package/dist/matterbridgeComposed.d.ts.map +1 -0
  32. package/dist/matterbridgeComposed.js +58 -0
  33. package/dist/matterbridgeComposed.js.map +1 -0
  34. package/dist/matterbridgeDevice.d.ts +209 -4
  35. package/dist/matterbridgeDevice.d.ts.map +1 -1
  36. package/dist/matterbridgeDevice.js +587 -51
  37. package/dist/matterbridgeDevice.js.map +1 -1
  38. package/dist/matterbridgeDynamicPlatform.d.ts +50 -11
  39. package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -1
  40. package/dist/matterbridgeDynamicPlatform.js +56 -41
  41. package/dist/matterbridgeDynamicPlatform.js.map +1 -1
  42. package/frontend/build/Matterbridge.jpg +0 -0
  43. package/frontend/build/asset-manifest.json +6 -6
  44. package/frontend/build/index.html +1 -1
  45. package/frontend/build/static/css/main.6d93e0db.css +2 -0
  46. package/frontend/build/static/css/main.6d93e0db.css.map +1 -0
  47. package/frontend/build/static/js/main.21c55a60.js +3 -0
  48. package/frontend/build/static/js/{main.a000062f.js.LICENSE.txt → main.21c55a60.js.LICENSE.txt} +2 -0
  49. package/frontend/build/static/js/main.21c55a60.js.map +1 -0
  50. package/package.json +8 -4
  51. package/.eslintrc.json +0 -45
  52. package/.gitattributes +0 -2
  53. package/.prettierignore +0 -2
  54. package/.prettierrc.json +0 -12
  55. package/frontend/README.md +0 -70
  56. package/frontend/build/static/css/main.8b969fd5.css +0 -2
  57. package/frontend/build/static/css/main.8b969fd5.css.map +0 -1
  58. package/frontend/build/static/js/main.a000062f.js +0 -3
  59. package/frontend/build/static/js/main.a000062f.js.map +0 -1
  60. package/frontend/package-lock.json +0 -18351
  61. package/frontend/package.json +0 -40
  62. package/frontend/public/favicon.ico +0 -0
  63. package/frontend/public/index.html +0 -15
  64. package/frontend/public/manifest.json +0 -15
  65. package/frontend/public/matter.png +0 -0
  66. package/frontend/public/robots.txt +0 -3
@@ -1,21 +1,47 @@
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.1.1
8
+ *
9
+ * Copyright 2023, 2024 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
+ */
1
23
  import { NodeStorageManager } from 'node-persist-manager';
2
- import { AnsiLogger, BLUE, BRIGHT, GREEN, RESET, REVERSE, REVERSEOFF, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, nf, rs, } from 'node-ansi-logger';
24
+ import { AnsiLogger, BRIGHT, GREEN, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr } from 'node-ansi-logger';
3
25
  import { fileURLToPath, pathToFileURL } from 'url';
4
26
  import { promises as fs } from 'fs';
5
- import EventEmitter from 'events';
6
27
  import express from 'express';
7
28
  import os from 'os';
8
29
  import path from 'path';
9
30
  import { CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
10
- import { VendorId } from '@project-chip/matter-node.js/datatype';
31
+ import { BasicInformationCluster, BridgedDeviceBasicInformationCluster, ClusterServer } from '@project-chip/matter-node.js/cluster';
32
+ import { DeviceTypeId, VendorId } from '@project-chip/matter-node.js/datatype';
11
33
  import { Aggregator, DeviceTypes } from '@project-chip/matter-node.js/device';
12
34
  import { Format, Level, Logger } from '@project-chip/matter-node.js/log';
13
35
  import { QrCodeSchema } from '@project-chip/matter-node.js/schema';
14
36
  import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
15
- import { requireMinNodeVersion, getParameter, hasParameter } from '@project-chip/matter-node.js/util';
37
+ import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
16
38
  import { CryptoNode } from '@project-chip/matter-node.js/crypto';
17
- import { BasicInformationCluster, BridgedDeviceBasicInformationCluster } from '@project-chip/matter-node.js/cluster';
18
- export class Matterbridge extends EventEmitter {
39
+ const plg = '\u001B[38;5;33m';
40
+ const dev = '\u001B[38;5;79m';
41
+ /**
42
+ * Represents the Matterbridge application.
43
+ */
44
+ export class Matterbridge {
19
45
  systemInformation = {
20
46
  ipv4Address: '',
21
47
  ipv6Address: '',
@@ -29,14 +55,16 @@ export class Matterbridge extends EventEmitter {
29
55
  freeMemory: '',
30
56
  systemUptime: '',
31
57
  };
58
+ homeDirectory;
32
59
  rootDirectory;
60
+ matterbridgeDirectory;
33
61
  bridgeMode = '';
34
62
  log;
35
63
  hasCleanupStarted = false;
36
64
  registeredPlugins = [];
37
65
  registeredDevices = [];
38
- storage = undefined;
39
- context = undefined;
66
+ nodeStorage = undefined;
67
+ nodeContext = undefined;
40
68
  app;
41
69
  storageManager;
42
70
  matterbridgeContext;
@@ -45,13 +73,58 @@ export class Matterbridge extends EventEmitter {
45
73
  matterAggregator;
46
74
  commissioningServer;
47
75
  commissioningController;
76
+ static instance;
48
77
  constructor() {
49
- super();
78
+ // we load asynchroneously the instance
79
+ }
80
+ /**
81
+ * Loads an instance of the Matterbridge class.
82
+ * If an instance already exists, an error will be thrown.
83
+ * @returns The loaded instance of the Matterbridge class.
84
+ * @throws Error if an instance of Matterbridge already exists.
85
+ */
86
+ static async loadInstance() {
87
+ if (!Matterbridge.instance) {
88
+ Matterbridge.instance = new Matterbridge();
89
+ await Matterbridge.instance.initialize();
90
+ }
91
+ else {
92
+ throw new Error('Matterbridge instance already exists');
93
+ }
94
+ return Matterbridge.instance;
95
+ }
96
+ /**
97
+ * Initializes the Matterbridge application.
98
+ *
99
+ * @remarks
100
+ * This method performs the necessary setup and initialization steps for the Matterbridge application.
101
+ * It displays the help information if the 'help' parameter is provided, sets up the logger, checks the
102
+ * node version, registers signal handlers, initializes storage, and parses the command line.
103
+ *
104
+ * @returns A Promise that resolves when the initialization is complete.
105
+ */
106
+ async initialize() {
107
+ // Display the help
108
+ if (hasParameter('help')) {
109
+ // eslint-disable-next-line no-console
110
+ console.log(`\nUsage: matterbridge [options]\n
111
+ Options:
112
+ - help: show the help
113
+ - bridge: start Matterbridge in bridge mode
114
+ - childbridge: start Matterbridge in childbridge mode
115
+ - frontend [port]: start the frontend on the given port (default 3000)
116
+ - list: list the registered plugins
117
+ - add [plugin path]: register the plugin
118
+ - remove [plugin path]: remove the plugin
119
+ - enable [plugin path]: enable the plugin
120
+ - disable [plugin path]: disable the plugin\n`);
121
+ process.exit(0);
122
+ }
50
123
  // set Matterbridge logger
51
124
  this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ });
52
125
  this.log.info('Matterbridge is running...');
53
- // log system info
54
- this.logNodeAndSystemInfo();
126
+ // log system info and create .matterbridge directory
127
+ await this.logNodeAndSystemInfo();
55
128
  // check node version and throw error
56
129
  requireMinNodeVersion(18);
57
130
  // register SIGINT SIGTERM signal handlers
@@ -59,32 +132,33 @@ export class Matterbridge extends EventEmitter {
59
132
  // set matter.js logger level and format
60
133
  Logger.defaultLogLevel = Level.DEBUG;
61
134
  Logger.format = Format.ANSI;
62
- this.initialize();
63
- }
64
- async initialize() {
65
135
  // Initialize NodeStorage
66
- this.storage = new NodeStorageManager();
67
- this.context = await this.storage.createStorage('matterbridge');
68
- this.registeredPlugins = await this.context?.get('plugins', []);
69
- // Initialize frontend
70
- this.initializeFrontend();
136
+ this.log.debug('Creating node storage manager');
137
+ this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'storage') });
138
+ this.log.debug('Creating node storage context for matterbridge');
139
+ this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
140
+ this.registeredPlugins = await this.nodeContext.get('plugins', []);
141
+ /*
142
+ this.registeredPlugins.forEach(async (plugin) => {
143
+ this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
144
+ plugin.nodeContext = await this.nodeStorage?.createStorage(plugin.name);
145
+ });
146
+ */
71
147
  // Parse command line
72
- await this.parseCommandLine();
148
+ this.parseCommandLine();
73
149
  }
150
+ /**
151
+ * Parses the command line arguments and performs the corresponding actions.
152
+ * @private
153
+ * @returns {Promise<void>} A promise that resolves when the command line arguments have been processed.
154
+ */
74
155
  async parseCommandLine() {
75
- if (hasParameter('help')) {
76
- this.log.info(`\nmatterbridge -help -bridge -add <plugin path> -remove <plugin path>
77
- - help: show the help
78
- - bridge: start the bridge
79
- - list: list the registered plugin
80
- - add <plugin path>: register the plugin
81
- - remove <plugin path>: remove the plugin\n`);
82
- process.exit(0);
83
- }
84
156
  if (hasParameter('list')) {
85
157
  this.log.info('Registered plugins:');
86
158
  this.registeredPlugins.forEach((plugin) => {
87
- this.log.info(`- ${BLUE}${plugin.name}${nf} '${BLUE}${BRIGHT}${plugin.description}${RESET}${nf}' type: ${GREEN}${plugin.type}${nf}`);
159
+ this.log.info(`- ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" version: ${plugin.version}` +
160
+ ` author: "${plugin.author}" type: ${GREEN}${plugin.type}${nf} ${YELLOW}${plugin.enabled ? 'enabled' : 'disabled'}${nf}`);
161
+ // loaded: ${plugin.loaded} started: ${plugin.started} paired: ${plugin.paired} connected: ${plugin.connected}
88
162
  });
89
163
  process.exit(0);
90
164
  }
@@ -98,11 +172,26 @@ export class Matterbridge extends EventEmitter {
98
172
  await this.loadPlugin(getParameter('remove'), 'remove');
99
173
  process.exit(0);
100
174
  }
101
- await this.startStorage('json', '.matterbridge.json');
175
+ if (getParameter('enable')) {
176
+ this.log.debug(`Enable plugin ${getParameter('enable')}`);
177
+ await this.loadPlugin(getParameter('enable'), 'enable');
178
+ process.exit(0);
179
+ }
180
+ if (getParameter('disable')) {
181
+ this.log.debug(`Disable plugin ${getParameter('disable')}`);
182
+ await this.loadPlugin(getParameter('disable'), 'disable');
183
+ process.exit(0);
184
+ }
185
+ // Start the storage (we need it now for frontend and later for matterbridge)
186
+ await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
187
+ // Initialize frontend
188
+ await this.initializeFrontend(getIntParameter('frontend'));
102
189
  if (hasParameter('childbridge')) {
103
190
  this.bridgeMode = 'childbridge';
104
191
  this.registeredPlugins.forEach(async (plugin) => {
105
- this.log.info(`Loading plugin ${BLUE}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
192
+ if (!plugin.enabled)
193
+ return;
194
+ this.log.info(`Loading registered plugin ${plg}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
106
195
  await this.loadPlugin(plugin.path, 'load');
107
196
  });
108
197
  await this.startMatterBridge();
@@ -110,67 +199,59 @@ export class Matterbridge extends EventEmitter {
110
199
  if (hasParameter('bridge')) {
111
200
  this.bridgeMode = 'bridge';
112
201
  this.registeredPlugins.forEach(async (plugin) => {
113
- this.log.info(`Loading plugin ${BLUE}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
202
+ if (!plugin.enabled)
203
+ return;
204
+ this.log.info(`Loading registered plugin ${plg}${plugin.name}${nf} type ${GREEN}${plugin.type}${nf}`);
114
205
  await this.loadPlugin(plugin.path, 'load');
115
206
  });
116
207
  await this.startMatterBridge();
117
208
  }
118
209
  }
119
- // Typed method for emitting events
120
- emit(event, ...args) {
121
- return super.emit(event, ...args);
122
- }
123
- // Typed method for listening to events
124
- on(event, listener) {
125
- super.on(event, listener);
126
- return this;
127
- }
210
+ /**
211
+ * Loads a plugin from the specified package.json file path.
212
+ * @param packageJsonPath - The path to the package.json file of the plugin.
213
+ * @param mode - The mode of operation. Possible values are 'load', 'add', 'remove', 'enable', 'disable'.
214
+ * @returns A Promise that resolves when the plugin is loaded successfully, or rejects with an error if loading fails.
215
+ */
128
216
  async loadPlugin(packageJsonPath, mode = 'load') {
129
217
  if (!packageJsonPath.endsWith('package.json'))
130
218
  packageJsonPath = path.join(packageJsonPath, 'package.json');
131
- this.log.debug(`Loading plugin ${BLUE}${packageJsonPath}${RESET}`);
219
+ packageJsonPath = path.resolve(packageJsonPath);
220
+ this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
132
221
  try {
133
222
  // Load the package.json of the plugin
134
223
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
224
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === packageJson.name);
225
+ if (plugin && plugin.platform) {
226
+ this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
227
+ }
135
228
  // Resolve the main module path relative to package.json
136
229
  const pluginPath = path.resolve(path.dirname(packageJsonPath), packageJson.main);
137
230
  // Convert the file path to a URL
138
231
  const pluginUrl = pathToFileURL(pluginPath);
139
232
  // Dynamically import the plugin
140
- this.log.debug(`Importing plugin ${BLUE}${pluginUrl.href}${RESET}`);
141
- const plugin = await import(pluginUrl.href);
233
+ this.log.debug(`Importing plugin ${plg}${plugin?.name}${db} from ${pluginUrl.href}`);
234
+ const pluginInstance = await import(pluginUrl.href);
235
+ this.log.debug(`Imported plugin ${plg}${plugin?.name}${db} from ${pluginUrl.href}`);
142
236
  // Call the default export function of the plugin, passing this MatterBridge instance
143
- if (plugin.default) {
144
- const platform = plugin.default(this, new AnsiLogger({ logName: packageJson.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ }));
237
+ if (pluginInstance.default) {
238
+ const platform = pluginInstance.default(this, new AnsiLogger({ logName: packageJson.description, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */ }));
145
239
  platform.name = packageJson.name;
146
240
  if (mode === 'load') {
147
- this.log.info(`Plugin ${BLUE}${packageJsonPath}${RESET} type ${GREEN}${platform.type}${RESET} loaded (entrypoint ${UNDERLINE}${pluginPath}${UNDERLINEOFF})`);
241
+ this.log.info(`Plugin ${plg}${plugin?.name}${nf} type ${GREEN}${platform.type}${nf} loaded (entrypoint ${UNDERLINE}${pluginPath}${UNDERLINEOFF})`);
148
242
  // Update plugin info
149
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === packageJson.name);
150
243
  if (plugin) {
244
+ plugin.path = packageJsonPath;
151
245
  plugin.name = packageJson.name;
152
246
  plugin.description = packageJson.description;
153
247
  plugin.version = packageJson.version;
154
248
  plugin.author = packageJson.author;
155
249
  plugin.type = platform.type;
156
250
  plugin.loaded = true;
251
+ plugin.platform = platform;
157
252
  }
158
253
  else {
159
- this.log.error(`Plugin ${packageJson.name} not found`);
160
- }
161
- // Register handlers
162
- if (platform.type === 'AccessoryPlatform') {
163
- platform.on('registerDeviceAccessoryPlatform', (device) => {
164
- this.log.debug(`Received ${REVERSE}registerDeviceAccessoryPlatform${REVERSEOFF} for device ${device.name}`);
165
- });
166
- }
167
- else if (platform.type === 'DynamicPlatform') {
168
- platform.on('registerDeviceDynamicPlatform', (device) => {
169
- this.log.debug(`Received ${REVERSE}registerDeviceDynamicPlatform${REVERSEOFF} for device ${device.name}`);
170
- });
171
- }
172
- else {
173
- this.log.error(`loadPlugin error platform.type ${REVERSE}${platform.type}${REVERSEOFF} for plugin ${packageJson.name}`);
254
+ this.log.error(`Plugin ${plg}${packageJson.name}${er} not found`);
174
255
  }
175
256
  }
176
257
  else if (mode === 'add') {
@@ -182,34 +263,61 @@ export class Matterbridge extends EventEmitter {
182
263
  version: packageJson.version,
183
264
  description: packageJson.description,
184
265
  author: packageJson.author,
266
+ enabled: true,
185
267
  });
186
- await this.context?.set('plugins', this.registeredPlugins);
187
- this.log.info(`Plugin ${packageJsonPath} type ${platform.type} added to matterbridge`);
268
+ await this.nodeContext?.set('plugins', this.registeredPlugins);
269
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${platform.type} added to matterbridge`);
188
270
  }
189
271
  else {
190
- this.log.warn(`Plugin ${packageJsonPath} already added to matterbridge`);
272
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} already added to matterbridge`);
191
273
  }
192
274
  }
193
275
  else if (mode === 'remove') {
194
- if (this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
195
- this.registeredPlugins.splice(this.registeredPlugins.findIndex((plugin) => plugin.name === packageJson.name));
196
- await this.context?.set('plugins', this.registeredPlugins);
197
- this.log.info(`Plugin ${packageJsonPath} removed from matterbridge`);
276
+ if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
277
+ this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
278
+ await this.nodeContext?.set('plugins', this.registeredPlugins);
279
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
198
280
  }
199
281
  else {
200
- this.log.warn(`Plugin ${packageJsonPath} not registerd in matterbridge`);
282
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
201
283
  }
202
284
  }
203
- }
204
- else {
205
- this.log.error(`Plugin at ${pluginPath} does not provide a default export`);
285
+ else if (mode === 'enable') {
286
+ const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
287
+ if (plugin) {
288
+ plugin.enabled = true;
289
+ await this.nodeContext?.set('plugins', this.registeredPlugins);
290
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} enabled`);
291
+ }
292
+ else {
293
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
294
+ }
295
+ }
296
+ else if (mode === 'disable') {
297
+ const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name);
298
+ if (plugin) {
299
+ plugin.enabled = false;
300
+ await this.nodeContext?.set('plugins', this.registeredPlugins);
301
+ this.log.info(`Plugin ${plg}${packageJsonPath}${nf} disabled`);
302
+ }
303
+ else {
304
+ this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
305
+ }
306
+ }
307
+ else {
308
+ this.log.error(`Plugin at ${plg}${pluginPath}${er} does not provide a default export`);
309
+ }
206
310
  }
207
311
  }
208
312
  catch (err) {
209
- this.log.error(`Failed to load plugin from ${packageJsonPath}: ${err}`);
313
+ this.log.error(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
210
314
  }
211
315
  }
212
- registerSignalHandlers() {
316
+ /**
317
+ * Registers the signal handlers for SIGINT and SIGTERM.
318
+ * When either of these signals are received, the cleanup method is called with an appropriate message.
319
+ */
320
+ async registerSignalHandlers() {
213
321
  process.on('SIGINT', async () => {
214
322
  await this.cleanup('SIGINT received, cleaning up...');
215
323
  });
@@ -217,71 +325,127 @@ export class Matterbridge extends EventEmitter {
217
325
  await this.cleanup('SIGTERM received, cleaning up...');
218
326
  });
219
327
  }
328
+ /**
329
+ * Performs cleanup operations before shutting down Matterbridge.
330
+ * @param message - The reason for the cleanup.
331
+ */
220
332
  async cleanup(message) {
221
333
  if (!this.hasCleanupStarted) {
222
334
  this.hasCleanupStarted = true;
223
335
  this.log.debug(message);
224
- // Emitting the shutdown event with a reason
225
- this.emit('shutdown', 'Matterbridge is closing: ' + message);
336
+ // Callint the shutdown functions with a reason
337
+ this.registeredPlugins.forEach((plugin) => {
338
+ if (plugin.platform) {
339
+ plugin.platform.onShutdown('Matterbridge is closing: ' + message);
340
+ }
341
+ });
226
342
  // Set reachability to false
227
- this.log.debug(`*Changing reachability for ${this.registeredDevices.length} devices:`);
228
- this.registeredDevices.forEach((device) => {
229
- this.log.debug(`*--child device: ${device.device.name}`);
230
- if (this.bridgeMode === 'bridge')
231
- device.device.setBridgedDeviceReachability(false);
343
+ /*
344
+ this.log.debug(`*Changing reachability to false for ${this.registeredDevices.length} devices (${this.bridgeMode} mode):`);
345
+ this.registeredDevices.forEach((registeredDevice) => {
346
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === registeredDevice.plugin);
347
+ if (!plugin) {
348
+ this.log.error(`Plugin ${plg}${registeredDevice.plugin}${er} not found`);
349
+ return;
350
+ }
351
+ this.log.debug(`*-- device: ${dev}${registeredDevice.device.name}${db} plugin ${plg}${registeredDevice.plugin}${db} type ${GREEN}${plugin.type}${db}`);
352
+ if (this.bridgeMode === 'bridge') registeredDevice.device.setBridgedDeviceReachability(false);
353
+ if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') plugin.aggregator?.removeBridgedDevice(registeredDevice.device);
354
+ if (this.bridgeMode === 'childbridge') plugin.commissioningServer?.setReachability(false);
355
+ if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform') this.setReachableAttribute(registeredDevice.device, false);
356
+ if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatform') registeredDevice.device.setBridgedDeviceReachability(false);
232
357
  });
358
+ */
233
359
  setTimeout(async () => {
234
360
  // Closing matter
235
361
  await this.stopMatter();
236
362
  // Closing storage
237
363
  await this.stopStorage();
364
+ //await this.context?.set<RegisteredDevice[]>('plugins', this.registeredDevices);
238
365
  this.log.debug('Cleanup completed.');
239
366
  process.exit(0);
240
- }, this.bridgeMode === 'bridge' ? 2000 : 0);
367
+ }, 2 * 1000);
368
+ }
369
+ }
370
+ /**
371
+ * Sets the reachable attribute of a device.
372
+ *
373
+ * @param device - The device for which to set the reachable attribute.
374
+ * @param reachable - The value to set for the reachable attribute.
375
+ */
376
+ setReachableAttribute(device, reachable) {
377
+ const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
378
+ if (!basicInformationCluster) {
379
+ this.log.error('setReachableAttribute BasicInformationCluster needs to be set!');
380
+ return;
241
381
  }
382
+ basicInformationCluster.setReachableAttribute(reachable);
242
383
  }
384
+ /**
385
+ * Adds a device to the Matterbridge.
386
+ * @param pluginName - The name of the plugin.
387
+ * @param device - The device to be added.
388
+ */
243
389
  async addDevice(pluginName, device) {
390
+ this.log.info(`Adding device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
391
+ // Check if the plugin is registered
392
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
393
+ if (!plugin) {
394
+ this.log.error(`addDevice error: device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${er} not found`);
395
+ return;
396
+ }
397
+ // Add and register the device to the matterbridge in bridge mode
244
398
  if (this.bridgeMode === 'bridge') {
245
399
  const basic = device.getClusterServerById(BasicInformationCluster.id);
246
400
  if (!basic) {
247
- this.log.error('addDevice error: cannot find the BasicInformationCluster');
401
+ this.log.error(`addDevice error: cannot find the BasicInformationCluster device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${nf}`);
248
402
  return;
249
403
  }
250
- device.createDefaultBridgedDeviceBasicInformationClusterServer(basic.getNodeLabelAttribute(), basic.getUniqueIdAttribute(), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductNameAttribute());
404
+ device.createDefaultBridgedDeviceBasicInformationClusterServer(basic.getNodeLabelAttribute(), basic.getSerialNumberAttribute(), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductNameAttribute(), basic.getSoftwareVersionAttribute(), basic.getSoftwareVersionStringAttribute(), basic.getHardwareVersionAttribute(), basic.getHardwareVersionStringAttribute());
405
+ //console.log(basic.getSoftwareVersionAttribute(), basic.getSoftwareVersionStringAttribute());
251
406
  this.matterAggregator.addBridgedDevice(device);
252
- this.registeredDevices.push({ plugin: pluginName, device });
253
- this.log.debug(`addDevice called from plugin ${pluginName}`);
407
+ this.registeredDevices.push({ plugin: pluginName, device, added: true });
408
+ this.log.info(`Added and registered device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
254
409
  }
410
+ // Only register the device in childbridge mode
255
411
  if (this.bridgeMode === 'childbridge') {
256
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
257
- if (plugin) {
258
- plugin.started = true;
259
- this.registeredDevices.push({ plugin: pluginName, device });
260
- this.log.debug(`addDevice called from plugin ${pluginName}`);
261
- }
262
- else {
263
- this.log.error(`addDevice error: plugin ${pluginName} not found`);
264
- }
412
+ this.registeredDevices.push({ plugin: pluginName, device, added: false });
413
+ this.log.info(`Registered device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
265
414
  }
266
415
  }
416
+ /**
417
+ * Adds a bridged device to the Matterbridge.
418
+ * @param pluginName - The name of the plugin.
419
+ * @param device - The bridged device to add.
420
+ */
267
421
  async addBridgedDevice(pluginName, device) {
422
+ this.log.info(`Adding bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
423
+ // Check if the plugin is registered
424
+ const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
425
+ if (!plugin) {
426
+ this.log.error(`addBridgedDevice error: device ${dev}${device.name}${nf} plugin ${plg}${pluginName}${er} not found`);
427
+ return;
428
+ }
429
+ // Add and register the device to the matterbridge in bridge mode
268
430
  if (this.bridgeMode === 'bridge') {
269
431
  this.matterAggregator.addBridgedDevice(device);
270
- this.registeredDevices.push({ plugin: pluginName, device });
271
- this.log.debug(`addBridgedDevice called from plugin ${pluginName}`);
432
+ this.registeredDevices.push({ plugin: pluginName, device, added: true });
433
+ this.log.info(`Added and registered bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
272
434
  }
435
+ // Only register the device in childbridge mode
273
436
  if (this.bridgeMode === 'childbridge') {
274
- const plugin = this.registeredPlugins.find((plugin) => plugin.name === pluginName);
275
- if (plugin) {
276
- plugin.started = true;
277
- this.registeredDevices.push({ plugin: pluginName, device });
278
- this.log.debug(`addBridgedDevice called from plugin ${pluginName}`);
279
- }
280
- else {
281
- this.log.error(`addBridgedDevice error: plugin ${pluginName} not found`);
282
- }
437
+ this.registeredDevices.push({ plugin: pluginName, device, added: false });
438
+ this.log.info(`Registered bridged device ${dev}${device.name}${nf} for plugin ${plg}${pluginName}${nf}`);
439
+ //const basic = device.getClusterServerById(BridgedDeviceBasicInformationCluster.id);
440
+ //console.log(JSON.stringify(basic, null, 2));
283
441
  }
284
442
  }
443
+ /**
444
+ * Starts the storage process based on the specified storage type and name.
445
+ * @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
446
+ * @param {string} storageName - The name of the storage file.
447
+ * @returns {Promise<void>} - A promise that resolves when the storage process is started.
448
+ */
285
449
  async startStorage(storageType, storageName) {
286
450
  if (!storageName.endsWith('.json')) {
287
451
  storageName += '.json';
@@ -299,7 +463,7 @@ export class Matterbridge extends EventEmitter {
299
463
  await this.storageManager.initialize();
300
464
  this.log.debug('Storage initialized');
301
465
  if (storageType === 'json') {
302
- this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
466
+ await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
303
467
  }
304
468
  }
305
469
  catch (error) {
@@ -307,6 +471,12 @@ export class Matterbridge extends EventEmitter {
307
471
  process.exit(1);
308
472
  }
309
473
  }
474
+ /**
475
+ * Makes a backup copy of the specified JSON storage file.
476
+ *
477
+ * @param storageName - The name of the JSON storage file to be backed up.
478
+ * @param backupName - The name of the backup file to be created.
479
+ */
310
480
  async backupJsonStorage(storageName, backupName) {
311
481
  try {
312
482
  this.log.debug(`Making backup copy of ${storageName}`);
@@ -327,94 +497,103 @@ export class Matterbridge extends EventEmitter {
327
497
  }
328
498
  }
329
499
  }
500
+ /**
501
+ * Stops the storage.
502
+ * @returns {Promise<void>} A promise that resolves when the storage is stopped.
503
+ */
330
504
  async stopStorage() {
331
505
  this.log.debug('Stopping storage');
332
- await this.storageManager?.close();
506
+ await this.storageManager.close();
333
507
  this.log.debug('Storage closed');
334
508
  }
509
+ /**
510
+ * Starts the Matterbridge based on the bridge mode.
511
+ * If the bridge mode is 'bridge', it creates a commissioning server, matter aggregator,
512
+ * and starts the matter server.
513
+ * If the bridge mode is 'childbridge', it starts the plugins, creates commissioning servers,
514
+ * and starts the matter server when all plugins are loaded and started.
515
+ * @private
516
+ * @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
517
+ */
335
518
  async startMatterBridge() {
336
519
  this.log.debug('Starting matterbridge in mode', this.bridgeMode);
337
520
  await this.createMatterServer(this.storageManager);
338
- this.log.debug('Creating matterbridge context: matterbridge');
339
- this.matterbridgeContext = this.storageManager.createContext('matterbridge');
340
- this.matterbridgeContext.set('port', 5500);
341
- this.matterbridgeContext.set('passcode', 20232024);
342
- this.matterbridgeContext.set('discriminator', 3940);
343
- this.matterbridgeContext.set('deviceName', 'matterbridge aggregator');
344
- this.matterbridgeContext.set('deviceType', DeviceTypes.AGGREGATOR.code);
345
- this.matterbridgeContext.set('vendorId', 0xfff1);
346
- this.matterbridgeContext.set('vendorName', 'matterbridge');
347
- this.matterbridgeContext.set('productId', 0x8000);
348
- this.matterbridgeContext.set('productName', 'node-matterbridge');
349
- this.matterbridgeContext.set('uniqueId', this.matterbridgeContext.get('uniqueId', CryptoNode.getRandomData(8).toHex()));
350
- this.log.debug('Creating matterbridge commissioning server');
351
- this.commissioningServer = await this.createMatterCommisioningServer(this.matterbridgeContext, 'Matterbridge');
352
521
  if (this.bridgeMode === 'bridge') {
522
+ // Plugins are loaded by loadPlugin on startup and plugin.loaded is set to true
523
+ // Plugins are started by callback when Matterbridge is commissioned and plugin.started is set to true
524
+ this.log.debug('Creating commissioning server context for Matterbridge');
525
+ this.matterbridgeContext = this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggragator');
526
+ this.log.debug('Creating commissioning server for Matterbridge');
527
+ this.commissioningServer = this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
353
528
  this.log.debug('Creating matter aggregator for matterbridge');
354
- this.matterAggregator = await this.createMatterAggregator();
355
- this.log.debug('Adding matter aggregator to commissioning server');
529
+ this.matterAggregator = this.createMatterAggregator(this.matterbridgeContext);
530
+ this.log.debug('Adding matterbridge aggregator to matterbridge commissioning server');
356
531
  this.commissioningServer.addDevice(this.matterAggregator);
357
- this.matterServer.addCommissioningServer(this.commissioningServer);
532
+ this.log.debug('Adding matterbridge commissioning server to matter server');
533
+ await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
358
534
  this.log.debug('Starting matter server');
359
535
  await this.matterServer.start();
360
536
  this.log.debug('Started matter server');
361
537
  this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
362
538
  }
363
539
  if (this.bridgeMode === 'childbridge') {
364
- this.log.debug('Creating matter aggregator for matterbridge');
365
- this.matterAggregator = await this.createMatterAggregator();
366
- this.log.debug('Adding matter aggregator to commissioning server');
367
- this.commissioningServer.addDevice(this.matterAggregator);
368
- this.matterServer.addCommissioningServer(this.commissioningServer);
540
+ // Plugins are loaded by loadPlugin on startup and plugin.loaded is set to true
541
+ // Plugins are started here and plugin.started is set to true.
542
+ // addDevice and addBridgedDeevice just register the devices that are added here to the plugin commissioning server for Accessory Platform
543
+ // or to the plugin aggregator for Dynamic Platform after the commissioning is done
369
544
  this.registeredPlugins.forEach(async (plugin) => {
370
- // Start the interval to check if all plugins are loaded
545
+ if (!plugin.enabled)
546
+ return;
547
+ // Start the interval to check if the plugin is loaded
371
548
  const loadedInterval = setInterval(async () => {
372
- this.log.debug(`Waiting for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
549
+ this.log.debug(`Waiting in load interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
373
550
  if (!plugin.loaded)
374
551
  return;
375
- this.log.debug(`Sending start platform for plugin ${BLUE}${plugin.name}${db}`);
376
- this.emit('startAccessoryPlatform', 'Matterbridge is commissioned and controllers are connected');
377
- this.emit('startDynamicPlatform', 'Matterbridge is commissioned and controllers are connected');
552
+ plugin.started = true;
553
+ plugin.registeredDevices = 0;
378
554
  clearInterval(loadedInterval);
379
555
  }, 1000);
380
- // Start the interval to check if all plugins are started
556
+ // Start the interval to check if the plugins is started
381
557
  const startedInterval = setInterval(async () => {
382
- this.log.debug(`**Waiting for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
558
+ this.log.debug(`Waiting in started interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
383
559
  if (!plugin.started)
384
560
  return;
385
- this.log.debug(`**Creating storage context for plugin ${BLUE}${plugin.name}${db}`);
386
- plugin.storageContext = this.storageManager.createContext(plugin.name);
387
- //plugin.storageContext.set('port', undefined);
388
- //plugin.storageContext.set('passcode', undefined);
389
- //plugin.storageContext.set('discriminator', undefined);
390
- plugin.storageContext.set('deviceName', 'matterbridge aggregator');
391
- plugin.storageContext.set('deviceType', DeviceTypes.AGGREGATOR.code);
392
- plugin.storageContext.set('vendorId', 0xfff1);
393
- plugin.storageContext.set('vendorName', 'matterbridge');
394
- plugin.storageContext.set('productId', 0x8000);
395
- plugin.storageContext.set('productName', plugin.name.slice(0, 32));
396
- plugin.storageContext.set('uniqueId', plugin.storageContext.get('uniqueId', CryptoNode.getRandomData(8).toHex()));
397
- this.log.debug(`**Creating commissioning server for plugin ${BLUE}${plugin.name}${db}`);
398
- plugin.commissioningServer = await this.createMatterCommisioningServer(plugin.storageContext, plugin.name);
399
561
  if (plugin.type === 'AccessoryPlatform') {
400
- this.registeredDevices.forEach((registeredDevice) => {
562
+ this.log.debug(`Starting accessory platform for plugin ${plg}${plugin.name}${db}`);
563
+ if (plugin.platform)
564
+ await plugin.platform.onStart('Matterbridge Accessory platform has started commissioning');
565
+ else
566
+ this.log.error(`Platform not found for plugin ${plg}${plugin.name}${er}`);
567
+ this.registeredDevices.forEach(async (registeredDevice) => {
401
568
  if (registeredDevice.plugin === plugin.name) {
402
- plugin.commissioningServer?.addDevice(registeredDevice.device);
403
- this.matterServer.addCommissioningServer(plugin.commissioningServer);
569
+ plugin.storageContext = this.importCommissioningServerContext(plugin.name, registeredDevice.device); // Generate serialNumber and uniqueId
570
+ plugin.commissioningServer = this.createCommisioningServer(plugin.storageContext, plugin.name);
571
+ this.log.debug(`Adding device ${dev}${registeredDevice.device.name}${db} to commissioning server for plugin ${plg}${plugin.name}${db}`);
572
+ plugin.commissioningServer.addDevice(registeredDevice.device);
573
+ if (plugin.registeredDevices !== undefined)
574
+ plugin.registeredDevices++;
575
+ this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db} `);
576
+ await this.matterServer.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
404
577
  return;
405
578
  }
406
579
  });
407
580
  }
408
581
  if (plugin.type === 'DynamicPlatform') {
409
- this.log.debug(`**Creating aggregator for plugin ${BLUE}${plugin.name}${db}`);
410
- plugin.aggregator = await this.createMatterAggregator();
411
- this.log.debug(`**Adding matter aggregator to commissioning server for plugin ${BLUE}${plugin.name}${db}`);
412
- plugin.commissioningServer?.addDevice(plugin.aggregator);
413
- this.registeredDevices.forEach((registeredDevice) => {
414
- if (registeredDevice.plugin === plugin.name)
415
- plugin.aggregator?.addBridgedDevice(registeredDevice.device);
416
- });
417
- this.matterServer.addCommissioningServer(plugin.commissioningServer);
582
+ plugin.storageContext = this.createCommissioningServerContext(
583
+ // Generate serialNumber and uniqueId
584
+ plugin.name, 'Matterbridge Dynamic Platform', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Dynamic Platform');
585
+ plugin.commissioningServer = this.createCommisioningServer(plugin.storageContext, plugin.name);
586
+ this.log.debug(`Creating aggregator for plugin ${plg}${plugin.name}${db}`);
587
+ plugin.aggregator = this.createMatterAggregator(plugin.storageContext); // Generate serialNumber and uniqueId
588
+ this.log.debug(`Starting dynamic platform for plugin ${plg}${plugin.name}${db}`);
589
+ if (plugin.platform)
590
+ await plugin.platform.onStart('Matterbridge Dynamic platform has started commissioning');
591
+ else
592
+ this.log.error(`Platform not found for plugin ${plg}${plugin.name}${er}`);
593
+ this.log.debug(`Adding matter aggregator to commissioning server for plugin ${plg}${plugin.name}${db}`);
594
+ plugin.commissioningServer.addDevice(plugin.aggregator);
595
+ this.log.debug(`Adding commissioning server to matter server for plugin ${plg}${plugin.name}${db}`);
596
+ await this.matterServer.addCommissioningServer(plugin.commissioningServer, { uniqueStorageKey: plugin.name });
418
597
  }
419
598
  clearInterval(startedInterval);
420
599
  }, 1000);
@@ -422,30 +601,112 @@ export class Matterbridge extends EventEmitter {
422
601
  // Start the interval to check if all plugins are loaded and started and so start the matter server
423
602
  const startMatterInterval = setInterval(async () => {
424
603
  let allStarted = true;
425
- this.registeredPlugins.forEach(async (plugin) => {
426
- this.log.debug(`***Waiting in startMatter interval for plugin ${BLUE}${plugin.name}${db} to load (${plugin.loaded}) and send device (${plugin.started})...`);
427
- if (!plugin.loaded || !plugin.started)
604
+ this.registeredPlugins.forEach((plugin) => {
605
+ if (!plugin.enabled)
606
+ return;
607
+ this.log.debug(`Waiting in start matter server interval for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) and send devices ...`);
608
+ if (plugin.enabled && (!plugin.loaded || !plugin.started))
428
609
  allStarted = false;
429
610
  });
430
611
  if (!allStarted)
431
612
  return;
432
- this.log.debug('***Starting matter server from startMatter interval');
613
+ // Setting reachability to true
614
+ this.registeredPlugins.forEach((plugin) => {
615
+ if (!plugin.enabled)
616
+ return;
617
+ this.log.debug(`Setting reachability to true for ${plg}${plugin.name}${db}`);
618
+ plugin.commissioningServer?.setReachability(true);
619
+ this.registeredDevices.forEach((registeredDevice) => {
620
+ if (registeredDevice.plugin === plugin.name) {
621
+ if (plugin.type === 'AccessoryPlatform')
622
+ this.setReachableAttribute(registeredDevice.device, true);
623
+ if (plugin.type === 'DynamicPlatform')
624
+ registeredDevice.device.setBridgedDeviceReachability(true);
625
+ }
626
+ });
627
+ });
628
+ this.log.debug('Starting matter server');
433
629
  await this.matterServer.start();
434
- this.log.debug('***Started matter server from startMatter interval');
435
- this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, 'Matterbridge');
630
+ this.log.debug('Started matter server');
436
631
  this.registeredPlugins.forEach(async (plugin) => {
437
632
  this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.name);
438
633
  });
634
+ Logger.defaultLogLevel = Level.DEBUG;
439
635
  clearInterval(startMatterInterval);
440
636
  }, 1000);
441
637
  return;
442
638
  }
443
639
  }
640
+ /**
641
+ * Imports the commissioning server context for a specific plugin and device.
642
+ * @param pluginName - The name of the plugin.
643
+ * @param device - The MatterbridgeDevice object representing the device.
644
+ * @returns The commissioning server context.
645
+ * @throws Error if the BasicInformationCluster is not found.
646
+ */
647
+ importCommissioningServerContext(pluginName, device) {
648
+ this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
649
+ const basic = device.getClusterServer(BasicInformationCluster);
650
+ if (!basic) {
651
+ throw new Error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
652
+ }
653
+ //const random = 'CS' + CryptoNode.getRandomData(8).toHex();
654
+ return this.createCommissioningServerContext(pluginName, basic.getNodeLabelAttribute(), DeviceTypeId(device.deviceType), basic.getVendorIdAttribute(), basic.getVendorNameAttribute(), basic.getProductIdAttribute(), basic.getProductNameAttribute(), basic.attributes.serialNumber?.getLocal(), basic.attributes.uniqueId?.getLocal(), basic.attributes.softwareVersion?.getLocal(), basic.attributes.softwareVersionString?.getLocal(), basic.attributes.hardwareVersion?.getLocal(), basic.attributes.hardwareVersionString?.getLocal());
655
+ }
656
+ /**
657
+ * Creates a commissioning server storage context.
658
+ *
659
+ * @param pluginName - The name of the plugin.
660
+ * @param deviceName - The name of the device.
661
+ * @param deviceType - The type of the device.
662
+ * @param vendorId - The vendor ID.
663
+ * @param vendorName - The vendor name.
664
+ * @param productId - The product ID.
665
+ * @param productName - The product name.
666
+ * @param serialNumber - The serial number of the device (optional).
667
+ * @param uniqueId - The unique ID of the device (optional).
668
+ * @param softwareVersion - The software version of the device (optional).
669
+ * @param softwareVersionString - The software version string of the device (optional).
670
+ * @param hardwareVersion - The hardware version of the device (optional).
671
+ * @param hardwareVersionString - The hardware version string of the device (optional).
672
+ * @returns The storage context for the commissioning server.
673
+ */
674
+ createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId, softwareVersion, softwareVersionString, hardwareVersion, hardwareVersionString) {
675
+ this.log.debug(`Creating commissioning server storage context for ${plg}${pluginName}${db}`);
676
+ const random = 'CS' + CryptoNode.getRandomData(8).toHex();
677
+ const storageContext = this.storageManager.createContext(pluginName);
678
+ storageContext.set('deviceName', deviceName);
679
+ storageContext.set('deviceType', deviceType);
680
+ storageContext.set('vendorId', vendorId);
681
+ storageContext.set('vendorName', vendorName.slice(0, 32));
682
+ storageContext.set('productId', productId);
683
+ storageContext.set('productName', productName.slice(0, 32));
684
+ storageContext.set('nodeLabel', productName.slice(0, 32));
685
+ storageContext.set('productLabel', productName.slice(0, 32));
686
+ storageContext.set('serialNumber', storageContext.get('serialNumber', random));
687
+ storageContext.set('uniqueId', storageContext.get('uniqueId', random));
688
+ storageContext.set('softwareVersion', softwareVersion ?? 1);
689
+ storageContext.set('softwareVersionString', softwareVersionString ?? '1.0.0');
690
+ storageContext.set('hardwareVersion', hardwareVersion ?? 1);
691
+ storageContext.set('hardwareVersionString', hardwareVersionString ?? '1.0.0');
692
+ return storageContext;
693
+ }
694
+ /**
695
+ * Shows the commissioning QR code for a given commissioning server, storage context, and name.
696
+ * If any of the parameters are missing, the method returns early.
697
+ * If the commissioning server is not commissioned, it logs the QR code and pairing code.
698
+ * If the commissioning server is already commissioned, it waits for controllers to connect.
699
+ * If the bridge mode is 'childbridge', it sets the 'paired' property of the plugin to true.
700
+ *
701
+ * @param commissioningServer - The commissioning server to show the QR code for.
702
+ * @param storageContext - The storage context to store the pairing codes.
703
+ * @param name - The name of the commissioning server.
704
+ */
444
705
  showCommissioningQRCode(commissioningServer, storageContext, name) {
445
706
  if (!commissioningServer || !storageContext || !name)
446
707
  return;
447
708
  if (!commissioningServer.isCommissioned()) {
448
- this.log.info(`***The commissioning server for ${BLUE}${name}${nf} is not commissioned. Pair it and restart the process to run matterbridge.`);
709
+ this.log.info(`***The commissioning server for ${plg}${name}${nf} is not commissioned. Pair it scanning the QR code ...`);
449
710
  const { qrPairingCode, manualPairingCode } = commissioningServer.getPairingCode();
450
711
  storageContext.set('qrPairingCode', qrPairingCode);
451
712
  storageContext.set('manualPairingCode', manualPairingCode);
@@ -453,23 +714,48 @@ export class Matterbridge extends EventEmitter {
453
714
  this.log.debug(`Pairing code\n\n${QrCode.encode(qrPairingCode)}\nManual pairing code: ${manualPairingCode}\n`);
454
715
  }
455
716
  else {
456
- this.log.info(`***The commissioning server for ${BLUE}${name}${nf} is already commissioned. Waiting for controllers to connect ...`);
717
+ this.log.info(`***The commissioning server for ${plg}${name}${nf} is already commissioned. Waiting for controllers to connect ...`);
718
+ if (this.bridgeMode === 'childbridge') {
719
+ const plugin = this.findPlugin(name);
720
+ if (plugin)
721
+ plugin.paired = true;
722
+ }
457
723
  }
458
724
  }
459
- async createMatterCommisioningServer(context, name) {
460
- //const port = context.get('port') as number;
461
- //const passcode = context.get('passcode') as number;
462
- //const discriminator = context.get('discriminator') as number;
725
+ /**
726
+ * Finds a plugin by its name.
727
+ *
728
+ * @param pluginName - The name of the plugin to find.
729
+ * @returns The found plugin, or undefined if not found.
730
+ */
731
+ findPlugin(pluginName) {
732
+ const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === pluginName);
733
+ if (!plugin) {
734
+ this.log.error(`Plugin ${plg}${pluginName}${er} not found`);
735
+ return;
736
+ }
737
+ return plugin;
738
+ }
739
+ /**
740
+ * Creates a matter commissioning server.
741
+ *
742
+ * @param {StorageContext} context - The storage context.
743
+ * @param {string} name - The name of the commissioning server.
744
+ * @returns {CommissioningServer} The created commissioning server.
745
+ */
746
+ createCommisioningServer(context, name) {
747
+ this.log.debug(`Creating matter commissioning server for plugin ${plg}${name}${db}`);
463
748
  const deviceName = context.get('deviceName');
464
749
  const deviceType = context.get('deviceType');
465
750
  const vendorId = context.get('vendorId');
466
751
  const vendorName = context.get('vendorName'); // Home app = Manufacturer
467
752
  const productId = context.get('productId');
468
753
  const productName = context.get('productName'); // Home app = Model
754
+ const serialNumber = context.get('serialNumber');
469
755
  const uniqueId = context.get('uniqueId');
756
+ this.log.debug(
470
757
  // eslint-disable-next-line max-len
471
- //this.log.debug(`Creating matter commissioning server with port ${port} passcode ${passcode} discriminator ${discriminator} deviceName ${deviceName} deviceType ${deviceType}`);
472
- this.log.debug(`Creating matter commissioning server with deviceName ${deviceName} deviceType ${deviceType}`);
758
+ `Creating matter commissioning server for plugin ${plg}${name}${db} with deviceName ${deviceName} deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')}) uniqueId ${uniqueId} serialNumber ${serialNumber}`);
473
759
  const commissioningServer = new CommissioningServer({
474
760
  port: undefined,
475
761
  passcode: undefined,
@@ -483,44 +769,128 @@ export class Matterbridge extends EventEmitter {
483
769
  productName,
484
770
  nodeLabel: productName,
485
771
  productLabel: productName,
486
- softwareVersion: 1,
487
- softwareVersionString: '1.0.0', // Home app = Firmware Revision
488
- hardwareVersion: 1,
489
- hardwareVersionString: '1.0.0',
772
+ softwareVersion: context.get('softwareVersion', 1),
773
+ softwareVersionString: context.get('softwareVersionString', '1.0.0'), // Home app = Firmware Revision
774
+ hardwareVersion: context.get('hardwareVersion', 1),
775
+ hardwareVersionString: context.get('hardwareVersionString', '1.0.0'),
490
776
  uniqueId,
491
- serialNumber: `matterbridge-${uniqueId}`,
777
+ serialNumber,
492
778
  reachable: true,
493
779
  },
494
780
  activeSessionsChangedCallback: (fabricIndex) => {
495
781
  const info = commissioningServer.getActiveSessionInformation(fabricIndex);
496
- this.log.debug(`Active sessions changed on fabric ${fabricIndex}`, debugStringify(info));
782
+ this.log.debug(`***Active sessions changed on fabric ${fabricIndex} for ${plg}${name}${nf}`, debugStringify(info));
497
783
  if (info && info[0]?.isPeerActive === true && info[0]?.secure === true && info[0]?.numberOfActiveSubscriptions >= 1) {
498
- this.log.info(`Controller connected to ${BLUE}${name}${nf} ready to start...`);
499
- Logger.defaultLogLevel = Level.INFO;
784
+ this.log.info(`***Controller connected to ${plg}${name}${nf} ready to start...`);
785
+ if (this.bridgeMode === 'childbridge') {
786
+ const plugin = this.findPlugin(name);
787
+ if (plugin) {
788
+ if (plugin.connected === true)
789
+ return; // Only once cause the devices are already added to the plugins aggregator
790
+ plugin.paired = true;
791
+ plugin.connected = true;
792
+ }
793
+ }
500
794
  setTimeout(() => {
501
795
  if (this.bridgeMode === 'bridge') {
502
- this.emit('startAccessoryPlatform', 'Matterbridge is commissioned and controllers are connected');
503
- this.emit('startDynamicPlatform', 'Matterbridge is commissioned and controllers are connected');
796
+ //Logger.defaultLogLevel = Level.INFO;
797
+ this.registeredPlugins.forEach(async (plugin) => {
798
+ if (!plugin.enabled)
799
+ return;
800
+ if (plugin.platform && !plugin.started) {
801
+ this.log.info(`***Starting plugin ${plg}${plugin.name}${nf}`);
802
+ await plugin.platform.onStart('Matterbridge is commissioned and controllers are connected');
803
+ plugin.started = true;
804
+ this.log.info(`***Started plugin ${plg}${plugin.name}${nf}`);
805
+ }
806
+ else {
807
+ this.log.error(`***Platform not found for plugin ${plg}${plugin.name}${er}`);
808
+ }
809
+ });
810
+ Logger.defaultLogLevel = Level.DEBUG;
504
811
  }
812
+ if (this.bridgeMode === 'childbridge') {
813
+ //Logger.defaultLogLevel = Level.INFO;
814
+ const plugin = this.findPlugin(name);
815
+ if (!plugin || plugin.type === 'AccessoryPlatform')
816
+ return;
817
+ this.registeredDevices.forEach(async (registeredDevice) => {
818
+ if (registeredDevice.plugin !== name)
819
+ return;
820
+ this.log.debug(`***Adding device ${registeredDevice.device.name} to aggregator for plugin ${plg}${plugin.name}${db}`);
821
+ if (!plugin.aggregator) {
822
+ this.log.error(`***Aggregator not found for plugin ${plg}${plugin.name}${er}`);
823
+ return;
824
+ }
825
+ plugin.aggregator.addBridgedDevice(registeredDevice.device);
826
+ if (plugin.registeredDevices !== undefined)
827
+ plugin.registeredDevices++;
828
+ registeredDevice.added = true;
829
+ });
830
+ Logger.defaultLogLevel = Level.DEBUG;
831
+ }
832
+ //logEndpoint(commissioningServer.getRootEndpoint());
505
833
  }, 2000);
506
834
  }
507
835
  },
508
836
  commissioningChangedCallback: (fabricIndex) => {
509
837
  const info = commissioningServer.getCommissionedFabricInformation(fabricIndex);
510
- this.log.debug(`Commissioning changed on fabric ${fabricIndex}`, debugStringify(info));
838
+ this.log.debug(`***Commissioning changed on fabric ${fabricIndex} for ${plg}${name}${nf}`, debugStringify(info));
839
+ if (info.length === 0) {
840
+ this.log.warn(`***Commissioning removed from fabric ${fabricIndex} for ${plg}${name}${nf}`);
841
+ }
511
842
  },
512
843
  });
513
844
  commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
514
845
  return commissioningServer;
515
846
  }
516
- async createMatterServer(storageManager) {
847
+ /**
848
+ * Creates a Matter server using the provided storage manager.
849
+ * @param storageManager The storage manager to be used by the Matter server.
850
+ *
851
+ */
852
+ createMatterServer(storageManager) {
517
853
  this.log.debug('Creating matter server');
518
854
  this.matterServer = new MatterServer(storageManager, { mdnsAnnounceInterface: undefined });
519
855
  }
520
- async createMatterAggregator() {
856
+ /**
857
+ * Creates a Matter Aggregator.
858
+ * @param {StorageContext} context - The storage context.
859
+ * @returns {Aggregator} - The created Matter Aggregator.
860
+ */
861
+ createMatterAggregator(context) {
862
+ const random = 'AG' + CryptoNode.getRandomData(8).toHex();
863
+ context.set('aggregatorSerialNumber', context.get('aggregatorSerialNumber', random));
864
+ context.set('aggregatorUniqueId', context.get('aggregatorUniqueId', random));
521
865
  const matterAggregator = new Aggregator();
866
+ matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
867
+ dataModelRevision: 1,
868
+ location: 'XX',
869
+ vendorId: VendorId(0xfff1),
870
+ vendorName: 'Matterbridge',
871
+ productId: 0x8000,
872
+ productName: 'Matterbridge aggregator',
873
+ productLabel: 'Matterbridge aggregator',
874
+ nodeLabel: 'Matterbridge aggregator',
875
+ serialNumber: context.get('aggregatorSerialNumber'),
876
+ uniqueId: context.get('aggregatorUniqueId'),
877
+ softwareVersion: 1,
878
+ softwareVersionString: 'v.1.0',
879
+ hardwareVersion: 1,
880
+ hardwareVersionString: 'v.1.0',
881
+ reachable: true,
882
+ capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
883
+ }, {}, {
884
+ startUp: true,
885
+ shutDown: true,
886
+ leave: true,
887
+ reachableChanged: true,
888
+ }));
522
889
  return matterAggregator;
523
890
  }
891
+ /**
892
+ * Stops the Matter server and associated controllers.
893
+ */
524
894
  async stopMatter() {
525
895
  this.log.debug('Stopping matter commissioningServer');
526
896
  await this.commissioningServer?.close();
@@ -530,7 +900,10 @@ export class Matterbridge extends EventEmitter {
530
900
  await this.matterServer?.close();
531
901
  this.log.debug('Matter server closed');
532
902
  }
533
- logNodeAndSystemInfo() {
903
+ /**
904
+ * Logs the node and system information.
905
+ */
906
+ async logNodeAndSystemInfo() {
534
907
  // IP address information
535
908
  const networkInterfaces = os.networkInterfaces();
536
909
  this.systemInformation.ipv4Address = 'Not found';
@@ -579,16 +952,45 @@ export class Matterbridge extends EventEmitter {
579
952
  this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
580
953
  this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
581
954
  this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
582
- // Command line arguments (excluding 'node' and the script name)
583
- const cmdArgs = process.argv.slice(2).join(' ');
584
- this.log.debug(`Command Line Arguments: ${cmdArgs}`);
585
- // Current working directory
586
- const currentDir = process.cwd();
587
- this.log.debug(`Current Working Directory: ${currentDir}`);
955
+ // Home directory
956
+ this.homeDirectory = os.homedir();
957
+ this.log.debug(`Home Directory: ${this.homeDirectory}`);
588
958
  // Package root directory
589
959
  const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
590
960
  this.rootDirectory = path.resolve(currentFileDirectory, '../');
591
961
  this.log.debug(`Root Directory: ${this.rootDirectory}`);
962
+ // Create the data directory .matterbridge in the home directory
963
+ this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
964
+ try {
965
+ await fs.access(this.matterbridgeDirectory);
966
+ }
967
+ catch (err) {
968
+ await fs.mkdir(this.matterbridgeDirectory);
969
+ }
970
+ this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
971
+ // Current working directory
972
+ const currentDir = process.cwd();
973
+ this.log.debug(`Current Working Directory: ${currentDir}`);
974
+ // Command line arguments (excluding 'node' and the script name)
975
+ const cmdArgs = process.argv.slice(2).join(' ');
976
+ this.log.debug(`Command Line Arguments: ${cmdArgs}`);
977
+ }
978
+ getBaseRegisteredPlugin() {
979
+ const baseRegisteredPlugins = this.registeredPlugins.map((plugin) => ({
980
+ path: plugin.path,
981
+ type: plugin.type,
982
+ name: plugin.name,
983
+ version: plugin.version,
984
+ description: plugin.description,
985
+ author: plugin.author,
986
+ enabled: plugin.enabled,
987
+ loaded: plugin.loaded,
988
+ started: plugin.started,
989
+ paired: plugin.paired,
990
+ connected: plugin.connected,
991
+ registeredDevices: plugin.registeredDevices,
992
+ }));
993
+ return baseRegisteredPlugins;
592
994
  }
593
995
  /**
594
996
  * Initializes the frontend of Matterbridge.
@@ -596,8 +998,6 @@ export class Matterbridge extends EventEmitter {
596
998
  * @param port The port number to run the frontend server on. Default is 3000.
597
999
  */
598
1000
  async initializeFrontend(port = 3000) {
599
- //const __filename = fileURLToPath(import.meta.url);
600
- //const __dirname = path.dirname(__filename);
601
1001
  this.log.debug(`Initializing the frontend on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${rs}`);
602
1002
  this.app = express();
603
1003
  // Serve React build directory
@@ -605,8 +1005,19 @@ export class Matterbridge extends EventEmitter {
605
1005
  // Endpoint to provide QR pairing code
606
1006
  this.app.get('/api/qr-code', (req, res) => {
607
1007
  this.log.debug('The frontend sent /api/qr-code');
608
- const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode') };
609
- res.json(qrData);
1008
+ if (this.bridgeMode === 'childbridge') {
1009
+ this.log.debug('qrPairingCode for /api/qr-code not available in childbridge mode');
1010
+ res.json({});
1011
+ return;
1012
+ }
1013
+ try {
1014
+ const qrData = { qrPairingCode: this.matterbridgeContext.get('qrPairingCode'), manualPairingCode: this.matterbridgeContext.get('manualPairingCode') };
1015
+ res.json(qrData);
1016
+ }
1017
+ catch (error) {
1018
+ this.log.error('qrPairingCode for /api/qr-code not found');
1019
+ res.json({});
1020
+ }
610
1021
  });
611
1022
  // Endpoint to provide system information
612
1023
  this.app.get('/api/system-info', (req, res) => {
@@ -616,35 +1027,127 @@ export class Matterbridge extends EventEmitter {
616
1027
  // Endpoint to provide plugins
617
1028
  this.app.get('/api/plugins', (req, res) => {
618
1029
  this.log.debug('The frontend sent /api/plugins');
619
- const data = [];
620
- this.registeredPlugins.forEach((plugin) => {
621
- data.push({ name: plugin.name, description: plugin.description, version: plugin.version, author: plugin.author, type: plugin.type });
622
- });
623
- res.json(data);
1030
+ const baseRegisteredPlugins = this.getBaseRegisteredPlugin();
1031
+ res.json(baseRegisteredPlugins);
624
1032
  });
625
1033
  // Endpoint to provide devices
626
1034
  this.app.get('/api/devices', (req, res) => {
627
1035
  this.log.debug('The frontend sent /api/devices');
628
1036
  const data = [];
629
- this.registeredDevices.forEach((device) => {
630
- let name = device.device.getClusterServer(BasicInformationCluster)?.getNodeLabelAttribute();
631
- if (!name && device.device.getClusterServer(BridgedDeviceBasicInformationCluster)) {
632
- const bridgeInfo = device.device.getClusterServer(BridgedDeviceBasicInformationCluster);
633
- if (bridgeInfo && bridgeInfo.getNodeLabelAttribute)
634
- name = bridgeInfo.getNodeLabelAttribute();
1037
+ this.registeredDevices.forEach((registeredDevice) => {
1038
+ let name = registeredDevice.device.getClusterServer(BasicInformationCluster)?.attributes.nodeLabel?.getLocal();
1039
+ if (!name)
1040
+ name = registeredDevice.device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.nodeLabel?.getLocal() ?? 'Unknown';
1041
+ let serial = registeredDevice.device.getClusterServer(BasicInformationCluster)?.attributes.serialNumber?.getLocal();
1042
+ if (!serial)
1043
+ serial = registeredDevice.device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.serialNumber?.getLocal() ?? 'Unknown';
1044
+ let uniqueId = registeredDevice.device.getClusterServer(BasicInformationCluster)?.attributes.uniqueId?.getLocal();
1045
+ if (!uniqueId)
1046
+ uniqueId = registeredDevice.device.getClusterServer(BridgedDeviceBasicInformationCluster)?.attributes.uniqueId?.getLocal() ?? 'Unknown';
1047
+ const cluster = this.getClusterTextFromDevice(registeredDevice.device);
1048
+ data.push({
1049
+ pluginName: registeredDevice.plugin,
1050
+ type: registeredDevice.device.name + ' (0x' + registeredDevice.device.deviceType.toString(16).padStart(4, '0') + ')',
1051
+ endpoint: registeredDevice.device.id,
1052
+ name,
1053
+ serial,
1054
+ uniqueId,
1055
+ cluster: cluster,
1056
+ });
1057
+ });
1058
+ res.json(data);
1059
+ });
1060
+ // Endpoint to provide the cluster servers of the devices
1061
+ this.app.get('/api/devices_clusters/:selectedPluginName', (req, res) => {
1062
+ const selectedPluginName = req.params.selectedPluginName;
1063
+ this.log.debug('The frontend sent /api/devices_clusters', selectedPluginName);
1064
+ if (selectedPluginName === 'none') {
1065
+ res.json([]);
1066
+ return;
1067
+ }
1068
+ const data = [];
1069
+ this.registeredDevices.forEach((registeredDevice) => {
1070
+ if (registeredDevice.plugin === selectedPluginName) {
1071
+ const clusterServers = registeredDevice.device.getAllClusterServers();
1072
+ clusterServers.forEach((clusterServer) => {
1073
+ Object.entries(clusterServer.attributes).forEach(([key, value]) => {
1074
+ if (clusterServer.name === 'EveHistory')
1075
+ return;
1076
+ //this.log.debug(`***--clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute:${key}(${value.id}) ${value.isFixed} ${value.isWritable} ${value.isWritable}`);
1077
+ let attributeValue;
1078
+ try {
1079
+ if (typeof value.getLocal() === 'object')
1080
+ attributeValue = stringify(value.getLocal());
1081
+ else
1082
+ attributeValue = value.getLocal().toString();
1083
+ }
1084
+ catch (error) {
1085
+ attributeValue = 'Unavailable';
1086
+ this.log.debug(`****${error} in clusterServer: ${clusterServer.name}(${clusterServer.id}) attribute: ${key}(${value.id})`);
1087
+ //console.log(error);
1088
+ }
1089
+ data.push({
1090
+ clusterName: clusterServer.name,
1091
+ clusterId: '0x' + clusterServer.id.toString(16).padStart(2, '0'),
1092
+ attributeName: key,
1093
+ attributeId: '0x' + value.id.toString(16).padStart(2, '0'),
1094
+ attributeValue,
1095
+ });
1096
+ });
1097
+ });
635
1098
  }
636
- data.push({ pluginName: device.plugin, type: device.device.name, endpoint: device.device.id, name: name ?? 'Unknown', cluster: 'Unknown' });
637
1099
  });
638
1100
  res.json(data);
639
1101
  });
640
1102
  // Fallback for routing
641
1103
  this.app.get('*', (req, res) => {
642
- this.log.warn('The frontend sent *');
1104
+ this.log.warn('The frontend sent *', req.url);
643
1105
  res.sendFile(path.join(this.rootDirectory, 'frontend/build/index.html'));
644
1106
  });
645
1107
  this.app.listen(port, () => {
646
- this.log.debug(`The frontend is running on ${UNDERLINE}http://localhost:${port}${rs}`);
1108
+ this.log.info(`The frontend is running on ${UNDERLINE}http://localhost:${port}${rs}`);
1109
+ });
1110
+ this.log.debug(`Frontend initialized on port ${YELLOW}${port}${db} static ${UNDERLINE}${path.join(this.rootDirectory, 'frontend/build')}${rs}`);
1111
+ }
1112
+ /**
1113
+ * Retrieves the cluster text from a given device.
1114
+ * @param device - The MatterbridgeDevice object.
1115
+ * @returns The attributes of the cluster servers in the device.
1116
+ */
1117
+ getClusterTextFromDevice(device) {
1118
+ let attributes = '';
1119
+ //this.log.debug(`getClusterTextFromDevice: ${device.name}`);
1120
+ const clusterServers = device.getAllClusterServers();
1121
+ clusterServers.forEach((clusterServer) => {
1122
+ //this.log.debug(`***--clusterServer: ${clusterServer.id} (${clusterServer.name})`);
1123
+ if (clusterServer.name === 'OnOff')
1124
+ attributes += `OnOff: ${clusterServer.getOnOffAttribute()} `;
1125
+ if (clusterServer.name === 'Switch')
1126
+ attributes += `Position: ${clusterServer.getCurrentPositionAttribute()} `;
1127
+ if (clusterServer.name === 'WindowCovering')
1128
+ attributes += `Cover position: ${clusterServer.attributes.currentPositionLiftPercent100ths.getLocal() / 100}% `;
1129
+ if (clusterServer.name === 'LevelControl')
1130
+ attributes += `Level: ${clusterServer.getCurrentLevelAttribute()}% `;
1131
+ if (clusterServer.name === 'ColorControl')
1132
+ attributes += `Hue: ${clusterServer.getCurrentHueAttribute()} Saturation: ${clusterServer.getCurrentSaturationAttribute()}% `;
1133
+ if (clusterServer.name === 'BooleanState')
1134
+ attributes += `Contact: ${clusterServer.getStateValueAttribute()} `;
1135
+ if (clusterServer.name === 'OccupancySensing')
1136
+ attributes += `Occupancy: ${clusterServer.getOccupancyAttribute().occupied} `;
1137
+ if (clusterServer.name === 'IlluminanceMeasurement')
1138
+ attributes += `Illuminance: ${clusterServer.getMeasuredValueAttribute()} `;
1139
+ if (clusterServer.name === 'AirQuality')
1140
+ attributes += `Air quality: ${clusterServer.getAirQualityAttribute()} `;
1141
+ if (clusterServer.name === 'TvocMeasurement')
1142
+ attributes += `Voc: ${clusterServer.getMeasuredValueAttribute()} `;
1143
+ if (clusterServer.name === 'TemperatureMeasurement')
1144
+ attributes += `Temperature: ${clusterServer.getMeasuredValueAttribute() / 100}°C `;
1145
+ if (clusterServer.name === 'RelativeHumidityMeasurement')
1146
+ attributes += `Humidity: ${clusterServer.getMeasuredValueAttribute() / 100}% `;
1147
+ if (clusterServer.name === 'PressureMeasurement')
1148
+ attributes += `Pressure: ${clusterServer.getMeasuredValueAttribute()} `;
647
1149
  });
1150
+ return attributes;
648
1151
  }
649
1152
  }
650
1153
  /*