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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +202 -21
- package/README.md +160 -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 +188 -17
- package/dist/matterbridge.d.ts.map +1 -1
- package/dist/matterbridge.js +762 -224
- package/dist/matterbridge.js.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.d.ts +65 -11
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.js +73 -39
- 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/matterbridgeController.d.ts +2 -0
- package/dist/matterbridgeController.d.ts.map +1 -0
- package/dist/matterbridgeController.js +309 -0
- package/dist/matterbridgeController.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 +65 -11
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -1
- package/dist/matterbridgeDynamicPlatform.js +73 -39
- package/dist/matterbridgeDynamicPlatform.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +33 -0
- package/dist/utils.js.map +1 -0
- 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.b5a876cf.js +3 -0
- package/frontend/build/static/js/{main.a000062f.js.LICENSE.txt → main.b5a876cf.js.LICENSE.txt} +2 -0
- package/frontend/build/static/js/main.b5a876cf.js.map +1 -0
- package/package.json +16 -12
- 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,28 @@ 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
|
+
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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 ${
|
|
141
|
-
const
|
|
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 (
|
|
144
|
-
const platform =
|
|
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 ${
|
|
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.
|
|
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((
|
|
195
|
-
this.registeredPlugins.splice(this.registeredPlugins.findIndex((
|
|
196
|
-
await this.
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
225
|
-
this.
|
|
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
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
},
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
}
|
|
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.
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
this.log.debug(
|
|
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.
|
|
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.
|
|
360
|
-
this.
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
376
|
-
|
|
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
|
|
561
|
+
// Start the interval to check if the plugins is started
|
|
381
562
|
const startedInterval = setInterval(async () => {
|
|
382
|
-
this.log.debug(
|
|
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.
|
|
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.
|
|
403
|
-
this.
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
plugin.commissioningServer
|
|
413
|
-
this.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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(
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
787
|
+
serialNumber,
|
|
492
788
|
reachable: true,
|
|
493
789
|
},
|
|
494
790
|
activeSessionsChangedCallback: (fabricIndex) => {
|
|
495
791
|
const info = commissioningServer.getActiveSessionInformation(fabricIndex);
|
|
496
|
-
this.log.debug(
|
|
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(
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
583
|
-
|
|
584
|
-
this.log.debug(`
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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
|
|
620
|
-
|
|
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((
|
|
630
|
-
let name =
|
|
631
|
-
if (!name
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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.
|
|
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
|