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