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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +202 -21
- package/README.md +90 -5
- package/Screenshot devices page.png +0 -0
- package/Screenshot home page.png +0 -0
- package/dist/AirQualityCluster.d.ts +22 -0
- package/dist/AirQualityCluster.d.ts.map +1 -1
- package/dist/AirQualityCluster.js +23 -1
- package/dist/AirQualityCluster.js.map +1 -1
- package/dist/ColorControlServer.d.ts +20 -3
- package/dist/ColorControlServer.d.ts.map +1 -1
- package/dist/ColorControlServer.js +20 -3
- package/dist/ColorControlServer.js.map +1 -1
- package/dist/TvocCluster.d.ts +262 -0
- package/dist/TvocCluster.d.ts.map +1 -0
- package/dist/TvocCluster.js +114 -0
- package/dist/TvocCluster.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/matterbridge.d.ts +187 -17
- package/dist/matterbridge.d.ts.map +1 -1
- package/dist/matterbridge.js +721 -218
- package/dist/matterbridge.js.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.d.ts +50 -11
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.js +56 -41
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -1
- package/dist/matterbridgeComposed.d.ts +43 -0
- package/dist/matterbridgeComposed.d.ts.map +1 -0
- package/dist/matterbridgeComposed.js +58 -0
- package/dist/matterbridgeComposed.js.map +1 -0
- package/dist/matterbridgeDevice.d.ts +209 -4
- package/dist/matterbridgeDevice.d.ts.map +1 -1
- package/dist/matterbridgeDevice.js +587 -51
- package/dist/matterbridgeDevice.js.map +1 -1
- package/dist/matterbridgeDynamicPlatform.d.ts +50 -11
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -1
- package/dist/matterbridgeDynamicPlatform.js +56 -41
- package/dist/matterbridgeDynamicPlatform.js.map +1 -1
- package/frontend/build/Matterbridge.jpg +0 -0
- package/frontend/build/asset-manifest.json +6 -6
- package/frontend/build/index.html +1 -1
- package/frontend/build/static/css/main.6d93e0db.css +2 -0
- package/frontend/build/static/css/main.6d93e0db.css.map +1 -0
- package/frontend/build/static/js/main.21c55a60.js +3 -0
- package/frontend/build/static/js/{main.a000062f.js.LICENSE.txt → main.21c55a60.js.LICENSE.txt} +2 -0
- package/frontend/build/static/js/main.21c55a60.js.map +1 -0
- package/package.json +8 -4
- package/.eslintrc.json +0 -45
- package/.gitattributes +0 -2
- package/.prettierignore +0 -2
- package/.prettierrc.json +0 -12
- package/frontend/README.md +0 -70
- package/frontend/build/static/css/main.8b969fd5.css +0 -2
- package/frontend/build/static/css/main.8b969fd5.css.map +0 -1
- package/frontend/build/static/js/main.a000062f.js +0 -3
- package/frontend/build/static/js/main.a000062f.js.map +0 -1
- package/frontend/package-lock.json +0 -18351
- package/frontend/package.json +0 -40
- package/frontend/public/favicon.ico +0 -0
- package/frontend/public/index.html +0 -15
- package/frontend/public/manifest.json +0 -15
- package/frontend/public/matter.png +0 -0
- package/frontend/public/robots.txt +0 -3
package/dist/matterbridge.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
|
|
70
|
-
this.
|
|
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
|
-
|
|
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(`- ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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 ${
|
|
141
|
-
const
|
|
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 (
|
|
144
|
-
const platform =
|
|
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 ${
|
|
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.
|
|
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((
|
|
195
|
-
this.registeredPlugins.splice(this.registeredPlugins.findIndex((
|
|
196
|
-
await this.
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
225
|
-
this.
|
|
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
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
},
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
257
|
-
|
|
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.
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
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 =
|
|
355
|
-
this.log.debug('Adding
|
|
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.
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
556
|
+
// Start the interval to check if the plugins is started
|
|
381
557
|
const startedInterval = setInterval(async () => {
|
|
382
|
-
this.log.debug(
|
|
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.
|
|
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.
|
|
403
|
-
this.
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
plugin.commissioningServer
|
|
413
|
-
this.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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(
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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('
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
777
|
+
serialNumber,
|
|
492
778
|
reachable: true,
|
|
493
779
|
},
|
|
494
780
|
activeSessionsChangedCallback: (fabricIndex) => {
|
|
495
781
|
const info = commissioningServer.getActiveSessionInformation(fabricIndex);
|
|
496
|
-
this.log.debug(
|
|
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(
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
583
|
-
|
|
584
|
-
this.log.debug(`
|
|
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
|
-
|
|
609
|
-
|
|
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
|
|
620
|
-
|
|
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((
|
|
630
|
-
let name =
|
|
631
|
-
if (!name
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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.
|
|
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
|
/*
|