matterbridge 1.3.12 → 1.3.28
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 +48 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +10 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -6
- package/dist/index.js.map +1 -1
- package/dist/matterbridge.d.ts +157 -261
- package/dist/matterbridge.d.ts.map +1 -1
- package/dist/matterbridge.js +1395 -1611
- package/dist/matterbridge.js.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.d.ts.map +1 -1
- package/dist/matterbridgeAccessoryPlatform.js +1 -0
- package/dist/matterbridgeAccessoryPlatform.js.map +1 -1
- package/dist/matterbridgeDevice.d.ts +9 -3
- package/dist/matterbridgeDevice.d.ts.map +1 -1
- package/dist/matterbridgeDevice.js +11 -4
- package/dist/matterbridgeDevice.js.map +1 -1
- package/dist/matterbridgeDynamicPlatform.d.ts.map +1 -1
- package/dist/matterbridgeDynamicPlatform.js +1 -0
- package/dist/matterbridgeDynamicPlatform.js.map +1 -1
- package/dist/matterbridgeTypes.d.ts +111 -0
- package/dist/matterbridgeTypes.d.ts.map +1 -0
- package/dist/matterbridgeTypes.js +2 -0
- package/dist/matterbridgeTypes.js.map +1 -0
- package/dist/plugins.d.ts +102 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +674 -0
- package/dist/plugins.js.map +1 -0
- package/dist/utils/export.d.ts +3 -0
- package/dist/utils/export.d.ts.map +1 -0
- package/dist/utils/export.js +3 -0
- package/dist/utils/export.js.map +1 -0
- package/dist/utils/utils.d.ts +37 -2
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +79 -7
- package/dist/utils/utils.js.map +1 -1
- package/frontend/build/asset-manifest.json +6 -6
- package/frontend/build/index.html +1 -1
- package/frontend/build/static/css/{main.b4d28450.css → main.df840158.css} +2 -2
- package/frontend/build/static/css/main.df840158.css.map +1 -0
- package/frontend/build/static/js/{main.3105733e.js → main.2a46688a.js} +3 -3
- package/frontend/build/static/js/main.2a46688a.js.map +1 -0
- package/package.json +25 -20
- package/__mocks__/@project-chip/matter-node.js/util.js +0 -41
- package/dist/utils.d.ts +0 -94
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -291
- package/dist/utils.js.map +0 -1
- package/frontend/build/static/css/main.b4d28450.css.map +0 -1
- package/frontend/build/static/js/main.3105733e.js.map +0 -1
- /package/frontend/build/static/js/{main.3105733e.js.LICENSE.txt → main.2a46688a.js.LICENSE.txt} +0 -0
package/dist/matterbridge.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* limitations under the License. *
|
|
22
22
|
*/
|
|
23
23
|
import { NodeStorageManager } from 'node-persist-manager';
|
|
24
|
-
import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr, RED, GREEN, zb, hk, or, idn, BLUE } from 'node-ansi-logger';
|
|
24
|
+
import { AnsiLogger, BRIGHT, RESET, UNDERLINE, UNDERLINEOFF, YELLOW, db, debugStringify, stringify, er, nf, rs, wr, RED, GREEN, zb, hk, or, idn, BLUE, CYAN } from 'node-ansi-logger';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
26
26
|
import { promises as fs } from 'fs';
|
|
27
27
|
import { exec, spawn } from 'child_process';
|
|
@@ -36,7 +36,7 @@ import WebSocket, { WebSocketServer } from 'ws';
|
|
|
36
36
|
import { MatterbridgeDevice } from './matterbridgeDevice.js';
|
|
37
37
|
import { shelly_config, somfytahoma_config, zigbee2mqtt_config } from './defaultConfigSchema.js';
|
|
38
38
|
import { BridgedDeviceBasicInformation, BridgedDeviceBasicInformationCluster } from './cluster/BridgedDeviceBasicInformationCluster.js';
|
|
39
|
-
import { logInterfaces } from './utils/utils.js';
|
|
39
|
+
import { logInterfaces, wait, waiter } from './utils/utils.js';
|
|
40
40
|
// @project-chip/matter-node.js
|
|
41
41
|
import { CommissioningController, CommissioningServer, MatterServer } from '@project-chip/matter-node.js';
|
|
42
42
|
import { BasicInformationCluster, ClusterServer, FixedLabelCluster, GeneralCommissioning, PowerSourceCluster, SwitchCluster, ThreadNetworkDiagnosticsCluster, getClusterNameById } from '@project-chip/matter-node.js/cluster';
|
|
@@ -47,6 +47,7 @@ import { ManualPairingCodeCodec, QrCodeSchema } from '@project-chip/matter-node.
|
|
|
47
47
|
import { StorageBackendDisk, StorageBackendJsonFile, StorageManager } from '@project-chip/matter-node.js/storage';
|
|
48
48
|
import { requireMinNodeVersion, getParameter, getIntParameter, hasParameter } from '@project-chip/matter-node.js/util';
|
|
49
49
|
import { CryptoNode } from '@project-chip/matter-node.js/crypto';
|
|
50
|
+
import { Plugins } from './plugins.js';
|
|
50
51
|
// Default colors
|
|
51
52
|
const plg = '\u001B[38;5;33m';
|
|
52
53
|
const dev = '\u001B[38;5;79m';
|
|
@@ -96,29 +97,41 @@ export class Matterbridge extends EventEmitter {
|
|
|
96
97
|
matterbridgeVersion = '';
|
|
97
98
|
matterbridgeLatestVersion = '';
|
|
98
99
|
matterbridgeFabricInformations = [];
|
|
100
|
+
matterbridgeSessionInformations = [];
|
|
99
101
|
matterbridgePaired = false;
|
|
100
102
|
matterbridgeConnected = false;
|
|
101
|
-
matterbridgeSessionInformations = [];
|
|
102
|
-
checkUpdateInterval; // = 24 * 60 * 60 * 1000; // 24 hours
|
|
103
103
|
bridgeMode = '';
|
|
104
104
|
restartMode = '';
|
|
105
105
|
debugEnabled = false;
|
|
106
|
-
|
|
107
|
-
port = 5540; // first commissioning server port
|
|
108
|
-
passcode; // first commissioning server passcode
|
|
109
|
-
discriminator; // first commissioning server discriminator
|
|
106
|
+
profile = getParameter('profile');
|
|
110
107
|
log;
|
|
111
|
-
|
|
112
|
-
// private plugins = new Map<string, RegisteredPlugin>();
|
|
113
|
-
// private devices = new Map<string, RegisteredDevice>();
|
|
114
|
-
registeredPlugins = [];
|
|
108
|
+
plugins;
|
|
115
109
|
registeredDevices = [];
|
|
116
110
|
nodeStorage;
|
|
117
111
|
nodeContext;
|
|
112
|
+
matterStorageName = 'matterbridge' + (getParameter('profile') ? '.' + getParameter('profile') : '') + '.json';
|
|
113
|
+
nodeStorageName = 'storage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
|
|
114
|
+
// Cleanup
|
|
115
|
+
hasCleanupStarted = false;
|
|
116
|
+
initialized = false;
|
|
117
|
+
execRunningCount = 0;
|
|
118
|
+
cleanupTimeout1;
|
|
119
|
+
cleanupTimeout2;
|
|
120
|
+
checkUpdateInterval;
|
|
121
|
+
configureTimeout;
|
|
122
|
+
reachabilityTimeout;
|
|
123
|
+
sigintHandler;
|
|
124
|
+
sigtermHandler;
|
|
125
|
+
// Frontend
|
|
118
126
|
expressApp;
|
|
119
127
|
httpServer;
|
|
120
128
|
httpsServer;
|
|
121
129
|
webSocketServer;
|
|
130
|
+
// Matter
|
|
131
|
+
mdnsInterface; // matter server mdnsInterface: e.g. 'eth0' or 'wlan0' or 'WiFi'
|
|
132
|
+
port = 5540; // first commissioning server port
|
|
133
|
+
passcode; // first commissioning server passcode
|
|
134
|
+
discriminator; // first commissioning server discriminator
|
|
122
135
|
storageManager;
|
|
123
136
|
matterbridgeContext;
|
|
124
137
|
mattercontrollerContext;
|
|
@@ -131,6 +144,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
131
144
|
constructor() {
|
|
132
145
|
super();
|
|
133
146
|
}
|
|
147
|
+
/** ***********************************************************************************************************************************/
|
|
148
|
+
/** loadInstance() and cleanup() methods */
|
|
149
|
+
/** ***********************************************************************************************************************************/
|
|
134
150
|
/**
|
|
135
151
|
* Loads an instance of the Matterbridge class.
|
|
136
152
|
* If an instance already exists, return that instance.
|
|
@@ -142,7 +158,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
142
158
|
if (!Matterbridge.instance) {
|
|
143
159
|
// eslint-disable-next-line no-console
|
|
144
160
|
if (hasParameter('debug'))
|
|
145
|
-
console.log(
|
|
161
|
+
console.log(GREEN + 'Creating a new instance of Matterbridge.', initialize ? 'Initializing...' : 'Not initializing...', rs);
|
|
146
162
|
Matterbridge.instance = new Matterbridge();
|
|
147
163
|
if (initialize)
|
|
148
164
|
await Matterbridge.instance.initialize();
|
|
@@ -150,111 +166,16 @@ export class Matterbridge extends EventEmitter {
|
|
|
150
166
|
return Matterbridge.instance;
|
|
151
167
|
}
|
|
152
168
|
/**
|
|
153
|
-
* Call
|
|
154
|
-
* @deprecated This method is deprecated and is only used for jest.
|
|
169
|
+
* Call cleanup().
|
|
170
|
+
* @deprecated This method is deprecated and is only used for jest tests.
|
|
155
171
|
*
|
|
156
172
|
*/
|
|
157
173
|
async destroyInstance() {
|
|
158
|
-
await this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*
|
|
164
|
-
* @returns A Promise that resolves when the initialization is complete.
|
|
165
|
-
*/
|
|
166
|
-
async startExtension(dataPath, debugEnabled, extensionVersion, port = 5560) {
|
|
167
|
-
// Set the bridge mode
|
|
168
|
-
this.bridgeMode = 'bridge';
|
|
169
|
-
// Set the first port to use
|
|
170
|
-
this.port = port;
|
|
171
|
-
// Set Matterbridge logger
|
|
172
|
-
this.debugEnabled = debugEnabled;
|
|
173
|
-
this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
|
|
174
|
-
this.log.debug('Matterbridge extension is starting...');
|
|
175
|
-
// Initialize NodeStorage
|
|
176
|
-
this.matterbridgeDirectory = dataPath;
|
|
177
|
-
this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
|
|
178
|
-
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
|
|
179
|
-
this.log.debug('Creating node storage context for matterbridge: matterbridge');
|
|
180
|
-
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
181
|
-
const plugin = {
|
|
182
|
-
path: '',
|
|
183
|
-
type: 'DynamicPlatform',
|
|
184
|
-
name: 'MatterbridgeExtension',
|
|
185
|
-
version: '1.0.0',
|
|
186
|
-
description: 'Matterbridge extension',
|
|
187
|
-
author: 'https://github.com/Luligu',
|
|
188
|
-
enabled: false,
|
|
189
|
-
registeredDevices: 0,
|
|
190
|
-
addedDevices: 0,
|
|
191
|
-
};
|
|
192
|
-
this.registeredPlugins.push(plugin);
|
|
193
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
194
|
-
// Log system info and create .matterbridge directory
|
|
195
|
-
await this.logNodeAndSystemInfo();
|
|
196
|
-
this.matterbridgeDirectory = dataPath;
|
|
197
|
-
// Set matter.js logger level and format
|
|
198
|
-
Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
|
|
199
|
-
Logger.format = Format.ANSI;
|
|
200
|
-
// Start the storage and create matterbridgeContext
|
|
201
|
-
await this.startStorage('json', path.join(this.matterbridgeDirectory, 'matterbridge.json'));
|
|
202
|
-
if (!this.storageManager)
|
|
203
|
-
return false;
|
|
204
|
-
this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
|
|
205
|
-
if (!this.matterbridgeContext)
|
|
206
|
-
return false;
|
|
207
|
-
await this.matterbridgeContext.set('softwareVersion', 1);
|
|
208
|
-
await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
|
|
209
|
-
await this.matterbridgeContext.set('hardwareVersion', 1);
|
|
210
|
-
await this.matterbridgeContext.set('hardwareVersionString', extensionVersion); // Update with the extension version
|
|
211
|
-
this.matterServer = this.createMatterServer(this.storageManager);
|
|
212
|
-
this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
|
|
213
|
-
this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
|
|
214
|
-
this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
|
|
215
|
-
this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
|
|
216
|
-
this.log.debug('Adding matterbridge aggregator to commissioning server');
|
|
217
|
-
this.commissioningServer.addDevice(this.matterAggregator);
|
|
218
|
-
this.log.debug('Adding matterbridge commissioning server to matter server');
|
|
219
|
-
await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
|
|
220
|
-
await this.startMatterServer();
|
|
221
|
-
this.log.info('Matter server started');
|
|
222
|
-
await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
|
|
223
|
-
// Set reachability to true and trigger event after 60 seconds
|
|
224
|
-
setTimeout(() => {
|
|
225
|
-
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
226
|
-
if (this.commissioningServer)
|
|
227
|
-
this.setCommissioningServerReachability(this.commissioningServer, true);
|
|
228
|
-
if (this.matterAggregator)
|
|
229
|
-
this.setAggregatorReachability(this.matterAggregator, true);
|
|
230
|
-
}, 60 * 1000);
|
|
231
|
-
return this.commissioningServer.isCommissioned();
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Close the Matterbridge instance as extension for zigbee2mqtt.
|
|
235
|
-
* @deprecated This method is deprecated and will be removed in a future version.
|
|
236
|
-
*
|
|
237
|
-
* @returns A Promise that resolves when the initialization is complete.
|
|
238
|
-
*/
|
|
239
|
-
async stopExtension() {
|
|
240
|
-
// Closing matter
|
|
241
|
-
await this.stopMatter();
|
|
242
|
-
// Clearing the session manager
|
|
243
|
-
// this.matterbridgeContext?.createContext('SessionManager').clear();
|
|
244
|
-
// Closing storage
|
|
245
|
-
await this.stopStorage();
|
|
246
|
-
this.log.info('Matter server stopped');
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Checks if the extension is commissioned.
|
|
250
|
-
* @deprecated This method is deprecated and will be removed in a future version.
|
|
251
|
-
*
|
|
252
|
-
* @returns {boolean} Returns true if the extension is commissioned, false otherwise.
|
|
253
|
-
*/
|
|
254
|
-
isExtensionCommissioned() {
|
|
255
|
-
if (!this.commissioningServer)
|
|
256
|
-
return false;
|
|
257
|
-
return this.commissioningServer.isCommissioned();
|
|
174
|
+
await this.cleanup('destroying instance...', false);
|
|
175
|
+
await waiter('destroying instance...', () => {
|
|
176
|
+
return this.initialized === false && this.execRunningCount <= 0 ? true : false;
|
|
177
|
+
}, false, 60000, 100, false);
|
|
178
|
+
await wait(1000, 'Wait for the global node_modules and matterbridge version', false);
|
|
258
179
|
}
|
|
259
180
|
/**
|
|
260
181
|
* Initializes the Matterbridge application.
|
|
@@ -267,37 +188,6 @@ export class Matterbridge extends EventEmitter {
|
|
|
267
188
|
* @returns A Promise that resolves when the initialization is complete.
|
|
268
189
|
*/
|
|
269
190
|
async initialize() {
|
|
270
|
-
// Display the help
|
|
271
|
-
if (hasParameter('help')) {
|
|
272
|
-
// eslint-disable-next-line no-console
|
|
273
|
-
console.log(`\nUsage: matterbridge [options]\n
|
|
274
|
-
Options:
|
|
275
|
-
- help: show the help
|
|
276
|
-
- bridge: start Matterbridge in bridge mode
|
|
277
|
-
- childbridge: start Matterbridge in childbridge mode
|
|
278
|
-
- port [port]: start the commissioning server on the given port (default 5540)
|
|
279
|
-
- mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
|
|
280
|
-
- frontend [port]: start the frontend on the given port (default 8283)
|
|
281
|
-
- debug: enable the Matterbridge debug mode (default false)
|
|
282
|
-
- matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
|
|
283
|
-
- reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
|
|
284
|
-
- factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
|
|
285
|
-
- list: list the registered plugins
|
|
286
|
-
- loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option)
|
|
287
|
-
- logstorage: log the node storage
|
|
288
|
-
- ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
|
|
289
|
-
- add [plugin path]: register the plugin from the given absolute or relative path
|
|
290
|
-
- add [plugin name]: register the globally installed plugin with the given name
|
|
291
|
-
- remove [plugin path]: remove the plugin from the given absolute or relative path
|
|
292
|
-
- remove [plugin name]: remove the globally installed plugin with the given name
|
|
293
|
-
- enable [plugin path]: enable the plugin from the given absolute or relative path
|
|
294
|
-
- enable [plugin name]: enable the globally installed plugin with the given name
|
|
295
|
-
- disable [plugin path]: disable the plugin from the given absolute or relative path
|
|
296
|
-
- disable [plugin name]: disable the globally installed plugin with the given name
|
|
297
|
-
- reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it!
|
|
298
|
-
- reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!\n`);
|
|
299
|
-
process.exit(0);
|
|
300
|
-
}
|
|
301
191
|
// Set the interface to use for the matter server mdnsInterface
|
|
302
192
|
this.mdnsInterface = getParameter('mdnsinterface');
|
|
303
193
|
// Set the first port to use for the commissioning server (will be incremented in childbridge mode)
|
|
@@ -316,25 +206,51 @@ export class Matterbridge extends EventEmitter {
|
|
|
316
206
|
this.debugEnabled = true;
|
|
317
207
|
this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
|
|
318
208
|
this.log.debug('Matterbridge is starting...');
|
|
319
|
-
//
|
|
209
|
+
// Set matter.js logger level and format
|
|
210
|
+
if (hasParameter('matterlogger')) {
|
|
211
|
+
const level = getParameter('matterlogger');
|
|
212
|
+
if (level === 'debug') {
|
|
213
|
+
Logger.defaultLogLevel = Level.DEBUG;
|
|
214
|
+
}
|
|
215
|
+
else if (level === 'info') {
|
|
216
|
+
Logger.defaultLogLevel = Level.INFO;
|
|
217
|
+
}
|
|
218
|
+
else if (level === 'notice') {
|
|
219
|
+
Logger.defaultLogLevel = Level.NOTICE;
|
|
220
|
+
}
|
|
221
|
+
else if (level === 'warn') {
|
|
222
|
+
Logger.defaultLogLevel = Level.WARN;
|
|
223
|
+
}
|
|
224
|
+
else if (level === 'error') {
|
|
225
|
+
Logger.defaultLogLevel = Level.ERROR;
|
|
226
|
+
}
|
|
227
|
+
else if (level === 'fatal') {
|
|
228
|
+
Logger.defaultLogLevel = Level.FATAL;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.log.warn(`Invalid matterlogger level: ${level}. Using default level ${this.debugEnabled ? 'debug' : 'info'}.`);
|
|
232
|
+
Logger.defaultLogLevel = Level.INFO;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
Logger.defaultLogLevel = Level.INFO;
|
|
237
|
+
}
|
|
238
|
+
Logger.format = Format.ANSI;
|
|
239
|
+
// Initialize nodeStorage and nodeContext
|
|
320
240
|
this.homeDirectory = os.homedir();
|
|
321
241
|
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
322
|
-
this.log.debug(
|
|
323
|
-
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory,
|
|
242
|
+
this.log.debug(`Creating node storage manager: ${CYAN}${this.nodeStorageName}${db}`);
|
|
243
|
+
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, this.nodeStorageName), writeQueue: false, expiredInterval: undefined, logging: false });
|
|
324
244
|
this.log.debug('Creating node storage context for matterbridge');
|
|
325
245
|
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
326
|
-
//
|
|
327
|
-
this.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
plugin
|
|
334
|
-
plugin.description = packageJson.description;
|
|
335
|
-
plugin.author = packageJson.author;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
246
|
+
// Initialize Plugins
|
|
247
|
+
this.plugins = new Plugins(this);
|
|
248
|
+
await this.plugins.loadFromStorage();
|
|
249
|
+
// Get the plugins from node storage and create the plugins node storage contexts
|
|
250
|
+
for (const plugin of this.plugins) {
|
|
251
|
+
const packageJson = await this.plugins.parse(plugin);
|
|
252
|
+
if (packageJson === null) {
|
|
253
|
+
// Try to reinstall the plugin from npm (for Docker pull and external plugins)
|
|
338
254
|
this.log.info(`Error parsing plugin ${plg}${plugin.name}${nf}. Trying to reinstall it from npm.`);
|
|
339
255
|
try {
|
|
340
256
|
await this.spawnCommand('npm', ['install', '-g', plugin.name]);
|
|
@@ -347,7 +263,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
347
263
|
this.log.error(`Error installing plugin ${plg}${plugin.name}${er}. The plugin is disabled.`);
|
|
348
264
|
}
|
|
349
265
|
}
|
|
350
|
-
this.log.debug(`Creating node storage context for plugin
|
|
266
|
+
this.log.debug(`Creating node storage context for plugin ${plg}${plugin.name}${db}`);
|
|
351
267
|
plugin.nodeContext = await this.nodeStorage.createStorage(plugin.name);
|
|
352
268
|
await plugin.nodeContext.set('name', plugin.name);
|
|
353
269
|
await plugin.nodeContext.set('type', plugin.type);
|
|
@@ -364,38 +280,9 @@ export class Matterbridge extends EventEmitter {
|
|
|
364
280
|
requireMinNodeVersion(18);
|
|
365
281
|
// Register SIGINT SIGTERM signal handlers
|
|
366
282
|
this.registerSignalHandlers();
|
|
367
|
-
// Set matter.js logger level and format
|
|
368
|
-
if (hasParameter('matterlogger')) {
|
|
369
|
-
const level = getParameter('matterlogger');
|
|
370
|
-
if (level === 'debug') {
|
|
371
|
-
Logger.defaultLogLevel = Level.DEBUG;
|
|
372
|
-
}
|
|
373
|
-
else if (level === 'info') {
|
|
374
|
-
Logger.defaultLogLevel = Level.INFO;
|
|
375
|
-
}
|
|
376
|
-
else if (level === 'notice') {
|
|
377
|
-
Logger.defaultLogLevel = Level.NOTICE;
|
|
378
|
-
}
|
|
379
|
-
else if (level === 'warn') {
|
|
380
|
-
Logger.defaultLogLevel = Level.WARN;
|
|
381
|
-
}
|
|
382
|
-
else if (level === 'error') {
|
|
383
|
-
Logger.defaultLogLevel = Level.ERROR;
|
|
384
|
-
}
|
|
385
|
-
else if (level === 'fatal') {
|
|
386
|
-
Logger.defaultLogLevel = Level.FATAL;
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
this.log.warn(`Invalid matterlogger level: ${level}. Using default level ${this.debugEnabled ? 'debug' : 'info'}.`);
|
|
390
|
-
Logger.defaultLogLevel = Level.INFO;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
else {
|
|
394
|
-
Logger.defaultLogLevel = Level.INFO;
|
|
395
|
-
}
|
|
396
|
-
Logger.format = Format.ANSI;
|
|
397
283
|
// Parse command line
|
|
398
284
|
await this.parseCommandLine();
|
|
285
|
+
this.initialized = true;
|
|
399
286
|
}
|
|
400
287
|
/**
|
|
401
288
|
* Parses the command line arguments and performs the corresponding actions.
|
|
@@ -403,10 +290,41 @@ export class Matterbridge extends EventEmitter {
|
|
|
403
290
|
* @returns {Promise<void>} A promise that resolves when the command line arguments have been processed, or the process exits.
|
|
404
291
|
*/
|
|
405
292
|
async parseCommandLine() {
|
|
293
|
+
if (hasParameter('help')) {
|
|
294
|
+
this.log.info(`\nUsage: matterbridge [options]\n
|
|
295
|
+
Options:
|
|
296
|
+
- help: show the help
|
|
297
|
+
- bridge: start Matterbridge in bridge mode
|
|
298
|
+
- childbridge: start Matterbridge in childbridge mode
|
|
299
|
+
- port [port]: start the commissioning server on the given port (default 5540)
|
|
300
|
+
- mdnsinterface [name]: set the interface to use for the matter server mdnsInterface (default all interfaces)
|
|
301
|
+
- frontend [port]: start the frontend on the given port (default 8283)
|
|
302
|
+
- debug: enable the Matterbridge debug mode (default false)
|
|
303
|
+
- matterlogger: set the matter.js logger level: debug | info | notice | warn | error | fatal (default info)
|
|
304
|
+
- reset: remove the commissioning for Matterbridge (bridge mode). Shutdown Matterbridge before using it!
|
|
305
|
+
- factoryreset: remove all commissioning information and reset all internal storages. Shutdown Matterbridge before using it!
|
|
306
|
+
- list: list the registered plugins
|
|
307
|
+
- loginterfaces: log the network interfaces (usefull for finding the name of the interface to use with -mdnsinterface option)
|
|
308
|
+
- logstorage: log the node storage
|
|
309
|
+
- ssl: enable SSL for the frontend and WebSockerServer (certificates in .matterbridge/certs directory cert.pem, key.pem and ca.pem (optional))
|
|
310
|
+
- add [plugin path]: register the plugin from the given absolute or relative path
|
|
311
|
+
- add [plugin name]: register the globally installed plugin with the given name
|
|
312
|
+
- remove [plugin path]: remove the plugin from the given absolute or relative path
|
|
313
|
+
- remove [plugin name]: remove the globally installed plugin with the given name
|
|
314
|
+
- enable [plugin path]: enable the plugin from the given absolute or relative path
|
|
315
|
+
- enable [plugin name]: enable the globally installed plugin with the given name
|
|
316
|
+
- disable [plugin path]: disable the plugin from the given absolute or relative path
|
|
317
|
+
- disable [plugin name]: disable the globally installed plugin with the given name
|
|
318
|
+
- reset [plugin path]: remove the commissioning for the plugin from the given absolute or relative path (childbridge mode). Shutdown Matterbridge before using it!
|
|
319
|
+
- reset [plugin name]: remove the commissioning for the globally installed plugin (childbridge mode). Shutdown Matterbridge before using it!${rs}`);
|
|
320
|
+
this.emit('shutdown');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
406
323
|
if (hasParameter('list')) {
|
|
407
|
-
this.log.info(`│ Registered plugins (${this.
|
|
408
|
-
|
|
409
|
-
|
|
324
|
+
this.log.info(`│ Registered plugins (${this.plugins.length})`);
|
|
325
|
+
let index = 0;
|
|
326
|
+
for (const plugin of this.plugins) {
|
|
327
|
+
if (index !== this.plugins.length - 1) {
|
|
410
328
|
this.log.info(`├─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
|
|
411
329
|
this.log.info(`│ └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
|
|
412
330
|
}
|
|
@@ -414,7 +332,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
414
332
|
this.log.info(`└─┬─ plugin ${plg}${plugin.name}${nf}: "${plg}${BRIGHT}${plugin.description}${RESET}${nf}" type: ${typ}${plugin.type}${nf} ${plugin.enabled ? GREEN : RED}enabled ${plugin.paired ? GREEN : RED}paired${nf}`);
|
|
415
333
|
this.log.info(` └─ entry ${UNDERLINE}${db}${plugin.path}${UNDERLINEOFF}${db}`);
|
|
416
334
|
}
|
|
417
|
-
|
|
335
|
+
index++;
|
|
336
|
+
}
|
|
418
337
|
const serializedRegisteredDevices = await this.nodeContext?.get('devices', []);
|
|
419
338
|
this.log.info(`│ Registered devices (${serializedRegisteredDevices?.length})`);
|
|
420
339
|
serializedRegisteredDevices?.forEach((device, index) => {
|
|
@@ -428,69 +347,78 @@ export class Matterbridge extends EventEmitter {
|
|
|
428
347
|
}
|
|
429
348
|
});
|
|
430
349
|
this.emit('shutdown');
|
|
431
|
-
|
|
350
|
+
return;
|
|
432
351
|
}
|
|
433
352
|
if (hasParameter('logstorage')) {
|
|
434
353
|
this.log.info(`${plg}Matterbridge${nf} storage log`);
|
|
435
354
|
await this.nodeContext?.logStorage();
|
|
436
|
-
for (const plugin of this.
|
|
355
|
+
for (const plugin of this.plugins) {
|
|
437
356
|
this.log.info(`${plg}${plugin.name}${nf} storage log`);
|
|
438
357
|
await plugin.nodeContext?.logStorage();
|
|
439
358
|
}
|
|
440
359
|
this.emit('shutdown');
|
|
441
|
-
|
|
360
|
+
return;
|
|
442
361
|
}
|
|
443
362
|
if (hasParameter('loginterfaces')) {
|
|
444
363
|
this.log.info(`${plg}Matterbridge${nf} network interfaces log`);
|
|
445
364
|
logInterfaces();
|
|
446
365
|
this.emit('shutdown');
|
|
447
|
-
|
|
366
|
+
return;
|
|
448
367
|
}
|
|
449
368
|
if (getParameter('add')) {
|
|
450
|
-
this.log.debug(`
|
|
451
|
-
await this.executeCommandLine(getParameter('add'), 'add');
|
|
369
|
+
this.log.debug(`Adding plugin ${getParameter('add')}`);
|
|
370
|
+
// await this.executeCommandLine(getParameter('add') as string, 'add');
|
|
371
|
+
await this.plugins.add(getParameter('add'));
|
|
452
372
|
this.emit('shutdown');
|
|
453
|
-
|
|
373
|
+
return;
|
|
454
374
|
}
|
|
455
375
|
if (getParameter('remove')) {
|
|
456
|
-
this.log.debug(`
|
|
457
|
-
await this.executeCommandLine(getParameter('remove'), 'remove');
|
|
376
|
+
this.log.debug(`Removing plugin ${getParameter('remove')}`);
|
|
377
|
+
// await this.executeCommandLine(getParameter('remove') as string, 'remove');
|
|
378
|
+
await this.plugins.remove(getParameter('remove'));
|
|
458
379
|
this.emit('shutdown');
|
|
459
|
-
|
|
380
|
+
return;
|
|
460
381
|
}
|
|
461
382
|
if (getParameter('enable')) {
|
|
462
|
-
this.log.debug(`
|
|
463
|
-
await this.executeCommandLine(getParameter('enable'), 'enable');
|
|
383
|
+
this.log.debug(`Enabling plugin ${getParameter('enable')}`);
|
|
384
|
+
// await this.executeCommandLine(getParameter('enable') as string, 'enable');
|
|
385
|
+
await this.plugins.enable(getParameter('enable'));
|
|
464
386
|
this.emit('shutdown');
|
|
465
|
-
|
|
387
|
+
return;
|
|
466
388
|
}
|
|
467
389
|
if (getParameter('disable')) {
|
|
468
|
-
this.log.debug(`
|
|
469
|
-
await this.executeCommandLine(getParameter('disable'), 'disable');
|
|
390
|
+
this.log.debug(`Disabling plugin ${getParameter('disable')}`);
|
|
391
|
+
// await this.executeCommandLine(getParameter('disable') as string, 'disable');
|
|
392
|
+
await this.plugins.disable(getParameter('disable'));
|
|
470
393
|
this.emit('shutdown');
|
|
471
|
-
|
|
394
|
+
return;
|
|
472
395
|
}
|
|
473
396
|
if (hasParameter('factoryreset')) {
|
|
474
397
|
try {
|
|
475
398
|
// Delete matter storage file
|
|
476
|
-
await fs.unlink(path.join(this.matterbridgeDirectory,
|
|
399
|
+
await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
477
400
|
}
|
|
478
401
|
catch (err) {
|
|
479
402
|
this.log.error(`Error deleting storage: ${err}`);
|
|
480
403
|
}
|
|
481
404
|
try {
|
|
482
405
|
// Delete node storage directory with its subdirectories
|
|
483
|
-
await fs.rm(path.join(this.matterbridgeDirectory,
|
|
406
|
+
await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
|
|
484
407
|
}
|
|
485
408
|
catch (err) {
|
|
486
409
|
this.log.error(`Error removing storage directory: ${err}`);
|
|
487
410
|
}
|
|
488
411
|
this.log.info('Factory reset done! Remove all paired devices from the controllers.');
|
|
412
|
+
this.nodeContext = undefined;
|
|
413
|
+
this.nodeStorage = undefined;
|
|
414
|
+
this.plugins.clear();
|
|
415
|
+
this.registeredDevices = [];
|
|
489
416
|
this.emit('shutdown');
|
|
490
|
-
|
|
417
|
+
return;
|
|
491
418
|
}
|
|
492
|
-
// Start the storage and create
|
|
493
|
-
await this.startStorage('json', path.join(this.matterbridgeDirectory,
|
|
419
|
+
// Start the matter storage and create the matterbridge context
|
|
420
|
+
await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
421
|
+
this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
|
|
494
422
|
this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge aggregator');
|
|
495
423
|
if (hasParameter('reset') && getParameter('reset') === undefined) {
|
|
496
424
|
this.log.info('Resetting Matterbridge commissioning information...');
|
|
@@ -498,14 +426,26 @@ export class Matterbridge extends EventEmitter {
|
|
|
498
426
|
await this.stopStorage();
|
|
499
427
|
this.log.info('Reset done! Remove the device from the controller.');
|
|
500
428
|
this.emit('shutdown');
|
|
501
|
-
|
|
429
|
+
return;
|
|
502
430
|
}
|
|
503
431
|
if (getParameter('reset') && getParameter('reset') !== undefined) {
|
|
504
432
|
this.log.debug(`Reset plugin ${getParameter('reset')}`);
|
|
505
|
-
|
|
433
|
+
const plugin = this.plugins.get(getParameter('reset'));
|
|
434
|
+
if (plugin) {
|
|
435
|
+
if (!this.storageManager)
|
|
436
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} storageManager not found`);
|
|
437
|
+
const context = this.storageManager?.createContext(plugin.name);
|
|
438
|
+
if (!context)
|
|
439
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
|
|
440
|
+
await context?.clearAll();
|
|
441
|
+
this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.log.warn(`Plugin ${plg}${getParameter('reset')}${wr} not registerd in matterbridge`);
|
|
445
|
+
}
|
|
506
446
|
await this.stopStorage();
|
|
507
447
|
this.emit('shutdown');
|
|
508
|
-
|
|
448
|
+
return;
|
|
509
449
|
}
|
|
510
450
|
// Initialize frontend
|
|
511
451
|
if (getIntParameter('frontend') !== 0 || getIntParameter('frontend') === undefined)
|
|
@@ -513,41 +453,28 @@ export class Matterbridge extends EventEmitter {
|
|
|
513
453
|
// Check each 60 minutes the latest versions
|
|
514
454
|
this.checkUpdateInterval = setInterval(() => {
|
|
515
455
|
this.getMatterbridgeLatestVersion();
|
|
516
|
-
this.
|
|
456
|
+
for (const plugin of this.plugins) {
|
|
517
457
|
this.getPluginLatestVersion(plugin);
|
|
518
|
-
}
|
|
458
|
+
}
|
|
519
459
|
}, 60 * 60 * 1000);
|
|
520
460
|
if (hasParameter('test')) {
|
|
521
461
|
this.bridgeMode = 'childbridge';
|
|
522
462
|
MatterbridgeDevice.bridgeMode = 'childbridge';
|
|
523
|
-
await this.
|
|
463
|
+
await this.startTest();
|
|
524
464
|
return;
|
|
525
465
|
}
|
|
526
466
|
if (hasParameter('controller')) {
|
|
527
467
|
this.bridgeMode = 'controller';
|
|
528
|
-
await this.
|
|
468
|
+
await this.startController();
|
|
529
469
|
return;
|
|
530
470
|
}
|
|
531
471
|
if (hasParameter('bridge')) {
|
|
532
472
|
this.bridgeMode = 'bridge';
|
|
533
473
|
MatterbridgeDevice.bridgeMode = 'bridge';
|
|
534
|
-
if (!this.storageManager)
|
|
535
|
-
|
|
536
|
-
await this.cleanup('No storage manager initialized');
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
474
|
+
if (!this.storageManager)
|
|
475
|
+
throw new Error('No storage manager initialized');
|
|
539
476
|
this.log.debug('Starting matterbridge in mode', this.bridgeMode);
|
|
540
477
|
this.matterServer = this.createMatterServer(this.storageManager);
|
|
541
|
-
this.log.debug(`Creating commissioning server context for ${plg}Matterbridge${db}`);
|
|
542
|
-
this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Aggregator');
|
|
543
|
-
if (!this.matterbridgeContext) {
|
|
544
|
-
this.log.error(`Error creating storage context for ${plg}Matterbridge${er}`);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (!this.nodeContext) {
|
|
548
|
-
this.log.error(`Node storage context undefined for ${plg}Matterbridge${er}`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
478
|
this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
|
|
552
479
|
this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
|
|
553
480
|
this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
|
|
@@ -556,18 +483,18 @@ export class Matterbridge extends EventEmitter {
|
|
|
556
483
|
this.commissioningServer.addDevice(this.matterAggregator);
|
|
557
484
|
this.log.debug('Adding matterbridge commissioning server to matter server');
|
|
558
485
|
await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
|
|
559
|
-
for (const plugin of this.
|
|
486
|
+
for (const plugin of this.plugins) {
|
|
560
487
|
plugin.configJson = await this.loadPluginConfig(plugin);
|
|
561
488
|
plugin.schemaJson = await this.loadPluginSchema(plugin);
|
|
562
489
|
// Check if the plugin is available
|
|
563
|
-
if (!(await this.
|
|
490
|
+
if (!(await this.plugins.resolve(plugin.path))) {
|
|
564
491
|
this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
|
|
565
492
|
plugin.enabled = false;
|
|
566
493
|
plugin.error = true;
|
|
567
494
|
continue;
|
|
568
495
|
}
|
|
569
496
|
// Check if the plugin has a new version
|
|
570
|
-
this.getPluginLatestVersion(plugin);
|
|
497
|
+
this.getPluginLatestVersion(plugin); // No await do it asyncronously
|
|
571
498
|
if (!plugin.enabled) {
|
|
572
499
|
this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
|
|
573
500
|
continue;
|
|
@@ -584,31 +511,28 @@ export class Matterbridge extends EventEmitter {
|
|
|
584
511
|
plugin.manualPairingCode = undefined;
|
|
585
512
|
this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
|
|
586
513
|
}
|
|
587
|
-
await this.
|
|
514
|
+
await this.startBridge();
|
|
588
515
|
return;
|
|
589
516
|
}
|
|
590
517
|
if (hasParameter('childbridge')) {
|
|
591
518
|
this.bridgeMode = 'childbridge';
|
|
592
519
|
MatterbridgeDevice.bridgeMode = 'childbridge';
|
|
593
|
-
if (!this.storageManager)
|
|
594
|
-
|
|
595
|
-
await this.cleanup('No storage manager initialized');
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
520
|
+
if (!this.storageManager)
|
|
521
|
+
throw new Error('No storage manager initialized');
|
|
598
522
|
this.log.debug('Starting matterbridge in mode', this.bridgeMode);
|
|
599
523
|
this.matterServer = this.createMatterServer(this.storageManager);
|
|
600
|
-
for (const plugin of this.
|
|
524
|
+
for (const plugin of this.plugins) {
|
|
601
525
|
plugin.configJson = await this.loadPluginConfig(plugin);
|
|
602
526
|
plugin.schemaJson = await this.loadPluginSchema(plugin);
|
|
603
527
|
// Check if the plugin is available
|
|
604
|
-
if (!(await this.
|
|
528
|
+
if (!(await this.plugins.resolve(plugin.path))) {
|
|
605
529
|
this.log.error(`Plugin ${plg}${plugin.name}${er} not found. Disabling it.`);
|
|
606
530
|
plugin.enabled = false;
|
|
607
531
|
plugin.error = true;
|
|
608
532
|
continue;
|
|
609
533
|
}
|
|
610
534
|
// Check if the plugin has a new version
|
|
611
|
-
this.getPluginLatestVersion(plugin);
|
|
535
|
+
this.getPluginLatestVersion(plugin); // No await do it asyncronously
|
|
612
536
|
if (!plugin.enabled) {
|
|
613
537
|
this.log.info(`Plugin ${plg}${plugin.name}${nf} not enabled`);
|
|
614
538
|
continue;
|
|
@@ -625,182 +549,283 @@ export class Matterbridge extends EventEmitter {
|
|
|
625
549
|
plugin.manualPairingCode = (await plugin.nodeContext?.get('manualPairingCode', undefined)) ?? undefined;
|
|
626
550
|
this.loadPlugin(plugin, true, 'Matterbridge is starting'); // No await do it asyncronously
|
|
627
551
|
}
|
|
628
|
-
await this.
|
|
552
|
+
await this.startChildbridge();
|
|
629
553
|
return;
|
|
630
554
|
}
|
|
631
555
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Registers the signal handlers for SIGINT and SIGTERM.
|
|
558
|
+
* When either of these signals are received, the cleanup method is called with an appropriate message.
|
|
559
|
+
*/
|
|
560
|
+
registerSignalHandlers() {
|
|
561
|
+
this.log.debug(`Registering SIGINT and SIGTERM signal handlers...`);
|
|
562
|
+
this.sigintHandler = async () => {
|
|
563
|
+
await this.cleanup('SIGINT received, cleaning up...');
|
|
564
|
+
};
|
|
565
|
+
process.on('SIGINT', this.sigintHandler);
|
|
566
|
+
this.sigtermHandler = async () => {
|
|
567
|
+
await this.cleanup('SIGTERM received, cleaning up...');
|
|
568
|
+
};
|
|
569
|
+
process.on('SIGTERM', this.sigtermHandler);
|
|
642
570
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
this.registeredPlugins = await this.nodeContext.get('plugins', []);
|
|
571
|
+
/**
|
|
572
|
+
* Deregisters the SIGINT and SIGTERM signal handlers.
|
|
573
|
+
*/
|
|
574
|
+
deregisterSignalHandlers() {
|
|
575
|
+
this.log.debug(`Deregistering SIGINT and SIGTERM signal handlers...`);
|
|
576
|
+
this.sigintHandler && process.off('SIGINT', this.sigintHandler);
|
|
577
|
+
this.sigintHandler = undefined;
|
|
578
|
+
this.sigtermHandler && process.off('SIGTERM', this.sigtermHandler);
|
|
579
|
+
this.sigtermHandler = undefined;
|
|
653
580
|
}
|
|
654
581
|
/**
|
|
655
|
-
*
|
|
656
|
-
* @param pluginPath - The path to the plugin or the path to the plugin's package.json file.
|
|
657
|
-
* @returns The path to the resolved package.json file, or null if the package.json file is not found or does not contain a name.
|
|
582
|
+
* Logs the node and system information.
|
|
658
583
|
*/
|
|
659
|
-
async
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
584
|
+
async logNodeAndSystemInfo() {
|
|
585
|
+
// IP address information
|
|
586
|
+
const networkInterfaces = os.networkInterfaces();
|
|
587
|
+
this.systemInformation.ipv4Address = '';
|
|
588
|
+
this.systemInformation.ipv6Address = '';
|
|
589
|
+
for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
|
|
590
|
+
if (!interfaceDetails) {
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
for (const detail of interfaceDetails) {
|
|
594
|
+
if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
|
|
595
|
+
this.systemInformation.interfaceName = interfaceName;
|
|
596
|
+
this.systemInformation.ipv4Address = detail.address;
|
|
597
|
+
this.systemInformation.macAddress = detail.mac;
|
|
598
|
+
}
|
|
599
|
+
else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
|
|
600
|
+
this.systemInformation.interfaceName = interfaceName;
|
|
601
|
+
this.systemInformation.ipv6Address = detail.address;
|
|
602
|
+
this.systemInformation.macAddress = detail.mac;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (this.systemInformation.ipv4Address !== '' /* && this.systemInformation.ipv6Address !== ''*/) {
|
|
606
|
+
this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
|
|
607
|
+
this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
|
|
608
|
+
this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
|
|
609
|
+
this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
670
612
|
}
|
|
671
|
-
|
|
672
|
-
|
|
613
|
+
// Node information
|
|
614
|
+
this.systemInformation.nodeVersion = process.versions.node;
|
|
615
|
+
const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
|
|
616
|
+
const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
|
|
617
|
+
const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
|
|
618
|
+
// Host system information
|
|
619
|
+
this.systemInformation.hostname = os.hostname();
|
|
620
|
+
this.systemInformation.user = os.userInfo().username;
|
|
621
|
+
this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
|
|
622
|
+
this.systemInformation.osRelease = os.release(); // Kernel version
|
|
623
|
+
this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
|
|
624
|
+
this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
|
|
625
|
+
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
626
|
+
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
627
|
+
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
|
|
628
|
+
// Log the system information
|
|
629
|
+
this.log.debug('Host System Information:');
|
|
630
|
+
this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
|
|
631
|
+
this.log.debug(`- User: ${this.systemInformation.user}`);
|
|
632
|
+
this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
|
|
633
|
+
this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
|
|
634
|
+
this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
|
|
635
|
+
this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
|
|
636
|
+
this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
|
|
637
|
+
this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
|
|
638
|
+
this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
|
|
639
|
+
this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
|
|
640
|
+
this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
|
|
641
|
+
this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
|
|
642
|
+
this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
|
|
643
|
+
this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
|
|
644
|
+
// Home directory
|
|
645
|
+
this.homeDirectory = os.homedir();
|
|
646
|
+
this.matterbridgeInformation.homeDirectory = this.homeDirectory;
|
|
647
|
+
this.log.debug(`Home Directory: ${this.homeDirectory}`);
|
|
648
|
+
// Package root directory
|
|
649
|
+
const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
650
|
+
this.rootDirectory = path.resolve(currentFileDirectory, '../');
|
|
651
|
+
this.matterbridgeInformation.rootDirectory = this.rootDirectory;
|
|
652
|
+
this.log.debug(`Root Directory: ${this.rootDirectory}`);
|
|
653
|
+
// Global node_modules directory
|
|
654
|
+
if (this.nodeContext)
|
|
655
|
+
this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
656
|
+
// First run of Matterbridge so the node storage is empty
|
|
657
|
+
if (this.globalModulesDirectory === '') {
|
|
658
|
+
try {
|
|
659
|
+
this.globalModulesDirectory = await this.getGlobalNodeModules();
|
|
660
|
+
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
661
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
662
|
+
await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
666
|
+
}
|
|
673
667
|
}
|
|
674
|
-
|
|
675
|
-
this.
|
|
676
|
-
|
|
677
|
-
|
|
668
|
+
else {
|
|
669
|
+
this.getGlobalNodeModules()
|
|
670
|
+
.then(async (globalModulesDirectory) => {
|
|
671
|
+
this.globalModulesDirectory = globalModulesDirectory;
|
|
672
|
+
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
673
|
+
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
674
|
+
await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
|
|
675
|
+
})
|
|
676
|
+
.catch((error) => {
|
|
677
|
+
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
678
|
+
});
|
|
678
679
|
}
|
|
680
|
+
// Create the data directory .matterbridge in the home directory
|
|
681
|
+
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
682
|
+
this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
|
|
679
683
|
try {
|
|
680
|
-
|
|
681
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
682
|
-
if (!packageJson.name) {
|
|
683
|
-
this.log.debug(`Package.json name not found at ${packageJsonPath}`);
|
|
684
|
-
return null;
|
|
685
|
-
}
|
|
686
|
-
this.log.debug(`Package.json name: ${plg}${packageJson.name}${db} description: "${nf}${packageJson.description}${db}" found at "${nf}${packageJsonPath}${db}"`);
|
|
687
|
-
return packageJsonPath;
|
|
684
|
+
await fs.access(this.matterbridgeDirectory);
|
|
688
685
|
}
|
|
689
686
|
catch (err) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
* @param packageJsonPath - The path to the package.json file of the plugin.
|
|
697
|
-
* @param mode - The mode of operation. Possible values are 'add', 'remove', 'enable', 'disable'.
|
|
698
|
-
* @returns A Promise that resolves when the plugin is loaded successfully, or rejects with an error if loading fails.
|
|
699
|
-
*/
|
|
700
|
-
async executeCommandLine(packageJsonPath, mode) {
|
|
701
|
-
packageJsonPath = (await this.resolvePluginName(packageJsonPath)) ?? packageJsonPath;
|
|
702
|
-
this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
|
|
703
|
-
try {
|
|
704
|
-
// Load the package.json of the plugin
|
|
705
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
706
|
-
if (mode === 'add') {
|
|
707
|
-
if (!this.registeredPlugins.find((plugin) => plugin.name === packageJson.name)) {
|
|
708
|
-
const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
|
|
709
|
-
if (await this.loadPlugin(plugin)) {
|
|
710
|
-
this.registeredPlugins.push(plugin);
|
|
711
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
712
|
-
this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge`);
|
|
687
|
+
if (err instanceof Error) {
|
|
688
|
+
const nodeErr = err;
|
|
689
|
+
if (nodeErr.code === 'ENOENT') {
|
|
690
|
+
try {
|
|
691
|
+
await fs.mkdir(this.matterbridgeDirectory, { recursive: true });
|
|
692
|
+
this.log.info(`Created Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
713
693
|
}
|
|
714
|
-
|
|
715
|
-
this.log.error(`
|
|
694
|
+
catch (err) {
|
|
695
|
+
this.log.error(`Error creating directory: ${err}`);
|
|
716
696
|
}
|
|
717
697
|
}
|
|
718
698
|
else {
|
|
719
|
-
this.log.
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else if (mode === 'remove') {
|
|
723
|
-
if (this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === packageJson.name)) {
|
|
724
|
-
this.registeredPlugins.splice(this.registeredPlugins.findIndex((registeredPlugin) => registeredPlugin.name === packageJson.name), 1);
|
|
725
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
726
|
-
this.log.info(`Plugin ${plg}${packageJsonPath}${nf} removed from matterbridge`);
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
this.log.warn(`Plugin ${plg}${packageJsonPath}${wr} not registerd in matterbridge`);
|
|
699
|
+
this.log.error(`Error accessing directory: ${err}`);
|
|
730
700
|
}
|
|
731
701
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
702
|
+
}
|
|
703
|
+
this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
704
|
+
// Create the plugin directory Matterbridge in the home directory
|
|
705
|
+
this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
|
|
706
|
+
this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
|
|
707
|
+
try {
|
|
708
|
+
await fs.access(this.matterbridgePluginDirectory);
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
if (err instanceof Error) {
|
|
712
|
+
const nodeErr = err;
|
|
713
|
+
if (nodeErr.code === 'ENOENT') {
|
|
714
|
+
try {
|
|
715
|
+
await fs.mkdir(this.matterbridgePluginDirectory, { recursive: true });
|
|
716
|
+
this.log.info(`Created Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
this.log.error(`Error creating directory: ${err}`);
|
|
720
|
+
}
|
|
742
721
|
}
|
|
743
722
|
else {
|
|
744
|
-
this.log.
|
|
723
|
+
this.log.error(`Error accessing directory: ${err}`);
|
|
745
724
|
}
|
|
746
725
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
726
|
+
}
|
|
727
|
+
this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
728
|
+
// Matterbridge version
|
|
729
|
+
const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
|
|
730
|
+
this.matterbridgeVersion = packageJson.version;
|
|
731
|
+
this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
|
|
732
|
+
this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
|
|
733
|
+
// Matterbridge latest version
|
|
734
|
+
if (this.nodeContext)
|
|
735
|
+
this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
|
|
736
|
+
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
737
|
+
this.getMatterbridgeLatestVersion();
|
|
738
|
+
// Current working directory
|
|
739
|
+
const currentDir = process.cwd();
|
|
740
|
+
this.log.debug(`Current Working Directory: ${currentDir}`);
|
|
741
|
+
// Command line arguments (excluding 'node' and the script name)
|
|
742
|
+
const cmdArgs = process.argv.slice(2).join(' ');
|
|
743
|
+
this.log.debug(`Command Line Arguments: ${cmdArgs}`);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Retrieves the latest version of a package from the npm registry.
|
|
747
|
+
* @param packageName - The name of the package.
|
|
748
|
+
* @returns A Promise that resolves to the latest version of the package.
|
|
749
|
+
*/
|
|
750
|
+
async getLatestVersion(packageName) {
|
|
751
|
+
return new Promise((resolve, reject) => {
|
|
752
|
+
this.execRunningCount++;
|
|
753
|
+
exec(`npm view ${packageName} version`, (error, stdout) => {
|
|
754
|
+
this.execRunningCount--;
|
|
755
|
+
if (error) {
|
|
756
|
+
reject(error);
|
|
757
757
|
}
|
|
758
758
|
else {
|
|
759
|
-
|
|
759
|
+
resolve(stdout.trim());
|
|
760
760
|
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
this.log.error(`Plugin ${plg}${plugin.name}${er} context not found`);
|
|
776
|
-
await context?.clearAll();
|
|
777
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
778
|
-
this.log.info(`Reset commissionig for plugin ${plg}${plugin.name}${nf} done! Remove the device from the controller.`);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Retrieves the path to the global Node.js modules directory.
|
|
766
|
+
* @returns A promise that resolves to the path of the global Node.js modules directory.
|
|
767
|
+
*/
|
|
768
|
+
async getGlobalNodeModules() {
|
|
769
|
+
return new Promise((resolve, reject) => {
|
|
770
|
+
this.execRunningCount++;
|
|
771
|
+
exec('npm root -g', (error, stdout) => {
|
|
772
|
+
this.execRunningCount--;
|
|
773
|
+
if (error) {
|
|
774
|
+
reject(error);
|
|
779
775
|
}
|
|
780
776
|
else {
|
|
781
|
-
|
|
777
|
+
resolve(stdout.trim());
|
|
782
778
|
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
catch (err) {
|
|
786
|
-
this.log.error(`Failed to load plugin from ${plg}${packageJsonPath}${er}: ${err}`);
|
|
787
|
-
}
|
|
779
|
+
});
|
|
780
|
+
});
|
|
788
781
|
}
|
|
789
782
|
/**
|
|
790
|
-
*
|
|
791
|
-
*
|
|
783
|
+
* Retrieves the latest version of Matterbridge and performs necessary actions based on the version comparison.
|
|
784
|
+
* @private
|
|
785
|
+
* @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
|
|
792
786
|
*/
|
|
793
|
-
async
|
|
794
|
-
this.
|
|
795
|
-
|
|
796
|
-
|
|
787
|
+
async getMatterbridgeLatestVersion() {
|
|
788
|
+
this.getLatestVersion('matterbridge')
|
|
789
|
+
.then(async (matterbridgeLatestVersion) => {
|
|
790
|
+
this.matterbridgeLatestVersion = matterbridgeLatestVersion;
|
|
791
|
+
this.matterbridgeInformation.matterbridgeLatestVersion = this.matterbridgeLatestVersion;
|
|
792
|
+
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
793
|
+
await this.nodeContext?.set('matterbridgeLatestVersion', this.matterbridgeLatestVersion);
|
|
794
|
+
if (this.matterbridgeVersion !== this.matterbridgeLatestVersion) {
|
|
795
|
+
this.log.warn(`Matterbridge is out of date. Current version: ${this.matterbridgeVersion}, Latest version: ${this.matterbridgeLatestVersion}`);
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
.catch((error) => {
|
|
799
|
+
this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
|
|
800
|
+
// error.stack && this.log.debug(error.stack);
|
|
797
801
|
});
|
|
798
|
-
|
|
799
|
-
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Retrieves the latest version of a plugin and updates the plugin's latestVersion property.
|
|
805
|
+
* If the plugin's version is different from the latest version, logs a warning message.
|
|
806
|
+
* If the plugin's version is the same as the latest version, logs an info message.
|
|
807
|
+
* If there is an error retrieving the latest version, logs an error message.
|
|
808
|
+
*
|
|
809
|
+
* @private
|
|
810
|
+
* @param {RegisteredPlugin} plugin - The plugin for which to retrieve the latest version.
|
|
811
|
+
* @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
|
|
812
|
+
*/
|
|
813
|
+
async getPluginLatestVersion(plugin) {
|
|
814
|
+
this.getLatestVersion(plugin.name)
|
|
815
|
+
.then(async (latestVersion) => {
|
|
816
|
+
plugin.latestVersion = latestVersion;
|
|
817
|
+
if (plugin.version !== latestVersion)
|
|
818
|
+
this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
|
|
819
|
+
else
|
|
820
|
+
this.log.info(`The plugin ${plg}${plugin.name}${nf} is up to date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
|
|
821
|
+
})
|
|
822
|
+
.catch((error) => {
|
|
823
|
+
this.log.error(`Error getting ${plugin.name} latest version: ${error.message}`);
|
|
824
|
+
// error.stack && this.log.debug(error.stack);
|
|
800
825
|
});
|
|
801
826
|
}
|
|
802
827
|
/**
|
|
803
|
-
* Update matterbridge.
|
|
828
|
+
* Update matterbridge and cleanup.
|
|
804
829
|
*/
|
|
805
830
|
async updateProcess() {
|
|
806
831
|
await this.cleanup('updating...', false);
|
|
@@ -822,7 +847,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
822
847
|
*/
|
|
823
848
|
async unregisterAndShutdownProcess() {
|
|
824
849
|
this.log.info('Unregistering all devices and shutting down...');
|
|
825
|
-
for (const plugin of this.
|
|
850
|
+
for (const plugin of this.plugins /* .filter((plugin) => plugin.enabled && !plugin.error))*/) {
|
|
826
851
|
await this.removeAllBridgedDevices(plugin.name);
|
|
827
852
|
}
|
|
828
853
|
await this.cleanup('unregistered all devices and shutting down...', false);
|
|
@@ -846,82 +871,39 @@ export class Matterbridge extends EventEmitter {
|
|
|
846
871
|
* @returns A promise that resolves when the cleanup is completed.
|
|
847
872
|
*/
|
|
848
873
|
async cleanup(message, restart = false) {
|
|
849
|
-
if (!this.hasCleanupStarted) {
|
|
874
|
+
if (this.initialized && !this.hasCleanupStarted) {
|
|
850
875
|
this.hasCleanupStarted = true;
|
|
851
876
|
this.log.info(message);
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
// Clear the update interval
|
|
877
|
+
// Deregisters the SIGINT and SIGTERM signal handlers
|
|
878
|
+
this.deregisterSignalHandlers();
|
|
879
|
+
// Clear the check update interval
|
|
856
880
|
if (this.checkUpdateInterval)
|
|
857
881
|
clearInterval(this.checkUpdateInterval);
|
|
858
882
|
this.checkUpdateInterval = undefined;
|
|
859
|
-
this.log.debug('
|
|
860
|
-
//
|
|
861
|
-
|
|
883
|
+
this.log.debug('Check update interval cleared');
|
|
884
|
+
// Clear the configure timeout
|
|
885
|
+
if (this.configureTimeout) {
|
|
886
|
+
clearTimeout(this.configureTimeout);
|
|
887
|
+
this.configureTimeout = undefined;
|
|
888
|
+
this.log.debug('Matterbridge configure timeout cleared');
|
|
889
|
+
}
|
|
890
|
+
// Clear the reachability timeout
|
|
891
|
+
if (this.reachabilityTimeout) {
|
|
892
|
+
clearTimeout(this.reachabilityTimeout);
|
|
893
|
+
this.reachabilityTimeout = undefined;
|
|
894
|
+
this.log.debug('Matterbridge reachability timeout cleared');
|
|
895
|
+
}
|
|
896
|
+
// Calling the shutdown method of each plugin and clear the reachability timeout
|
|
897
|
+
for (const plugin of this.plugins) {
|
|
862
898
|
if (!plugin.enabled || plugin.error)
|
|
863
899
|
continue;
|
|
864
|
-
this.
|
|
865
|
-
if (plugin.
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
this.log.error(`Plugin ${plg}${plugin.name}${er} shutting down error: ${error}`);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
else {
|
|
875
|
-
this.log.debug(`Plugin ${plg}${plugin.name}${db} platform not found`);
|
|
900
|
+
await this.plugins.shutdown(plugin, 'Matterbridge is closing: ' + message, false);
|
|
901
|
+
if (plugin.reachabilityTimeout) {
|
|
902
|
+
clearTimeout(plugin.reachabilityTimeout);
|
|
903
|
+
plugin.reachabilityTimeout = undefined;
|
|
904
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} reachability timeout cleared`);
|
|
876
905
|
}
|
|
877
906
|
}
|
|
878
|
-
// Set reachability to false
|
|
879
|
-
/*
|
|
880
|
-
this.log.debug(`*Changing reachability to false for ${this.registeredDevices.length} devices (${this.bridgeMode} mode):`);
|
|
881
|
-
this.registeredDevices.forEach((registeredDevice) => {
|
|
882
|
-
const plugin = this.registeredPlugins.find((plugin) => plugin.name === registeredDevice.plugin);
|
|
883
|
-
if (!plugin) {
|
|
884
|
-
this.log.error(`Plugin ${plg}${registeredDevice.plugin}${er} not found`);
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (this.bridgeMode === 'bridge' && registeredDevice.device.number) {
|
|
888
|
-
this.log.debug(`*-- device: ${dev}${registeredDevice.device.name}${db} plugin ${plg}${registeredDevice.plugin}${db} type ${plugin.type}${db}`);
|
|
889
|
-
registeredDevice.device.setBridgedDeviceReachability(false);
|
|
890
|
-
registeredDevice.device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
|
|
891
|
-
}
|
|
892
|
-
if (this.bridgeMode === 'childbridge') {
|
|
893
|
-
if (plugin.type === 'DynamicPlatform' && registeredDevice.device.number) {
|
|
894
|
-
this.log.debug(`*-- device: ${dev}${registeredDevice.device.name}${db} plugin ${plg}${registeredDevice.plugin}${db} type ${plugin.type}${db}`);
|
|
895
|
-
registeredDevice.device.setBridgedDeviceReachability(false);
|
|
896
|
-
registeredDevice.device.getClusterServerById(BridgedDeviceBasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
if (this.bridgeMode === 'bridge') {
|
|
901
|
-
this.log.debug('*Changing reachability to false for Matterbridge');
|
|
902
|
-
this.matterAggregator?.getClusterServerById(BasicInformation.Cluster.id)?.setReachableAttribute(false);
|
|
903
|
-
this.matterAggregator?.getClusterServerById(BasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
|
|
904
|
-
this.commissioningServer?.setReachability(false);
|
|
905
|
-
}
|
|
906
|
-
if (this.bridgeMode === 'childbridge') {
|
|
907
|
-
for (const plugin of this.registeredPlugins) {
|
|
908
|
-
if (!plugin.enabled || plugin.error) continue;
|
|
909
|
-
this.log.debug(`*Changing reachability to false for plugin ${plg}${plugin.name}${db} type ${plugin.type}`);
|
|
910
|
-
plugin.aggregator?.getClusterServerById(BasicInformation.Cluster.id)?.setReachableAttribute(false);
|
|
911
|
-
plugin.aggregator?.getClusterServerById(BasicInformation.Cluster.id)?.triggerReachableChangedEvent({ reachableNewValue: false });
|
|
912
|
-
plugin.commissioningServer?.setReachability(false);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
*/
|
|
916
|
-
// Close the express server
|
|
917
|
-
/*
|
|
918
|
-
if (this.expressServer) {
|
|
919
|
-
this.expressServer.close();
|
|
920
|
-
this.expressServer.removeAllListeners();
|
|
921
|
-
this.expressServer = undefined;
|
|
922
|
-
this.log.debug('Express server closed successfully');
|
|
923
|
-
}
|
|
924
|
-
*/
|
|
925
907
|
// Close the http server
|
|
926
908
|
if (this.httpServer) {
|
|
927
909
|
this.httpServer.close();
|
|
@@ -960,75 +942,82 @@ export class Matterbridge extends EventEmitter {
|
|
|
960
942
|
});
|
|
961
943
|
this.webSocketServer = undefined;
|
|
962
944
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
this.registeredPlugins = [];
|
|
992
|
-
this.registeredDevices = [];
|
|
993
|
-
this.log.info('Waiting for matter to deliver last messages...');
|
|
994
|
-
const cleanupTimeout2 = setTimeout(async () => {
|
|
995
|
-
if (restart) {
|
|
996
|
-
if (message === 'updating...') {
|
|
997
|
-
this.log.info('Cleanup completed. Updating...');
|
|
998
|
-
Matterbridge.instance = undefined;
|
|
999
|
-
this.emit('update');
|
|
1000
|
-
}
|
|
1001
|
-
else if (message === 'restarting...') {
|
|
1002
|
-
this.log.info('Cleanup completed. Restarting...');
|
|
1003
|
-
Matterbridge.instance = undefined;
|
|
1004
|
-
this.emit('restart');
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
else {
|
|
1008
|
-
if (message === 'shutting down with reset...') {
|
|
1009
|
-
// Delete matter storage file
|
|
1010
|
-
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1011
|
-
await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
|
|
1012
|
-
this.log.info('Reset done! Remove all paired devices from the controllers.');
|
|
1013
|
-
}
|
|
1014
|
-
if (message === 'shutting down with factory reset...') {
|
|
1015
|
-
// Delete matter storage file
|
|
1016
|
-
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1017
|
-
await fs.unlink(path.join(this.matterbridgeDirectory, 'matterbridge.json'));
|
|
1018
|
-
// Delete node storage directory with its subdirectories
|
|
1019
|
-
this.log.info('Resetting Matterbridge storage...');
|
|
1020
|
-
await fs.rm(path.join(this.matterbridgeDirectory, 'storage'), { recursive: true });
|
|
1021
|
-
this.log.info('Factory reset done! Remove all paired devices from the controllers.');
|
|
1022
|
-
}
|
|
1023
|
-
this.log.info('Cleanup completed. Shutting down...');
|
|
1024
|
-
Matterbridge.instance = undefined;
|
|
1025
|
-
this.emit('shutdown');
|
|
945
|
+
// this.cleanupTimeout1 = setTimeout(async () => {
|
|
946
|
+
// Closing matter
|
|
947
|
+
await this.stopMatterServer();
|
|
948
|
+
// Closing storage
|
|
949
|
+
await this.stopStorage();
|
|
950
|
+
// Serialize registeredDevices
|
|
951
|
+
if (this.nodeStorage && this.nodeContext) {
|
|
952
|
+
this.log.info('Saving registered devices...');
|
|
953
|
+
const serializedRegisteredDevices = [];
|
|
954
|
+
this.registeredDevices.forEach((registeredDevice) => {
|
|
955
|
+
const serializedMatterbridgeDevice = registeredDevice.device.serialize(registeredDevice.plugin);
|
|
956
|
+
// this.log.info(`- ${serializedMatterbridgeDevice.deviceName}${rs}\n`, serializedMatterbridgeDevice);
|
|
957
|
+
if (serializedMatterbridgeDevice)
|
|
958
|
+
serializedRegisteredDevices.push(serializedMatterbridgeDevice);
|
|
959
|
+
});
|
|
960
|
+
await this.nodeContext.set('devices', serializedRegisteredDevices);
|
|
961
|
+
this.log.info(`Saved registered devices (${serializedRegisteredDevices?.length})`);
|
|
962
|
+
// Clear nodeContext and nodeStorage (they just need 1000ms to write the data to disk)
|
|
963
|
+
this.log.debug(`Closing node storage context for ${plg}Matterbridge${db}...`);
|
|
964
|
+
await this.nodeContext.close();
|
|
965
|
+
this.nodeContext = undefined;
|
|
966
|
+
// Clear nodeContext for each plugin (they just need 1000ms to write the data to disk)
|
|
967
|
+
for (const plugin of this.plugins) {
|
|
968
|
+
if (plugin.nodeContext) {
|
|
969
|
+
this.log.debug(`Closing node storage context for plugin ${plg}${plugin.name}${db}...`);
|
|
970
|
+
await plugin.nodeContext.close();
|
|
971
|
+
plugin.nodeContext = undefined;
|
|
1026
972
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
973
|
+
}
|
|
974
|
+
this.log.debug('Closing node storage manager...');
|
|
975
|
+
await this.nodeStorage.close();
|
|
976
|
+
this.nodeStorage = undefined;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
this.log.error('Error saving registered devices: nodeContext not found!');
|
|
980
|
+
}
|
|
981
|
+
this.plugins.clear();
|
|
982
|
+
this.registeredDevices = [];
|
|
983
|
+
// this.log.info('Waiting for matter to deliver last messages...');
|
|
984
|
+
// this.cleanupTimeout2 = setTimeout(async () => {
|
|
985
|
+
if (restart) {
|
|
986
|
+
if (message === 'updating...') {
|
|
987
|
+
this.log.info('Cleanup completed. Updating...');
|
|
988
|
+
Matterbridge.instance = undefined;
|
|
989
|
+
this.emit('update');
|
|
990
|
+
}
|
|
991
|
+
else if (message === 'restarting...') {
|
|
992
|
+
this.log.info('Cleanup completed. Restarting...');
|
|
993
|
+
Matterbridge.instance = undefined;
|
|
994
|
+
this.emit('restart');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
if (message === 'shutting down with reset...') {
|
|
999
|
+
// Delete matter storage file
|
|
1000
|
+
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1001
|
+
await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
1002
|
+
this.log.info('Reset done! Remove all paired devices from the controllers.');
|
|
1003
|
+
}
|
|
1004
|
+
if (message === 'shutting down with factory reset...') {
|
|
1005
|
+
// Delete matter storage file
|
|
1006
|
+
this.log.info('Resetting Matterbridge commissioning information...');
|
|
1007
|
+
await fs.unlink(path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
1008
|
+
// Delete node storage directory with its subdirectories
|
|
1009
|
+
this.log.info('Resetting Matterbridge storage...');
|
|
1010
|
+
await fs.rm(path.join(this.matterbridgeDirectory, this.nodeStorageName), { recursive: true });
|
|
1011
|
+
this.log.info('Factory reset done! Remove all paired devices from the controllers.');
|
|
1012
|
+
}
|
|
1013
|
+
this.log.info('Cleanup completed. Shutting down...');
|
|
1014
|
+
Matterbridge.instance = undefined;
|
|
1015
|
+
this.emit('shutdown');
|
|
1016
|
+
}
|
|
1017
|
+
this.hasCleanupStarted = false;
|
|
1018
|
+
this.initialized = false;
|
|
1019
|
+
// }, 2 * 1000);
|
|
1020
|
+
// }, 3 * 1000);
|
|
1032
1021
|
}
|
|
1033
1022
|
}
|
|
1034
1023
|
/**
|
|
@@ -1044,7 +1033,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1044
1033
|
}
|
|
1045
1034
|
this.log.debug(`Adding bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
|
|
1046
1035
|
// Check if the plugin is registered
|
|
1047
|
-
const plugin = this.
|
|
1036
|
+
const plugin = this.plugins.get(pluginName);
|
|
1048
1037
|
if (!plugin) {
|
|
1049
1038
|
this.log.error(`Error adding bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} not found`);
|
|
1050
1039
|
return;
|
|
@@ -1102,22 +1091,22 @@ export class Matterbridge extends EventEmitter {
|
|
|
1102
1091
|
*/
|
|
1103
1092
|
async removeBridgedDevice(pluginName, device) {
|
|
1104
1093
|
if (this.bridgeMode === 'bridge' && !this.matterAggregator) {
|
|
1105
|
-
this.log.error(`
|
|
1094
|
+
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: matterAggregator not found`);
|
|
1106
1095
|
return;
|
|
1107
1096
|
}
|
|
1108
1097
|
this.log.debug(`Removing bridged device ${dev}${device.deviceName}${db} (${dev}${device.name}${db}) for plugin ${plg}${pluginName}${db}`);
|
|
1109
1098
|
// Check if the plugin is registered
|
|
1110
|
-
const plugin = this.
|
|
1099
|
+
const plugin = this.plugins.get(pluginName);
|
|
1111
1100
|
if (!plugin) {
|
|
1112
|
-
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} not found`);
|
|
1101
|
+
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
1113
1102
|
return;
|
|
1114
1103
|
}
|
|
1115
|
-
if (this.bridgeMode === 'childbridge' && !plugin.aggregator) {
|
|
1116
|
-
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) plugin ${plg}${pluginName}${er} aggregator not found`);
|
|
1104
|
+
if (this.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatfoem' && !plugin.aggregator) {
|
|
1105
|
+
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: aggregator not found`);
|
|
1117
1106
|
return;
|
|
1118
1107
|
}
|
|
1119
|
-
if (this.bridgeMode === 'childbridge' && !plugin.
|
|
1120
|
-
this.log.
|
|
1108
|
+
if (this.bridgeMode === 'childbridge' && plugin.type === 'DynamicPlatfoem' && !plugin.commissioningServer) {
|
|
1109
|
+
this.log.error(`Error removing bridged device ${dev}${device.deviceName}${er} (${dev}${device.name}${er}) for plugin ${plg}${pluginName}${er}: commissioning server not found`);
|
|
1121
1110
|
return;
|
|
1122
1111
|
}
|
|
1123
1112
|
// Remove the device from matterbridge aggregator in bridge mode
|
|
@@ -1140,7 +1129,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
1140
1129
|
// Remove the device in childbridge mode
|
|
1141
1130
|
if (this.bridgeMode === 'childbridge') {
|
|
1142
1131
|
if (plugin.type === 'AccessoryPlatform') {
|
|
1143
|
-
this.
|
|
1132
|
+
this.registeredDevices.forEach((registeredDevice, index) => {
|
|
1133
|
+
if (registeredDevice.device === device) {
|
|
1134
|
+
this.registeredDevices.splice(index, 1);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1144
1138
|
}
|
|
1145
1139
|
else if (plugin.type === 'DynamicPlatform') {
|
|
1146
1140
|
this.registeredDevices.forEach((registeredDevice, index) => {
|
|
@@ -1158,6 +1152,11 @@ export class Matterbridge extends EventEmitter {
|
|
|
1158
1152
|
plugin.registeredDevices--;
|
|
1159
1153
|
if (plugin.addedDevices !== undefined)
|
|
1160
1154
|
plugin.addedDevices--;
|
|
1155
|
+
if (plugin.registeredDevices === 0 && plugin.addedDevices === 0 && plugin.commissioningServer) {
|
|
1156
|
+
this.matterServer?.removeCommissioningServer(plugin.commissioningServer);
|
|
1157
|
+
plugin.commissioningServer = undefined;
|
|
1158
|
+
this.log.info(`Removed commissioning server for plugin ${plg}${pluginName}${nf}`);
|
|
1159
|
+
}
|
|
1161
1160
|
}
|
|
1162
1161
|
}
|
|
1163
1162
|
/**
|
|
@@ -1167,11 +1166,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
1167
1166
|
* @returns A promise that resolves when all devices have been removed.
|
|
1168
1167
|
*/
|
|
1169
1168
|
async removeAllBridgedDevices(pluginName) {
|
|
1170
|
-
|
|
1171
|
-
if (this.bridgeMode === 'childbridge' && plugin?.type === 'AccessoryPlatform') {
|
|
1172
|
-
this.log.info(`Removing devices for plugin ${plg}${pluginName}${nf} type AccessoryPlatform is not supported in childbridge mode`);
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1169
|
+
this.log.debug(`Removing all bridged devices for plugin ${plg}${pluginName}${db}`);
|
|
1175
1170
|
const devicesToRemove = [];
|
|
1176
1171
|
for (const registeredDevice of this.registeredDevices) {
|
|
1177
1172
|
if (registeredDevice.plugin === pluginName) {
|
|
@@ -1182,83 +1177,6 @@ export class Matterbridge extends EventEmitter {
|
|
|
1182
1177
|
this.removeBridgedDevice(pluginName, registeredDevice.device);
|
|
1183
1178
|
}
|
|
1184
1179
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Starts the storage process based on the specified storage type and name.
|
|
1187
|
-
* @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
|
|
1188
|
-
* @param {string} storageName - The name of the storage file.
|
|
1189
|
-
* @returns {Promise<void>} - A promise that resolves when the storage process is started.
|
|
1190
|
-
*/
|
|
1191
|
-
async startStorage(storageType, storageName) {
|
|
1192
|
-
this.log.debug(`Starting storage ${storageType} ${storageName}`);
|
|
1193
|
-
if (storageType === 'disk') {
|
|
1194
|
-
const storageDisk = new StorageBackendDisk(storageName);
|
|
1195
|
-
this.storageManager = new StorageManager(storageDisk);
|
|
1196
|
-
}
|
|
1197
|
-
else if (storageType === 'json') {
|
|
1198
|
-
if (!storageName.endsWith('.json'))
|
|
1199
|
-
storageName += '.json';
|
|
1200
|
-
const storageJson = new StorageBackendJsonFile(storageName);
|
|
1201
|
-
this.storageManager = new StorageManager(storageJson);
|
|
1202
|
-
}
|
|
1203
|
-
else {
|
|
1204
|
-
this.log.error(`Unsupported storage type ${storageType}`);
|
|
1205
|
-
await this.cleanup('Unsupported storage type');
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
try {
|
|
1209
|
-
await this.storageManager.initialize();
|
|
1210
|
-
this.log.debug('Storage initialized');
|
|
1211
|
-
if (storageType === 'json') {
|
|
1212
|
-
await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
catch (error) {
|
|
1216
|
-
this.log.error('Storage initialize() error! The file .matterbridge/matterbridge.json may be corrupted.');
|
|
1217
|
-
this.log.error('Please delete it and rename matterbridge.backup.json to matterbridge.json and try to restart Matterbridge.');
|
|
1218
|
-
await this.cleanup('Storage initialize() error!');
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Makes a backup copy of the specified JSON storage file.
|
|
1223
|
-
*
|
|
1224
|
-
* @param storageName - The name of the JSON storage file to be backed up.
|
|
1225
|
-
* @param backupName - The name of the backup file to be created.
|
|
1226
|
-
*/
|
|
1227
|
-
async backupJsonStorage(storageName, backupName) {
|
|
1228
|
-
try {
|
|
1229
|
-
this.log.debug(`Making backup copy of ${storageName}`);
|
|
1230
|
-
await fs.copyFile(storageName, backupName);
|
|
1231
|
-
this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
|
|
1232
|
-
}
|
|
1233
|
-
catch (err) {
|
|
1234
|
-
if (err instanceof Error && 'code' in err) {
|
|
1235
|
-
if (err.code === 'ENOENT') {
|
|
1236
|
-
this.log.info(`No existing file to back up for ${storageName}. This is expected on the first run.`);
|
|
1237
|
-
}
|
|
1238
|
-
else {
|
|
1239
|
-
this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
else {
|
|
1243
|
-
this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Stops the storage.
|
|
1249
|
-
* @returns {Promise<void>} A promise that resolves when the storage is stopped.
|
|
1250
|
-
*/
|
|
1251
|
-
async stopStorage() {
|
|
1252
|
-
this.log.debug('Stopping storage');
|
|
1253
|
-
await this.storageManager?.close();
|
|
1254
|
-
this.log.debug('Storage closed');
|
|
1255
|
-
this.storageManager = undefined;
|
|
1256
|
-
this.matterbridgeContext = undefined;
|
|
1257
|
-
this.mattercontrollerContext = undefined;
|
|
1258
|
-
}
|
|
1259
|
-
async testStartMatterBridge() {
|
|
1260
|
-
// Start the Matterbridge
|
|
1261
|
-
}
|
|
1262
1180
|
/**
|
|
1263
1181
|
* Loads the schema for a plugin.
|
|
1264
1182
|
* If the schema file exists in the plugin directory, it reads the file and returns the parsed JSON data and delete the schema form .matterbridge.
|
|
@@ -1319,28 +1237,6 @@ export class Matterbridge extends EventEmitter {
|
|
|
1319
1237
|
return schema;
|
|
1320
1238
|
}
|
|
1321
1239
|
}
|
|
1322
|
-
/**
|
|
1323
|
-
* Saves the plugin configuration to a JSON file.
|
|
1324
|
-
* @param plugin - The registered plugin.
|
|
1325
|
-
* @param config - The platform configuration.
|
|
1326
|
-
* @returns A promise that resolves when the configuration is saved successfully, or with an error logged.
|
|
1327
|
-
*/
|
|
1328
|
-
async savePluginConfigFromJson(plugin, config) {
|
|
1329
|
-
if (!config.name || !config.type || config.name !== plugin.name) {
|
|
1330
|
-
this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config. Wrong config data content.`);
|
|
1331
|
-
return;
|
|
1332
|
-
}
|
|
1333
|
-
const configFile = path.join(this.matterbridgeDirectory, `${plugin.name}.config.json`);
|
|
1334
|
-
try {
|
|
1335
|
-
await this.writeFile(configFile, JSON.stringify(config, null, 2));
|
|
1336
|
-
plugin.configJson = config;
|
|
1337
|
-
this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, config);
|
|
1338
|
-
}
|
|
1339
|
-
catch (err) {
|
|
1340
|
-
this.log.error(`Error saving plugin ${plg}${plugin.name}${er} config: ${err}`);
|
|
1341
|
-
return;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
1240
|
/**
|
|
1345
1241
|
* Loads the configuration for a plugin.
|
|
1346
1242
|
* If the configuration file exists, it reads the file and returns the parsed JSON data.
|
|
@@ -1438,6 +1334,70 @@ export class Matterbridge extends EventEmitter {
|
|
|
1438
1334
|
this.log.error(`Error writing to ${filePath}:`, error);
|
|
1439
1335
|
}
|
|
1440
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Loads a plugin and returns the corresponding MatterbridgePlatform instance.
|
|
1339
|
+
* @param plugin - The plugin to load.
|
|
1340
|
+
* @param start - Optional flag indicating whether to start the plugin after loading. Default is false.
|
|
1341
|
+
* @param message - Optional message to pass to the plugin when starting.
|
|
1342
|
+
* @returns A Promise that resolves to the loaded MatterbridgePlatform instance.
|
|
1343
|
+
* @throws An error if the plugin is not enabled, already loaded, or fails to load.
|
|
1344
|
+
*/
|
|
1345
|
+
async loadPlugin(plugin, start = false, message = '') {
|
|
1346
|
+
if (!plugin.enabled) {
|
|
1347
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not enabled`);
|
|
1348
|
+
return Promise.resolve(undefined);
|
|
1349
|
+
}
|
|
1350
|
+
if (plugin.platform) {
|
|
1351
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
|
|
1352
|
+
return Promise.resolve(plugin.platform);
|
|
1353
|
+
}
|
|
1354
|
+
this.log.info(`Loading plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
1355
|
+
try {
|
|
1356
|
+
// Load the package.json of the plugin
|
|
1357
|
+
const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
|
|
1358
|
+
// Resolve the main module path relative to package.json
|
|
1359
|
+
const pluginEntry = path.resolve(path.dirname(plugin.path), packageJson.main);
|
|
1360
|
+
// Dynamically import the plugin
|
|
1361
|
+
const pluginUrl = pathToFileURL(pluginEntry);
|
|
1362
|
+
this.log.debug(`Importing plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
|
|
1363
|
+
const pluginInstance = await import(pluginUrl.href);
|
|
1364
|
+
this.log.debug(`Imported plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
|
|
1365
|
+
// Call the default export function of the plugin, passing this MatterBridge instance, the log and the config
|
|
1366
|
+
if (pluginInstance.default) {
|
|
1367
|
+
const config = await this.loadPluginConfig(plugin);
|
|
1368
|
+
const log = new AnsiLogger({ logName: plugin.description ?? 'No description', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: config.debug ?? false });
|
|
1369
|
+
const platform = pluginInstance.default(this, log, config);
|
|
1370
|
+
platform.name = packageJson.name;
|
|
1371
|
+
platform.config = config;
|
|
1372
|
+
platform.version = packageJson.version;
|
|
1373
|
+
plugin.name = packageJson.name;
|
|
1374
|
+
plugin.description = packageJson.description ?? 'No description';
|
|
1375
|
+
plugin.version = packageJson.version;
|
|
1376
|
+
plugin.author = packageJson.author ?? 'Unknown';
|
|
1377
|
+
plugin.type = platform.type;
|
|
1378
|
+
plugin.platform = platform;
|
|
1379
|
+
plugin.loaded = true;
|
|
1380
|
+
plugin.registeredDevices = 0;
|
|
1381
|
+
plugin.addedDevices = 0;
|
|
1382
|
+
plugin.configJson = config;
|
|
1383
|
+
plugin.schemaJson = await this.loadPluginSchema(plugin);
|
|
1384
|
+
this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type} ${db}(entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
|
|
1385
|
+
if (start)
|
|
1386
|
+
this.startPlugin(plugin, message); // No await do it asyncronously
|
|
1387
|
+
return Promise.resolve(platform);
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`);
|
|
1391
|
+
plugin.error = true;
|
|
1392
|
+
return Promise.resolve(undefined);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
catch (err) {
|
|
1396
|
+
this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
1397
|
+
plugin.error = true;
|
|
1398
|
+
return Promise.resolve(undefined);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1441
1401
|
/**
|
|
1442
1402
|
* Starts a plugin.
|
|
1443
1403
|
*
|
|
@@ -1493,13 +1453,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
1493
1453
|
this.log.info(`Plugin ${plg}${plugin.name}${nf} already configured`);
|
|
1494
1454
|
return Promise.resolve();
|
|
1495
1455
|
}
|
|
1496
|
-
this.log.info(`Configuring plugin ${plg}${plugin.name}${
|
|
1456
|
+
this.log.info(`Configuring plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
1497
1457
|
try {
|
|
1498
1458
|
plugin.platform
|
|
1499
1459
|
.onConfigure()
|
|
1500
1460
|
.then(() => {
|
|
1501
1461
|
plugin.configured = true;
|
|
1502
|
-
this.log.info(`Configured plugin ${plg}${plugin.name}${
|
|
1462
|
+
this.log.info(`Configured plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
1503
1463
|
this.savePluginConfig(plugin);
|
|
1504
1464
|
return Promise.resolve();
|
|
1505
1465
|
})
|
|
@@ -1515,96 +1475,167 @@ export class Matterbridge extends EventEmitter {
|
|
|
1515
1475
|
return Promise.resolve();
|
|
1516
1476
|
}
|
|
1517
1477
|
}
|
|
1478
|
+
async startTest() {
|
|
1479
|
+
// Start the Matterbridge
|
|
1480
|
+
}
|
|
1518
1481
|
/**
|
|
1519
|
-
*
|
|
1520
|
-
* @
|
|
1521
|
-
* @returns A
|
|
1482
|
+
* Starts the Matterbridge in bridge mode.
|
|
1483
|
+
* @private
|
|
1484
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1522
1485
|
*/
|
|
1523
|
-
async
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1486
|
+
async startBridge() {
|
|
1487
|
+
// Plugins are loaded and started by loadPlugin on startup and plugin.loaded and plugin.started are set to true
|
|
1488
|
+
// Plugins are configured by a timer when matter server is started and plugin.configured is set to true
|
|
1489
|
+
this.log.debug('***Starting startMatterInterval in bridge mode');
|
|
1490
|
+
let failCount = 0;
|
|
1491
|
+
const startMatterInterval = setInterval(async () => {
|
|
1492
|
+
for (const plugin of this.plugins) {
|
|
1493
|
+
// new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
|
|
1494
|
+
if (!plugin.enabled)
|
|
1495
|
+
continue;
|
|
1496
|
+
if (plugin.error) {
|
|
1497
|
+
clearInterval(startMatterInterval);
|
|
1498
|
+
this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
|
|
1499
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
|
|
1500
|
+
this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
|
|
1501
|
+
this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (!plugin.loaded || !plugin.started) {
|
|
1505
|
+
this.log.debug(`***Waiting (failSafeCount=${failCount}/30) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
|
|
1506
|
+
failCount++;
|
|
1507
|
+
if (failCount > 30) {
|
|
1508
|
+
this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
|
|
1509
|
+
plugin.error = true;
|
|
1510
|
+
}
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
clearInterval(startMatterInterval);
|
|
1515
|
+
this.log.debug('***Cleared startMatterInterval interval for Matterbridge');
|
|
1516
|
+
await this.startMatterServer();
|
|
1517
|
+
this.log.info('Matter server started');
|
|
1518
|
+
// Configure the plugins
|
|
1519
|
+
this.configureTimeout = setTimeout(async () => {
|
|
1520
|
+
for (const plugin of this.plugins) {
|
|
1521
|
+
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
1522
|
+
continue;
|
|
1523
|
+
try {
|
|
1524
|
+
await this.plugins.configure(plugin); // No await do it asyncronously
|
|
1525
|
+
}
|
|
1526
|
+
catch (error) {
|
|
1527
|
+
plugin.error = true;
|
|
1528
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}, 30 * 1000);
|
|
1532
|
+
// Show the QR code for commissioning or log the already commissioned message
|
|
1533
|
+
await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
|
|
1534
|
+
// Setting reachability to true
|
|
1535
|
+
this.reachabilityTimeout = setTimeout(() => {
|
|
1536
|
+
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
1537
|
+
if (this.commissioningServer)
|
|
1538
|
+
this.setCommissioningServerReachability(this.commissioningServer, true);
|
|
1539
|
+
if (this.matterAggregator)
|
|
1540
|
+
this.setAggregatorReachability(this.matterAggregator, true);
|
|
1541
|
+
}, 60 * 1000);
|
|
1542
|
+
}, 1000);
|
|
1534
1543
|
}
|
|
1535
1544
|
/**
|
|
1536
|
-
*
|
|
1537
|
-
* @
|
|
1538
|
-
* @
|
|
1539
|
-
* @param message - Optional message to pass to the plugin when starting.
|
|
1540
|
-
* @returns A Promise that resolves to the loaded MatterbridgePlatform instance.
|
|
1541
|
-
* @throws An error if the plugin is not enabled, already loaded, or fails to load.
|
|
1545
|
+
* Starts the Matterbridge in childbridge mode.
|
|
1546
|
+
* @private
|
|
1547
|
+
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1542
1548
|
*/
|
|
1543
|
-
async
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
plugin.version = packageJson.version;
|
|
1574
|
-
plugin.author = packageJson.author;
|
|
1575
|
-
plugin.type = platform.type;
|
|
1576
|
-
plugin.platform = platform;
|
|
1577
|
-
plugin.loaded = true;
|
|
1578
|
-
plugin.registeredDevices = 0;
|
|
1579
|
-
plugin.addedDevices = 0;
|
|
1580
|
-
plugin.configJson = config;
|
|
1581
|
-
plugin.schemaJson = await this.loadPluginSchema(plugin);
|
|
1582
|
-
// Save the updated plugin data in the node storage
|
|
1583
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
1584
|
-
// this.getPluginLatestVersion(plugin); moved to parseCommandLine
|
|
1585
|
-
this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type} ${db}(entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
|
|
1586
|
-
if (start)
|
|
1587
|
-
this.startPlugin(plugin, message); // No await do it asyncronously
|
|
1588
|
-
return Promise.resolve(platform);
|
|
1549
|
+
async startChildbridge() {
|
|
1550
|
+
// Plugins are loaded and started by loadPlugin on startup and plugin.loaded and plugin.started are set to true
|
|
1551
|
+
// addDevice and addBridgedDeevice create the commissionig servers and add the devices to the the commissioning server or to the aggregator
|
|
1552
|
+
// Plugins are configured by a timer when matter server is started and plugin.configured is set to true
|
|
1553
|
+
this.log.debug('***Starting start matter interval in childbridge mode...');
|
|
1554
|
+
let failCount = 0;
|
|
1555
|
+
const startMatterInterval = setInterval(async () => {
|
|
1556
|
+
let allStarted = true;
|
|
1557
|
+
for (const plugin of this.plugins) {
|
|
1558
|
+
// new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
|
|
1559
|
+
if (!plugin.enabled)
|
|
1560
|
+
continue;
|
|
1561
|
+
if (plugin.error) {
|
|
1562
|
+
clearInterval(startMatterInterval);
|
|
1563
|
+
this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
|
|
1564
|
+
this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
|
|
1565
|
+
this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
|
|
1566
|
+
this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
this.log.debug(`***Checking plugin ${plg}${plugin.name}${db} to start matter in childbridge mode...`);
|
|
1570
|
+
if (!plugin.loaded || !plugin.started) {
|
|
1571
|
+
allStarted = false;
|
|
1572
|
+
this.log.debug(`***Waiting (failSafeCount=${failCount}/30) for plugin ${plg}${plugin.name}${db} to load (${plugin.loaded}) and start (${plugin.started}) ...`);
|
|
1573
|
+
failCount++;
|
|
1574
|
+
if (failCount > 30) {
|
|
1575
|
+
this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error mode.`);
|
|
1576
|
+
plugin.error = true;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1589
1579
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1580
|
+
if (!allStarted)
|
|
1581
|
+
return;
|
|
1582
|
+
clearInterval(startMatterInterval);
|
|
1583
|
+
this.log.debug('***Cleared startMatterInterval interval in childbridge mode');
|
|
1584
|
+
await this.startMatterServer();
|
|
1585
|
+
this.log.info('Matter server started');
|
|
1586
|
+
// Configure the plugins
|
|
1587
|
+
this.configureTimeout = setTimeout(async () => {
|
|
1588
|
+
for (const plugin of this.plugins) {
|
|
1589
|
+
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
1590
|
+
continue;
|
|
1591
|
+
try {
|
|
1592
|
+
await this.plugins.configure(plugin); // No await do it asyncronously
|
|
1593
|
+
}
|
|
1594
|
+
catch (error) {
|
|
1595
|
+
plugin.error = true;
|
|
1596
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}, 30 * 1000);
|
|
1600
|
+
for (const plugin of this.plugins) {
|
|
1601
|
+
if (!plugin.enabled || plugin.error)
|
|
1602
|
+
continue;
|
|
1603
|
+
if (!plugin.addedDevices || plugin.addedDevices === 0) {
|
|
1604
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} didn't add any devices to Matterbridge. Verify the plugin configuration.`);
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (!plugin.commissioningServer) {
|
|
1608
|
+
this.log.error(`Commissioning server not found for plugin ${plg}${plugin.name}${er}`);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (!plugin.storageContext) {
|
|
1612
|
+
this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (!plugin.nodeContext) {
|
|
1616
|
+
this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
|
|
1620
|
+
// Setting reachability to true
|
|
1621
|
+
plugin.reachabilityTimeout = setTimeout(() => {
|
|
1622
|
+
this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
|
|
1623
|
+
if (plugin.commissioningServer)
|
|
1624
|
+
this.setCommissioningServerReachability(plugin.commissioningServer, true);
|
|
1625
|
+
if (plugin.type === 'AccessoryPlatform' && plugin.device)
|
|
1626
|
+
this.setDeviceReachability(plugin.device, true);
|
|
1627
|
+
if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
|
|
1628
|
+
this.setAggregatorReachability(plugin.aggregator, true);
|
|
1629
|
+
}, 60 * 1000);
|
|
1594
1630
|
}
|
|
1595
|
-
}
|
|
1596
|
-
catch (err) {
|
|
1597
|
-
this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
1598
|
-
plugin.error = true;
|
|
1599
|
-
return Promise.resolve(undefined);
|
|
1600
|
-
}
|
|
1631
|
+
}, 1000);
|
|
1601
1632
|
}
|
|
1602
1633
|
/**
|
|
1603
1634
|
* Starts the Matterbridge controller.
|
|
1604
1635
|
* @private
|
|
1605
1636
|
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1606
1637
|
*/
|
|
1607
|
-
async
|
|
1638
|
+
async startController() {
|
|
1608
1639
|
if (!this.storageManager) {
|
|
1609
1640
|
this.log.error('No storage manager initialized');
|
|
1610
1641
|
await this.cleanup('No storage manager initialized');
|
|
@@ -1775,143 +1806,105 @@ export class Matterbridge extends EventEmitter {
|
|
|
1775
1806
|
this.log.info('Subscribed to all attributes and events');
|
|
1776
1807
|
}
|
|
1777
1808
|
}
|
|
1809
|
+
/** ***********************************************************************************************************************************/
|
|
1810
|
+
/** Matter.js methods */
|
|
1811
|
+
/** ***********************************************************************************************************************************/
|
|
1778
1812
|
/**
|
|
1779
|
-
* Starts the
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
* and starts the matter server when all plugins are loaded and started.
|
|
1784
|
-
* @private
|
|
1785
|
-
* @returns {Promise<void>} A promise that resolves when the Matterbridge is started.
|
|
1813
|
+
* Starts the matter storage process based on the specified storage type and name.
|
|
1814
|
+
* @param {string} storageType - The type of storage to start (e.g., 'disk', 'json').
|
|
1815
|
+
* @param {string} storageName - The name of the storage file.
|
|
1816
|
+
* @returns {Promise<void>} - A promise that resolves when the storage process is started.
|
|
1786
1817
|
*/
|
|
1787
|
-
async
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
this.
|
|
1792
|
-
let failCount = 0;
|
|
1793
|
-
const startMatterInterval = setInterval(async () => {
|
|
1794
|
-
for (const plugin of this.registeredPlugins) {
|
|
1795
|
-
// new code to not start the bridge if one plugin is in error cause the controllers will delete the devices loosing all the configuration
|
|
1796
|
-
if (!plugin.enabled)
|
|
1797
|
-
continue;
|
|
1798
|
-
if (plugin.error) {
|
|
1799
|
-
clearInterval(startMatterInterval);
|
|
1800
|
-
this.log.debug('***Cleared startMatterInterval interval for Matterbridge for plugin in error state');
|
|
1801
|
-
this.log.error(`The plugin ${plg}${plugin.name}${er} is in error state.`);
|
|
1802
|
-
this.log.error('The bridge will not start until the problem is solved to prevent the controllers from deleting all registered devices.');
|
|
1803
|
-
this.log.error('If you want to start the bridge disable the plugin in error state and restart.');
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
if (!plugin.loaded || !plugin.started) {
|
|
1807
|
-
this.log.debug(`***Waiting (failSafeCount=${failCount}/30) in startMatterInterval interval for plugin ${plg}${plugin.name}${db} loaded: ${plugin.loaded} started: ${plugin.started}...`);
|
|
1808
|
-
failCount++;
|
|
1809
|
-
if (failCount > 30) {
|
|
1810
|
-
this.log.error(`Error waiting for plugin ${plg}${plugin.name}${er} to load and start. Plugin is in error state.`);
|
|
1811
|
-
plugin.error = true;
|
|
1812
|
-
}
|
|
1813
|
-
return;
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
clearInterval(startMatterInterval);
|
|
1817
|
-
this.log.debug('***Cleared startMatterInterval interval for Matterbridge');
|
|
1818
|
-
await this.startMatterServer();
|
|
1819
|
-
this.log.info('Matter server started');
|
|
1820
|
-
// Configure the plugins
|
|
1821
|
-
/*
|
|
1822
|
-
for (const plugin of this.registeredPlugins) {
|
|
1823
|
-
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error) continue;
|
|
1824
|
-
try {
|
|
1825
|
-
this.configurePlugin(plugin); // No await do it asyncronously
|
|
1826
|
-
} catch (error) {
|
|
1827
|
-
plugin.error = true;
|
|
1828
|
-
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
*/
|
|
1832
|
-
// Show the QR code for commissioning or log the already commissioned message
|
|
1833
|
-
await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
|
|
1834
|
-
// Setting reachability to true
|
|
1835
|
-
setTimeout(() => {
|
|
1836
|
-
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
1837
|
-
if (this.commissioningServer)
|
|
1838
|
-
this.setCommissioningServerReachability(this.commissioningServer, true);
|
|
1839
|
-
if (this.matterAggregator)
|
|
1840
|
-
this.setAggregatorReachability(this.matterAggregator, true);
|
|
1841
|
-
}, 60 * 1000);
|
|
1842
|
-
}, 1000);
|
|
1818
|
+
async startStorage(storageType, storageName) {
|
|
1819
|
+
this.log.debug(`Starting ${storageType} storage ${CYAN}${storageName}${db}`);
|
|
1820
|
+
if (storageType === 'disk') {
|
|
1821
|
+
const storageDisk = new StorageBackendDisk(storageName);
|
|
1822
|
+
this.storageManager = new StorageManager(storageDisk);
|
|
1843
1823
|
}
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1824
|
+
else if (storageType === 'json') {
|
|
1825
|
+
if (!storageName.endsWith('.json'))
|
|
1826
|
+
storageName += '.json';
|
|
1827
|
+
const storageJson = new StorageBackendJsonFile(storageName);
|
|
1828
|
+
this.storageManager = new StorageManager(storageJson);
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
this.log.error(`Unsupported storage type ${storageType}`);
|
|
1832
|
+
await this.cleanup('Unsupported storage type');
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
await this.storageManager.initialize();
|
|
1837
|
+
this.log.debug('Storage initialized');
|
|
1838
|
+
if (storageType === 'json') {
|
|
1839
|
+
await this.backupJsonStorage(storageName, storageName.replace('.json', '') + '.backup.json');
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
catch (error) {
|
|
1843
|
+
this.log.error(`Storage initialize() error! The file .matterbridge/${storageName} may be corrupted.`);
|
|
1844
|
+
this.log.error(`Please delete it and rename ${storageName.replace('.json', '.backup.json')} to ${storageName} and try to restart Matterbridge.`);
|
|
1845
|
+
await this.cleanup('Storage initialize() error!');
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* Makes a backup copy of the specified matter JSON storage file.
|
|
1850
|
+
*
|
|
1851
|
+
* @param storageName - The name of the JSON storage file to be backed up.
|
|
1852
|
+
* @param backupName - The name of the backup file to be created.
|
|
1853
|
+
*/
|
|
1854
|
+
async backupJsonStorage(storageName, backupName) {
|
|
1855
|
+
try {
|
|
1856
|
+
this.log.debug(`Making backup copy of ${storageName}`);
|
|
1857
|
+
await fs.copyFile(storageName, backupName);
|
|
1858
|
+
this.log.debug(`Successfully backed up ${storageName} to ${backupName}`);
|
|
1859
|
+
}
|
|
1860
|
+
catch (err) {
|
|
1861
|
+
if (err instanceof Error && 'code' in err) {
|
|
1862
|
+
if (err.code === 'ENOENT') {
|
|
1863
|
+
this.log.info(`No existing file to back up for ${storageName}. This is expected on the first run.`);
|
|
1875
1864
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
clearInterval(startMatterInterval);
|
|
1879
|
-
this.log.debug('***Cleared startMatterInterval interval in childbridge mode');
|
|
1880
|
-
await this.startMatterServer();
|
|
1881
|
-
this.log.info('Matter server started');
|
|
1882
|
-
for (const plugin of this.registeredPlugins) {
|
|
1883
|
-
if (!plugin.enabled || plugin.error)
|
|
1884
|
-
continue;
|
|
1885
|
-
if (!plugin.addedDevices || plugin.addedDevices === 0) {
|
|
1886
|
-
this.log.error(`Plugin ${plg}${plugin.name}${er} didn't add any devices to Matterbridge. Verify the plugin configuration.`);
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
if (!plugin.commissioningServer) {
|
|
1890
|
-
this.log.error(`Commissioning server not found for plugin ${plg}${plugin.name}${er}`);
|
|
1891
|
-
continue;
|
|
1892
|
-
}
|
|
1893
|
-
if (!plugin.storageContext) {
|
|
1894
|
-
this.log.error(`Storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1895
|
-
continue;
|
|
1896
|
-
}
|
|
1897
|
-
if (!plugin.nodeContext) {
|
|
1898
|
-
this.log.error(`Node storage context not found for plugin ${plg}${plugin.name}${er}`);
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
1901
|
-
await this.showCommissioningQRCode(plugin.commissioningServer, plugin.storageContext, plugin.nodeContext, plugin.name);
|
|
1902
|
-
// Setting reachability to true
|
|
1903
|
-
setTimeout(() => {
|
|
1904
|
-
this.log.info(`Setting reachability to true for ${plg}${plugin.name}${db}`);
|
|
1905
|
-
if (plugin.commissioningServer)
|
|
1906
|
-
this.setCommissioningServerReachability(plugin.commissioningServer, true);
|
|
1907
|
-
if (plugin.type === 'AccessoryPlatform' && plugin.device)
|
|
1908
|
-
this.setDeviceReachability(plugin.device, true);
|
|
1909
|
-
if (plugin.type === 'DynamicPlatform' && plugin.aggregator)
|
|
1910
|
-
this.setAggregatorReachability(plugin.aggregator, true);
|
|
1911
|
-
}, 60 * 1000);
|
|
1865
|
+
else {
|
|
1866
|
+
this.log.error(`Error making backup copy of ${storageName}: ${err.message}`);
|
|
1912
1867
|
}
|
|
1913
|
-
}
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
this.log.error(`An unexpected error occurred during the backup of ${storageName}: ${String(err)}`);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Stops the matter storage.
|
|
1876
|
+
* @returns {Promise<void>} A promise that resolves when the storage is stopped.
|
|
1877
|
+
*/
|
|
1878
|
+
async stopStorage() {
|
|
1879
|
+
this.log.debug('Stopping storage');
|
|
1880
|
+
await this.storageManager?.close();
|
|
1881
|
+
this.log.debug('Storage closed');
|
|
1882
|
+
this.storageManager = undefined;
|
|
1883
|
+
this.matterbridgeContext = undefined;
|
|
1884
|
+
this.mattercontrollerContext = undefined;
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Creates a Matter server using the provided storage manager and the provided mdnsInterface.
|
|
1888
|
+
* @param storageManager The storage manager to be used by the Matter server.
|
|
1889
|
+
*
|
|
1890
|
+
*/
|
|
1891
|
+
createMatterServer(storageManager) {
|
|
1892
|
+
this.log.debug('Creating matter server');
|
|
1893
|
+
// Validate mdnsInterface
|
|
1894
|
+
if (this.mdnsInterface) {
|
|
1895
|
+
const networkInterfaces = os.networkInterfaces();
|
|
1896
|
+
const availableInterfaces = Object.keys(networkInterfaces);
|
|
1897
|
+
if (!availableInterfaces.includes(this.mdnsInterface)) {
|
|
1898
|
+
this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
|
|
1899
|
+
this.mdnsInterface = undefined;
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
|
|
1903
|
+
}
|
|
1914
1904
|
}
|
|
1905
|
+
const matterServer = new MatterServer(storageManager, { mdnsInterface: this.mdnsInterface });
|
|
1906
|
+
this.log.debug('Created matter server');
|
|
1907
|
+
return matterServer;
|
|
1915
1908
|
}
|
|
1916
1909
|
/**
|
|
1917
1910
|
* Starts the Matter server.
|
|
@@ -1929,69 +1922,234 @@ export class Matterbridge extends EventEmitter {
|
|
|
1929
1922
|
// this.commissioningServer?.getRootEndpoint() && logEndpoint(this.commissioningServer?.getRootEndpoint());
|
|
1930
1923
|
}
|
|
1931
1924
|
/**
|
|
1932
|
-
*
|
|
1933
|
-
* @param pluginName - The name of the plugin.
|
|
1934
|
-
* @param device - The MatterbridgeDevice object representing the device.
|
|
1935
|
-
* @returns The commissioning server context.
|
|
1936
|
-
* @throws Error if the BasicInformationCluster is not found.
|
|
1925
|
+
* Stops the Matter server, commissioningServer and commissioningController.
|
|
1937
1926
|
*/
|
|
1938
|
-
async
|
|
1939
|
-
this.log.debug(
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
this.
|
|
1950
|
-
const storageContext = this.storageManager.createContext(pluginName);
|
|
1951
|
-
await storageContext.set('deviceName', basic.getNodeLabelAttribute());
|
|
1952
|
-
await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
|
|
1953
|
-
await storageContext.set('vendorId', basic.getVendorIdAttribute());
|
|
1954
|
-
await storageContext.set('vendorName', basic.getVendorNameAttribute());
|
|
1955
|
-
await storageContext.set('productId', basic.getProductIdAttribute());
|
|
1956
|
-
await storageContext.set('productName', basic.getProductNameAttribute());
|
|
1957
|
-
await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
|
|
1958
|
-
await storageContext.set('productLabel', basic.getNodeLabelAttribute());
|
|
1959
|
-
await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
|
|
1960
|
-
await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
|
|
1961
|
-
await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
|
|
1962
|
-
await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
|
|
1963
|
-
await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
|
|
1964
|
-
await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
|
|
1965
|
-
this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
|
|
1966
|
-
this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
|
|
1967
|
-
this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
1968
|
-
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
1969
|
-
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
1970
|
-
return storageContext;
|
|
1927
|
+
async stopMatterServer() {
|
|
1928
|
+
this.log.debug('Stopping matter commissioningServer');
|
|
1929
|
+
await this.commissioningServer?.close();
|
|
1930
|
+
this.log.debug('Stopping matter commissioningController');
|
|
1931
|
+
await this.commissioningController?.close();
|
|
1932
|
+
this.log.debug('Stopping matter server');
|
|
1933
|
+
await this.matterServer?.close();
|
|
1934
|
+
this.log.debug('Matter server closed');
|
|
1935
|
+
this.commissioningController = undefined;
|
|
1936
|
+
this.commissioningServer = undefined;
|
|
1937
|
+
this.matterAggregator = undefined;
|
|
1938
|
+
this.matterServer = undefined;
|
|
1971
1939
|
}
|
|
1972
1940
|
/**
|
|
1973
|
-
* Creates a
|
|
1941
|
+
* Creates a Matter Aggregator.
|
|
1942
|
+
* @param {StorageContext} context - The storage context.
|
|
1943
|
+
* @returns {Aggregator} - The created Matter Aggregator.
|
|
1944
|
+
*/
|
|
1945
|
+
async createMatterAggregator(context, pluginName) {
|
|
1946
|
+
const random = 'AG' + CryptoNode.getRandomData(8).toHex();
|
|
1947
|
+
await context.set('aggregatorSerialNumber', await context.get('aggregatorSerialNumber', random));
|
|
1948
|
+
await context.set('aggregatorUniqueId', await context.get('aggregatorUniqueId', random));
|
|
1949
|
+
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with uniqueId ${await context.get('aggregatorUniqueId')} serialNumber ${await context.get('aggregatorSerialNumber')}`);
|
|
1950
|
+
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with softwareVersion ${await context.get('softwareVersion', 1)} softwareVersionString ${await context.get('softwareVersionString', '1.0.0')}`);
|
|
1951
|
+
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with hardwareVersion ${await context.get('hardwareVersion', 1)} hardwareVersionString ${await context.get('hardwareVersionString', '1.0.0')}`);
|
|
1952
|
+
const matterAggregator = new Aggregator();
|
|
1953
|
+
matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
|
|
1954
|
+
dataModelRevision: 1,
|
|
1955
|
+
location: 'FR',
|
|
1956
|
+
vendorId: VendorId(0xfff1),
|
|
1957
|
+
vendorName: 'Matterbridge',
|
|
1958
|
+
productId: 0x8000,
|
|
1959
|
+
productName: 'Matterbridge aggregator',
|
|
1960
|
+
productLabel: 'Matterbridge aggregator',
|
|
1961
|
+
nodeLabel: 'Matterbridge aggregator',
|
|
1962
|
+
serialNumber: await context.get('aggregatorSerialNumber'),
|
|
1963
|
+
uniqueId: await context.get('aggregatorUniqueId'),
|
|
1964
|
+
softwareVersion: await context.get('softwareVersion', 1),
|
|
1965
|
+
softwareVersionString: await context.get('softwareVersionString', '1.0.0'),
|
|
1966
|
+
hardwareVersion: await context.get('hardwareVersion', 1),
|
|
1967
|
+
hardwareVersionString: await context.get('hardwareVersionString', '1.0.0'),
|
|
1968
|
+
reachable: true,
|
|
1969
|
+
capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
|
|
1970
|
+
}, {}, {
|
|
1971
|
+
startUp: true,
|
|
1972
|
+
shutDown: true,
|
|
1973
|
+
leave: true,
|
|
1974
|
+
reachableChanged: true,
|
|
1975
|
+
}));
|
|
1976
|
+
return matterAggregator;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Creates a matter commissioning server.
|
|
1974
1980
|
*
|
|
1975
|
-
* @param
|
|
1976
|
-
* @param
|
|
1977
|
-
* @
|
|
1978
|
-
* @param vendorId - The vendor ID.
|
|
1979
|
-
* @param vendorName - The vendor name.
|
|
1980
|
-
* @param productId - The product ID.
|
|
1981
|
-
* @param productName - The product name.
|
|
1982
|
-
* @param serialNumber - The serial number of the device (optional).
|
|
1983
|
-
* @param uniqueId - The unique ID of the device (optional).
|
|
1984
|
-
* @param softwareVersion - The software version of the device (optional).
|
|
1985
|
-
* @param softwareVersionString - The software version string of the device (optional).
|
|
1986
|
-
* @param hardwareVersion - The hardware version of the device (optional).
|
|
1987
|
-
* @param hardwareVersionString - The hardware version string of the device (optional).
|
|
1988
|
-
* @returns The storage context for the commissioning server.
|
|
1981
|
+
* @param {StorageContext} context - The storage context.
|
|
1982
|
+
* @param {string} pluginName - The name of the commissioning server.
|
|
1983
|
+
* @returns {CommissioningServer} The created commissioning server.
|
|
1989
1984
|
*/
|
|
1990
|
-
async
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1985
|
+
async createCommisioningServer(context, pluginName) {
|
|
1986
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
|
|
1987
|
+
const deviceName = await context.get('deviceName');
|
|
1988
|
+
const deviceType = await context.get('deviceType');
|
|
1989
|
+
const vendorId = await context.get('vendorId');
|
|
1990
|
+
const vendorName = await context.get('vendorName'); // Home app = Manufacturer
|
|
1991
|
+
const productId = await context.get('productId');
|
|
1992
|
+
const productName = await context.get('productName'); // Home app = Model
|
|
1993
|
+
const serialNumber = await context.get('serialNumber');
|
|
1994
|
+
const uniqueId = await context.get('uniqueId');
|
|
1995
|
+
const softwareVersion = await context.get('softwareVersion', 1);
|
|
1996
|
+
const softwareVersionString = await context.get('softwareVersionString', '1.0.0'); // Home app = Firmware Revision
|
|
1997
|
+
const hardwareVersion = await context.get('hardwareVersion', 1);
|
|
1998
|
+
const hardwareVersionString = await context.get('hardwareVersionString', '1.0.0');
|
|
1999
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with deviceName '${deviceName}' deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')})`);
|
|
2000
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with uniqueId ${uniqueId} serialNumber ${serialNumber}`);
|
|
2001
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with softwareVersion ${softwareVersion} softwareVersionString ${softwareVersionString}`);
|
|
2002
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with hardwareVersion ${hardwareVersion} hardwareVersionString ${hardwareVersionString}`);
|
|
2003
|
+
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with nodeLabel '${productName}' port ${this.port} passcode ${this.passcode} discriminator ${this.discriminator}`);
|
|
2004
|
+
const commissioningServer = new CommissioningServer({
|
|
2005
|
+
port: this.port++,
|
|
2006
|
+
// listeningAddressIpv4
|
|
2007
|
+
// listeningAddressIpv6
|
|
2008
|
+
passcode: this.passcode,
|
|
2009
|
+
discriminator: this.discriminator,
|
|
2010
|
+
deviceName,
|
|
2011
|
+
deviceType,
|
|
2012
|
+
basicInformation: {
|
|
2013
|
+
vendorId: VendorId(vendorId),
|
|
2014
|
+
vendorName,
|
|
2015
|
+
productId,
|
|
2016
|
+
productName,
|
|
2017
|
+
nodeLabel: productName,
|
|
2018
|
+
productLabel: productName,
|
|
2019
|
+
softwareVersion,
|
|
2020
|
+
softwareVersionString, // Home app = Firmware Revision
|
|
2021
|
+
hardwareVersion,
|
|
2022
|
+
hardwareVersionString,
|
|
2023
|
+
uniqueId,
|
|
2024
|
+
serialNumber,
|
|
2025
|
+
reachable: true,
|
|
2026
|
+
},
|
|
2027
|
+
activeSessionsChangedCallback: (fabricIndex) => {
|
|
2028
|
+
const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
|
|
2029
|
+
let connected = false;
|
|
2030
|
+
sessionInformations.forEach((session) => {
|
|
2031
|
+
this.log.info(`*Active session changed on fabric ${zb}${fabricIndex}${nf} id ${zb}${session.fabric?.fabricId}${nf} vendor ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
|
|
2032
|
+
if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
|
|
2033
|
+
this.log.info(`*Controller ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} connected to ${plg}${pluginName}${nf} on session ${session.name}`);
|
|
2034
|
+
connected = true;
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
if (connected) {
|
|
2038
|
+
if (this.bridgeMode === 'bridge') {
|
|
2039
|
+
this.matterbridgePaired = true;
|
|
2040
|
+
this.matterbridgeConnected = true;
|
|
2041
|
+
this.matterbridgeSessionInformations = this.sanitizeSessionInformation(sessionInformations);
|
|
2042
|
+
}
|
|
2043
|
+
if (this.bridgeMode === 'childbridge') {
|
|
2044
|
+
const plugin = this.plugins.get(pluginName);
|
|
2045
|
+
if (plugin) {
|
|
2046
|
+
plugin.paired = true;
|
|
2047
|
+
plugin.connected = true;
|
|
2048
|
+
plugin.sessionInformations = this.sanitizeSessionInformation(sessionInformations);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/*
|
|
2052
|
+
setTimeout(() => {
|
|
2053
|
+
// We just need to configure the plugins after the controllers are connected
|
|
2054
|
+
if (this.bridgeMode === 'bridge') {
|
|
2055
|
+
for (const plugin of this.plugins) {
|
|
2056
|
+
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error) continue;
|
|
2057
|
+
try {
|
|
2058
|
+
this.configurePlugin(plugin); // No await do it asyncronously
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
plugin.error = true;
|
|
2061
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
if (this.bridgeMode === 'childbridge') {
|
|
2066
|
+
for (const plugin of this.plugins) {
|
|
2067
|
+
if (plugin.name === pluginName && plugin.loaded === true && plugin.started === true && plugin.configured !== true) {
|
|
2068
|
+
try {
|
|
2069
|
+
this.configurePlugin(plugin); // No await do it asyncronously
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
plugin.error = true;
|
|
2072
|
+
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
// logEndpoint(commissioningServer.getRootEndpoint());
|
|
2078
|
+
}, 2000);
|
|
2079
|
+
*/
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
commissioningChangedCallback: async (fabricIndex) => {
|
|
2083
|
+
const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
|
|
2084
|
+
this.log.debug(`*Commissioning changed on fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${nf}`, debugStringify(fabricInfo));
|
|
2085
|
+
if (commissioningServer.getCommissionedFabricInformation().length === 0) {
|
|
2086
|
+
this.log.warn(`*Commissioning removed from fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
|
|
2087
|
+
await commissioningServer.factoryReset();
|
|
2088
|
+
if (pluginName === 'Matterbridge') {
|
|
2089
|
+
await this.matterbridgeContext?.clearAll();
|
|
2090
|
+
this.matterbridgeFabricInformations = [];
|
|
2091
|
+
this.matterbridgeSessionInformations = [];
|
|
2092
|
+
this.matterbridgePaired = false;
|
|
2093
|
+
this.matterbridgeConnected = false;
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
for (const plugin of this.plugins) {
|
|
2097
|
+
if (plugin.name === pluginName) {
|
|
2098
|
+
await plugin.platform?.onShutdown('Commissioning removed by the controller');
|
|
2099
|
+
plugin.fabricInformations = [];
|
|
2100
|
+
plugin.sessionInformations = [];
|
|
2101
|
+
plugin.paired = false;
|
|
2102
|
+
plugin.connected = false;
|
|
2103
|
+
await plugin.storageContext?.clearAll();
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
|
|
2108
|
+
}
|
|
2109
|
+
else {
|
|
2110
|
+
const fabricInfo = commissioningServer.getCommissionedFabricInformation();
|
|
2111
|
+
if (pluginName === 'Matterbridge') {
|
|
2112
|
+
this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
|
|
2113
|
+
this.matterbridgePaired = true;
|
|
2114
|
+
}
|
|
2115
|
+
else {
|
|
2116
|
+
const plugin = this.plugins.get(pluginName);
|
|
2117
|
+
if (plugin) {
|
|
2118
|
+
plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
|
|
2119
|
+
plugin.paired = true;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
},
|
|
2124
|
+
});
|
|
2125
|
+
if (this.passcode !== undefined)
|
|
2126
|
+
this.passcode++;
|
|
2127
|
+
if (this.discriminator !== undefined)
|
|
2128
|
+
this.discriminator++;
|
|
2129
|
+
commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
|
|
2130
|
+
return commissioningServer;
|
|
2131
|
+
}
|
|
2132
|
+
/**
|
|
2133
|
+
* Creates a commissioning server storage context.
|
|
2134
|
+
*
|
|
2135
|
+
* @param pluginName - The name of the plugin.
|
|
2136
|
+
* @param deviceName - The name of the device.
|
|
2137
|
+
* @param deviceType - The type of the device.
|
|
2138
|
+
* @param vendorId - The vendor ID.
|
|
2139
|
+
* @param vendorName - The vendor name.
|
|
2140
|
+
* @param productId - The product ID.
|
|
2141
|
+
* @param productName - The product name.
|
|
2142
|
+
* @param serialNumber - The serial number of the device (optional).
|
|
2143
|
+
* @param uniqueId - The unique ID of the device (optional).
|
|
2144
|
+
* @param softwareVersion - The software version of the device (optional).
|
|
2145
|
+
* @param softwareVersionString - The software version string of the device (optional).
|
|
2146
|
+
* @param hardwareVersion - The hardware version of the device (optional).
|
|
2147
|
+
* @param hardwareVersionString - The hardware version string of the device (optional).
|
|
2148
|
+
* @returns The storage context for the commissioning server.
|
|
2149
|
+
*/
|
|
2150
|
+
async createCommissioningServerContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName) {
|
|
2151
|
+
if (!this.storageManager)
|
|
2152
|
+
throw new Error('No storage manager initialized');
|
|
1995
2153
|
this.log.debug(`Creating commissioning server storage context for ${plg}${pluginName}${db}`);
|
|
1996
2154
|
const random = 'CS' + CryptoNode.getRandomData(8).toHex();
|
|
1997
2155
|
const storageContext = this.storageManager.createContext(pluginName);
|
|
@@ -2017,11 +2175,52 @@ export class Matterbridge extends EventEmitter {
|
|
|
2017
2175
|
return storageContext;
|
|
2018
2176
|
}
|
|
2019
2177
|
/**
|
|
2020
|
-
*
|
|
2178
|
+
* Imports the commissioning server context for a specific plugin and device.
|
|
2179
|
+
* @param pluginName - The name of the plugin.
|
|
2180
|
+
* @param device - The MatterbridgeDevice object representing the device.
|
|
2181
|
+
* @returns The commissioning server context.
|
|
2182
|
+
* @throws Error if the BasicInformationCluster is not found.
|
|
2183
|
+
*/
|
|
2184
|
+
async importCommissioningServerContext(pluginName, device) {
|
|
2185
|
+
this.log.debug(`Importing matter commissioning server storage context from device for ${plg}${pluginName}${db}`);
|
|
2186
|
+
const basic = device.getClusterServer(BasicInformationCluster);
|
|
2187
|
+
if (!basic) {
|
|
2188
|
+
this.log.error('importCommissioningServerContext error: cannot find the BasicInformationCluster');
|
|
2189
|
+
process.exit(1);
|
|
2190
|
+
}
|
|
2191
|
+
if (!this.storageManager) {
|
|
2192
|
+
this.log.error('importCommissioningServerContext error: no storage manager initialized');
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
this.log.debug(`Importing commissioning server storage context for ${plg}${pluginName}${db}`);
|
|
2196
|
+
const storageContext = this.storageManager.createContext(pluginName);
|
|
2197
|
+
await storageContext.set('deviceName', basic.getNodeLabelAttribute());
|
|
2198
|
+
await storageContext.set('deviceType', DeviceTypeId(device.deviceType));
|
|
2199
|
+
await storageContext.set('vendorId', basic.getVendorIdAttribute());
|
|
2200
|
+
await storageContext.set('vendorName', basic.getVendorNameAttribute());
|
|
2201
|
+
await storageContext.set('productId', basic.getProductIdAttribute());
|
|
2202
|
+
await storageContext.set('productName', basic.getProductNameAttribute());
|
|
2203
|
+
await storageContext.set('nodeLabel', basic.getNodeLabelAttribute());
|
|
2204
|
+
await storageContext.set('productLabel', basic.getNodeLabelAttribute());
|
|
2205
|
+
await storageContext.set('serialNumber', basic.attributes.serialNumber?.getLocal());
|
|
2206
|
+
await storageContext.set('uniqueId', basic.attributes.uniqueId?.getLocal());
|
|
2207
|
+
await storageContext.set('softwareVersion', basic.getSoftwareVersionAttribute());
|
|
2208
|
+
await storageContext.set('softwareVersionString', basic.getSoftwareVersionStringAttribute());
|
|
2209
|
+
await storageContext.set('hardwareVersion', basic.getHardwareVersionAttribute());
|
|
2210
|
+
await storageContext.set('hardwareVersionString', basic.getHardwareVersionStringAttribute());
|
|
2211
|
+
this.log.debug(`Imported commissioning server storage context for ${plg}${pluginName}${db}`);
|
|
2212
|
+
this.log.debug(`- deviceName: ${await storageContext.get('deviceName')} deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
|
|
2213
|
+
this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')} uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
2214
|
+
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
2215
|
+
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
2216
|
+
return storageContext;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* Shows the commissioning server QR code for a given plugin.
|
|
2021
2220
|
* @param {CommissioningServer} commissioningServer - The commissioning server instance.
|
|
2022
2221
|
* @param {StorageContext} storageContext - The storage context instance.
|
|
2023
2222
|
* @param {NodeStorage} nodeContext - The node storage instance.
|
|
2024
|
-
* @param {string} pluginName - The name of the plugin.
|
|
2223
|
+
* @param {string} pluginName - The name of the plugin of Matterbridge in bridge mode.
|
|
2025
2224
|
* @returns {Promise<void>} - A promise that resolves when the QR code is shown.
|
|
2026
2225
|
*/
|
|
2027
2226
|
async showCommissioningQRCode(commissioningServer, storageContext, nodeContext, pluginName) {
|
|
@@ -2037,8 +2236,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
2037
2236
|
await nodeContext.set('qrPairingCode', qrPairingCode);
|
|
2038
2237
|
await nodeContext.set('manualPairingCode', manualPairingCode);
|
|
2039
2238
|
const QrCode = new QrCodeSchema();
|
|
2040
|
-
this.log.info(
|
|
2041
|
-
|
|
2239
|
+
this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is not commissioned. Pair it scanning the QR code:\n\n`);
|
|
2240
|
+
// eslint-disable-next-line no-console
|
|
2241
|
+
console.log(`${QrCode.encode(qrPairingCode)}\n`);
|
|
2242
|
+
this.log.info(`${plg}${pluginName}${nf}\n\nqrPairingCode: ${qrPairingCode}\n\nManual pairing code: ${manualPairingCode}\n`);
|
|
2042
2243
|
if (pluginName === 'Matterbridge') {
|
|
2043
2244
|
this.matterbridgeFabricInformations = [];
|
|
2044
2245
|
this.matterbridgeSessionInformations = [];
|
|
@@ -2046,7 +2247,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
2046
2247
|
this.matterbridgeConnected = false;
|
|
2047
2248
|
}
|
|
2048
2249
|
if (pluginName !== 'Matterbridge') {
|
|
2049
|
-
const plugin = this.
|
|
2250
|
+
const plugin = this.plugins.get(pluginName);
|
|
2050
2251
|
if (plugin) {
|
|
2051
2252
|
plugin.qrPairingCode = qrPairingCode;
|
|
2052
2253
|
plugin.manualPairingCode = manualPairingCode;
|
|
@@ -2056,7 +2257,6 @@ export class Matterbridge extends EventEmitter {
|
|
|
2056
2257
|
plugin.connected = false;
|
|
2057
2258
|
}
|
|
2058
2259
|
}
|
|
2059
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
2060
2260
|
}
|
|
2061
2261
|
else {
|
|
2062
2262
|
this.log.info(`*The commissioning server on port ${commissioningServer.getPort()} for ${plg}${pluginName}${nf} is already commissioned. Waiting for controllers to connect ...`);
|
|
@@ -2072,14 +2272,13 @@ export class Matterbridge extends EventEmitter {
|
|
|
2072
2272
|
this.matterbridgePaired = true;
|
|
2073
2273
|
}
|
|
2074
2274
|
if (pluginName !== 'Matterbridge') {
|
|
2075
|
-
const plugin = this.
|
|
2275
|
+
const plugin = this.plugins.get(pluginName);
|
|
2076
2276
|
if (plugin) {
|
|
2077
2277
|
plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
|
|
2078
2278
|
plugin.sessionInformations = [];
|
|
2079
2279
|
plugin.paired = true;
|
|
2080
2280
|
}
|
|
2081
2281
|
}
|
|
2082
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
2083
2282
|
}
|
|
2084
2283
|
}
|
|
2085
2284
|
/**
|
|
@@ -2126,26 +2325,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
2126
2325
|
: undefined,
|
|
2127
2326
|
isPeerActive: info.isPeerActive,
|
|
2128
2327
|
secure: info.secure,
|
|
2129
|
-
lastInteractionTimestamp: info.lastInteractionTimestamp,
|
|
2130
|
-
lastActiveTimestamp: info.lastActiveTimestamp,
|
|
2328
|
+
lastInteractionTimestamp: info.lastInteractionTimestamp?.toString(),
|
|
2329
|
+
lastActiveTimestamp: info.lastActiveTimestamp?.toString(),
|
|
2131
2330
|
numberOfActiveSubscriptions: info.numberOfActiveSubscriptions,
|
|
2132
2331
|
};
|
|
2133
2332
|
});
|
|
2134
2333
|
}
|
|
2135
|
-
/**
|
|
2136
|
-
* Finds a plugin by its name.
|
|
2137
|
-
*
|
|
2138
|
-
* @param pluginName - The name of the plugin to find.
|
|
2139
|
-
* @returns The found plugin, or undefined if not found.
|
|
2140
|
-
*/
|
|
2141
|
-
findPlugin(pluginName) {
|
|
2142
|
-
const plugin = this.registeredPlugins.find((registeredPlugin) => registeredPlugin.name === pluginName);
|
|
2143
|
-
if (!plugin) {
|
|
2144
|
-
this.log.error(`Plugin ${plg}${pluginName}${er} not found`);
|
|
2145
|
-
return;
|
|
2146
|
-
}
|
|
2147
|
-
return plugin;
|
|
2148
|
-
}
|
|
2149
2334
|
/**
|
|
2150
2335
|
* Sets the reachability of a commissioning server and trigger.
|
|
2151
2336
|
*
|
|
@@ -2154,562 +2339,87 @@ export class Matterbridge extends EventEmitter {
|
|
|
2154
2339
|
*/
|
|
2155
2340
|
setCommissioningServerReachability(commissioningServer, reachable) {
|
|
2156
2341
|
const basicInformationCluster = commissioningServer?.getRootClusterServer(BasicInformationCluster);
|
|
2157
|
-
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2158
|
-
basicInformationCluster.setReachableAttribute(reachable);
|
|
2159
|
-
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2160
|
-
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2161
|
-
}
|
|
2162
|
-
/**
|
|
2163
|
-
* Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
|
|
2164
|
-
* @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
|
|
2165
|
-
* @param {boolean} reachable - A boolean indicating the reachability status to set.
|
|
2166
|
-
*/
|
|
2167
|
-
setAggregatorReachability(matterAggregator, reachable) {
|
|
2168
|
-
const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
|
|
2169
|
-
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2170
|
-
basicInformationCluster.setReachableAttribute(reachable);
|
|
2171
|
-
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2172
|
-
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2173
|
-
matterAggregator.getBridgedDevices().forEach((device) => {
|
|
2174
|
-
this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
|
|
2175
|
-
device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
|
|
2176
|
-
device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
/**
|
|
2180
|
-
* Sets the reachability of a device and trigger.
|
|
2181
|
-
*
|
|
2182
|
-
* @param {MatterbridgeDevice} device - The device to set the reachability for.
|
|
2183
|
-
* @param {boolean} reachable - The new reachability status of the device.
|
|
2184
|
-
*/
|
|
2185
|
-
setDeviceReachability(device, reachable) {
|
|
2186
|
-
const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
|
|
2187
|
-
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2188
|
-
basicInformationCluster.setReachableAttribute(reachable);
|
|
2189
|
-
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2190
|
-
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2191
|
-
}
|
|
2192
|
-
getVendorIdName = (vendorId) => {
|
|
2193
|
-
if (!vendorId)
|
|
2194
|
-
return '';
|
|
2195
|
-
let vendorName = '';
|
|
2196
|
-
switch (vendorId) {
|
|
2197
|
-
case 4937:
|
|
2198
|
-
vendorName = '(AppleHome)';
|
|
2199
|
-
break;
|
|
2200
|
-
case 4996:
|
|
2201
|
-
vendorName = '(AppleKeyChain)';
|
|
2202
|
-
break;
|
|
2203
|
-
case 4362:
|
|
2204
|
-
vendorName = '(SmartThings)';
|
|
2205
|
-
break;
|
|
2206
|
-
case 4939:
|
|
2207
|
-
vendorName = '(HomeAssistant)';
|
|
2208
|
-
break;
|
|
2209
|
-
case 24582:
|
|
2210
|
-
vendorName = '(GoogleHome)';
|
|
2211
|
-
break;
|
|
2212
|
-
case 4631:
|
|
2213
|
-
vendorName = '(Alexa)';
|
|
2214
|
-
break;
|
|
2215
|
-
case 4701:
|
|
2216
|
-
vendorName = '(Tuya)';
|
|
2217
|
-
break;
|
|
2218
|
-
case 4742:
|
|
2219
|
-
vendorName = '(eWeLink)';
|
|
2220
|
-
break;
|
|
2221
|
-
case 65521:
|
|
2222
|
-
vendorName = '(PythonMatterServer)';
|
|
2223
|
-
break;
|
|
2224
|
-
default:
|
|
2225
|
-
vendorName = '(unknown)';
|
|
2226
|
-
break;
|
|
2227
|
-
}
|
|
2228
|
-
return vendorName;
|
|
2229
|
-
};
|
|
2230
|
-
/**
|
|
2231
|
-
* Creates a matter commissioning server.
|
|
2232
|
-
*
|
|
2233
|
-
* @param {StorageContext} context - The storage context.
|
|
2234
|
-
* @param {string} pluginName - The name of the commissioning server.
|
|
2235
|
-
* @returns {CommissioningServer} The created commissioning server.
|
|
2236
|
-
*/
|
|
2237
|
-
async createCommisioningServer(context, pluginName) {
|
|
2238
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db}`);
|
|
2239
|
-
const deviceName = await context.get('deviceName');
|
|
2240
|
-
const deviceType = await context.get('deviceType');
|
|
2241
|
-
const vendorId = await context.get('vendorId');
|
|
2242
|
-
const vendorName = await context.get('vendorName'); // Home app = Manufacturer
|
|
2243
|
-
const productId = await context.get('productId');
|
|
2244
|
-
const productName = await context.get('productName'); // Home app = Model
|
|
2245
|
-
const serialNumber = await context.get('serialNumber');
|
|
2246
|
-
const uniqueId = await context.get('uniqueId');
|
|
2247
|
-
const softwareVersion = await context.get('softwareVersion', 1);
|
|
2248
|
-
const softwareVersionString = await context.get('softwareVersionString', '1.0.0'); // Home app = Firmware Revision
|
|
2249
|
-
const hardwareVersion = await context.get('hardwareVersion', 1);
|
|
2250
|
-
const hardwareVersionString = await context.get('hardwareVersionString', '1.0.0');
|
|
2251
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with deviceName '${deviceName}' deviceType ${deviceType}(0x${deviceType.toString(16).padStart(4, '0')})`);
|
|
2252
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with uniqueId ${uniqueId} serialNumber ${serialNumber}`);
|
|
2253
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with softwareVersion ${softwareVersion} softwareVersionString ${softwareVersionString}`);
|
|
2254
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with hardwareVersion ${hardwareVersion} hardwareVersionString ${hardwareVersionString}`);
|
|
2255
|
-
this.log.debug(`Creating matter commissioning server for plugin ${plg}${pluginName}${db} with nodeLabel '${productName}' port ${this.port} passcode ${this.passcode} discriminator ${this.discriminator}`);
|
|
2256
|
-
const commissioningServer = new CommissioningServer({
|
|
2257
|
-
port: this.port++,
|
|
2258
|
-
// listeningAddressIpv4
|
|
2259
|
-
// listeningAddressIpv6
|
|
2260
|
-
passcode: this.passcode,
|
|
2261
|
-
discriminator: this.discriminator,
|
|
2262
|
-
deviceName,
|
|
2263
|
-
deviceType,
|
|
2264
|
-
basicInformation: {
|
|
2265
|
-
vendorId: VendorId(vendorId),
|
|
2266
|
-
vendorName,
|
|
2267
|
-
productId,
|
|
2268
|
-
productName,
|
|
2269
|
-
nodeLabel: productName,
|
|
2270
|
-
productLabel: productName,
|
|
2271
|
-
softwareVersion,
|
|
2272
|
-
softwareVersionString, // Home app = Firmware Revision
|
|
2273
|
-
hardwareVersion,
|
|
2274
|
-
hardwareVersionString,
|
|
2275
|
-
uniqueId,
|
|
2276
|
-
serialNumber,
|
|
2277
|
-
reachable: true,
|
|
2278
|
-
},
|
|
2279
|
-
activeSessionsChangedCallback: (fabricIndex) => {
|
|
2280
|
-
const sessionInformations = commissioningServer.getActiveSessionInformation(fabricIndex);
|
|
2281
|
-
let connected = false;
|
|
2282
|
-
sessionInformations.forEach((session) => {
|
|
2283
|
-
this.log.info(`*Active session changed on fabric ${zb}${fabricIndex}${nf} id ${zb}${session.fabric?.fabricId}${nf} vendor ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} for ${plg}${pluginName}${nf}`, debugStringify(session));
|
|
2284
|
-
if (session.isPeerActive === true && session.secure === true && session.numberOfActiveSubscriptions >= 1) {
|
|
2285
|
-
this.log.info(`*Controller ${zb}${session.fabric?.rootVendorId}${nf} ${this.getVendorIdName(session.fabric?.rootVendorId)} ${session.fabric?.label} connected to ${plg}${pluginName}${nf} on session ${session.name}`);
|
|
2286
|
-
connected = true;
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
if (connected) {
|
|
2290
|
-
if (this.bridgeMode === 'bridge') {
|
|
2291
|
-
this.matterbridgePaired = true;
|
|
2292
|
-
this.matterbridgeConnected = true;
|
|
2293
|
-
this.matterbridgeSessionInformations = this.sanitizeSessionInformation(sessionInformations);
|
|
2294
|
-
}
|
|
2295
|
-
if (this.bridgeMode === 'childbridge') {
|
|
2296
|
-
const plugin = this.findPlugin(pluginName);
|
|
2297
|
-
if (plugin) {
|
|
2298
|
-
plugin.paired = true;
|
|
2299
|
-
plugin.connected = true;
|
|
2300
|
-
plugin.sessionInformations = this.sanitizeSessionInformation(sessionInformations);
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
setTimeout(() => {
|
|
2304
|
-
// We just need to configure the plugins after the controllers are connected
|
|
2305
|
-
if (this.bridgeMode === 'bridge') {
|
|
2306
|
-
for (const plugin of this.registeredPlugins) {
|
|
2307
|
-
if (!plugin.enabled || !plugin.loaded || !plugin.started || plugin.error)
|
|
2308
|
-
continue;
|
|
2309
|
-
try {
|
|
2310
|
-
this.configurePlugin(plugin); // No await do it asyncronously
|
|
2311
|
-
}
|
|
2312
|
-
catch (error) {
|
|
2313
|
-
plugin.error = true;
|
|
2314
|
-
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
if (this.bridgeMode === 'childbridge') {
|
|
2319
|
-
for (const plugin of this.registeredPlugins) {
|
|
2320
|
-
if (plugin.name === pluginName && plugin.loaded === true && plugin.started === true && plugin.configured !== true) {
|
|
2321
|
-
try {
|
|
2322
|
-
this.configurePlugin(plugin); // No await do it asyncronously
|
|
2323
|
-
}
|
|
2324
|
-
catch (error) {
|
|
2325
|
-
plugin.error = true;
|
|
2326
|
-
this.log.error(`Error configuring plugin ${plg}${plugin.name}${er}`, error);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
// logEndpoint(commissioningServer.getRootEndpoint());
|
|
2332
|
-
}, 2000);
|
|
2333
|
-
}
|
|
2334
|
-
},
|
|
2335
|
-
commissioningChangedCallback: async (fabricIndex) => {
|
|
2336
|
-
const fabricInfo = commissioningServer.getCommissionedFabricInformation(fabricIndex);
|
|
2337
|
-
this.log.debug(`*Commissioning changed on fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${nf}`, debugStringify(fabricInfo));
|
|
2338
|
-
if (commissioningServer.getCommissionedFabricInformation().length === 0) {
|
|
2339
|
-
this.log.warn(`*Commissioning removed from fabric ${zb}${fabricIndex}${nf} for ${plg}${pluginName}${wr}. Resetting the commissioning server ...`);
|
|
2340
|
-
await commissioningServer.factoryReset();
|
|
2341
|
-
if (pluginName === 'Matterbridge') {
|
|
2342
|
-
await this.matterbridgeContext?.clearAll();
|
|
2343
|
-
this.matterbridgeFabricInformations = [];
|
|
2344
|
-
this.matterbridgeSessionInformations = [];
|
|
2345
|
-
this.matterbridgePaired = false;
|
|
2346
|
-
this.matterbridgeConnected = false;
|
|
2347
|
-
}
|
|
2348
|
-
else {
|
|
2349
|
-
for (const plugin of this.registeredPlugins) {
|
|
2350
|
-
if (plugin.name === pluginName) {
|
|
2351
|
-
await plugin.platform?.onShutdown('Commissioning removed by the controller');
|
|
2352
|
-
plugin.fabricInformations = [];
|
|
2353
|
-
plugin.sessionInformations = [];
|
|
2354
|
-
plugin.paired = false;
|
|
2355
|
-
plugin.connected = false;
|
|
2356
|
-
await plugin.storageContext?.clearAll();
|
|
2357
|
-
}
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
this.log.warn(`*Restart to activate the pairing for ${plg}${pluginName}${wr}.`);
|
|
2361
|
-
}
|
|
2362
|
-
else {
|
|
2363
|
-
const fabricInfo = commissioningServer.getCommissionedFabricInformation();
|
|
2364
|
-
if (pluginName === 'Matterbridge') {
|
|
2365
|
-
this.matterbridgeFabricInformations = this.sanitizeFabricInformations(fabricInfo);
|
|
2366
|
-
this.matterbridgePaired = true;
|
|
2367
|
-
}
|
|
2368
|
-
else {
|
|
2369
|
-
const plugin = this.findPlugin(pluginName);
|
|
2370
|
-
if (plugin) {
|
|
2371
|
-
plugin.fabricInformations = this.sanitizeFabricInformations(fabricInfo);
|
|
2372
|
-
plugin.paired = true;
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
|
-
},
|
|
2377
|
-
});
|
|
2378
|
-
if (this.passcode !== undefined)
|
|
2379
|
-
this.passcode++;
|
|
2380
|
-
if (this.discriminator !== undefined)
|
|
2381
|
-
this.discriminator++;
|
|
2382
|
-
commissioningServer.addCommandHandler('testEventTrigger', async ({ request: { enableKey, eventTrigger } }) => this.log.info(`testEventTrigger called on GeneralDiagnostic cluster: ${enableKey} ${eventTrigger}`));
|
|
2383
|
-
return commissioningServer;
|
|
2384
|
-
}
|
|
2385
|
-
/**
|
|
2386
|
-
* Creates a Matter server using the provided storage manager and the provided mdnsInterface.
|
|
2387
|
-
* @param storageManager The storage manager to be used by the Matter server.
|
|
2388
|
-
*
|
|
2389
|
-
*/
|
|
2390
|
-
createMatterServer(storageManager) {
|
|
2391
|
-
this.log.debug('Creating matter server');
|
|
2392
|
-
// Validate mdnsInterface
|
|
2393
|
-
if (this.mdnsInterface) {
|
|
2394
|
-
const networkInterfaces = os.networkInterfaces();
|
|
2395
|
-
const availableInterfaces = Object.keys(networkInterfaces);
|
|
2396
|
-
if (!availableInterfaces.includes(this.mdnsInterface)) {
|
|
2397
|
-
this.log.error(`Invalid mdnsInterface: ${this.mdnsInterface}. Available interfaces are: ${availableInterfaces.join(', ')}. Using all available interfaces.`);
|
|
2398
|
-
this.mdnsInterface = undefined;
|
|
2399
|
-
}
|
|
2400
|
-
else {
|
|
2401
|
-
this.log.info(`Using mdnsInterface '${this.mdnsInterface}' for the Matter server MdnsBroadcaster.`);
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
const matterServer = new MatterServer(storageManager, { mdnsInterface: this.mdnsInterface });
|
|
2405
|
-
this.log.debug('Created matter server');
|
|
2406
|
-
return matterServer;
|
|
2407
|
-
}
|
|
2408
|
-
/**
|
|
2409
|
-
* Creates a Matter Aggregator.
|
|
2410
|
-
* @param {StorageContext} context - The storage context.
|
|
2411
|
-
* @returns {Aggregator} - The created Matter Aggregator.
|
|
2412
|
-
*/
|
|
2413
|
-
async createMatterAggregator(context, pluginName) {
|
|
2414
|
-
const random = 'AG' + CryptoNode.getRandomData(8).toHex();
|
|
2415
|
-
await context.set('aggregatorSerialNumber', await context.get('aggregatorSerialNumber', random));
|
|
2416
|
-
await context.set('aggregatorUniqueId', await context.get('aggregatorUniqueId', random));
|
|
2417
|
-
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with uniqueId ${await context.get('aggregatorUniqueId')} serialNumber ${await context.get('aggregatorSerialNumber')}`);
|
|
2418
|
-
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with softwareVersion ${await context.get('softwareVersion', 1)} softwareVersionString ${await context.get('softwareVersionString', '1.0.0')}`);
|
|
2419
|
-
this.log.debug(`Creating matter aggregator for plugin ${plg}${pluginName}${db} with hardwareVersion ${await context.get('hardwareVersion', 1)} hardwareVersionString ${await context.get('hardwareVersionString', '1.0.0')}`);
|
|
2420
|
-
const matterAggregator = new Aggregator();
|
|
2421
|
-
matterAggregator.addClusterServer(ClusterServer(BasicInformationCluster, {
|
|
2422
|
-
dataModelRevision: 1,
|
|
2423
|
-
location: 'FR',
|
|
2424
|
-
vendorId: VendorId(0xfff1),
|
|
2425
|
-
vendorName: 'Matterbridge',
|
|
2426
|
-
productId: 0x8000,
|
|
2427
|
-
productName: 'Matterbridge aggregator',
|
|
2428
|
-
productLabel: 'Matterbridge aggregator',
|
|
2429
|
-
nodeLabel: 'Matterbridge aggregator',
|
|
2430
|
-
serialNumber: await context.get('aggregatorSerialNumber'),
|
|
2431
|
-
uniqueId: await context.get('aggregatorUniqueId'),
|
|
2432
|
-
softwareVersion: await context.get('softwareVersion', 1),
|
|
2433
|
-
softwareVersionString: await context.get('softwareVersionString', '1.0.0'),
|
|
2434
|
-
hardwareVersion: await context.get('hardwareVersion', 1),
|
|
2435
|
-
hardwareVersionString: await context.get('hardwareVersionString', '1.0.0'),
|
|
2436
|
-
reachable: true,
|
|
2437
|
-
capabilityMinima: { caseSessionsPerFabric: 3, subscriptionsPerFabric: 3 },
|
|
2438
|
-
}, {}, {
|
|
2439
|
-
startUp: true,
|
|
2440
|
-
shutDown: true,
|
|
2441
|
-
leave: true,
|
|
2442
|
-
reachableChanged: true,
|
|
2443
|
-
}));
|
|
2444
|
-
return matterAggregator;
|
|
2445
|
-
}
|
|
2446
|
-
/**
|
|
2447
|
-
* Stops the Matter server and associated controllers.
|
|
2448
|
-
*/
|
|
2449
|
-
async stopMatter() {
|
|
2450
|
-
this.log.debug('Stopping matter commissioningServer');
|
|
2451
|
-
await this.commissioningServer?.close();
|
|
2452
|
-
this.log.debug('Stopping matter commissioningController');
|
|
2453
|
-
await this.commissioningController?.close();
|
|
2454
|
-
this.log.debug('Stopping matter server');
|
|
2455
|
-
await this.matterServer?.close();
|
|
2456
|
-
this.log.debug('Matter server closed');
|
|
2457
|
-
this.commissioningController = undefined;
|
|
2458
|
-
this.commissioningServer = undefined;
|
|
2459
|
-
this.matterAggregator = undefined;
|
|
2460
|
-
this.matterServer = undefined;
|
|
2461
|
-
}
|
|
2462
|
-
/**
|
|
2463
|
-
* Retrieves the latest version of a package from the npm registry.
|
|
2464
|
-
* @param packageName - The name of the package.
|
|
2465
|
-
* @returns A Promise that resolves to the latest version of the package.
|
|
2466
|
-
*/
|
|
2467
|
-
async getLatestVersion(packageName) {
|
|
2468
|
-
return new Promise((resolve, reject) => {
|
|
2469
|
-
exec(`npm view ${packageName} version`, (error, stdout) => {
|
|
2470
|
-
if (error) {
|
|
2471
|
-
reject(error);
|
|
2472
|
-
}
|
|
2473
|
-
else {
|
|
2474
|
-
resolve(stdout.trim());
|
|
2475
|
-
}
|
|
2476
|
-
});
|
|
2477
|
-
});
|
|
2478
|
-
}
|
|
2479
|
-
/**
|
|
2480
|
-
* Retrieves the path to the global Node.js modules directory.
|
|
2481
|
-
* @returns A promise that resolves to the path of the global Node.js modules directory.
|
|
2482
|
-
*/
|
|
2483
|
-
async getGlobalNodeModules() {
|
|
2484
|
-
return new Promise((resolve, reject) => {
|
|
2485
|
-
exec('npm root -g', (error, stdout) => {
|
|
2486
|
-
if (error) {
|
|
2487
|
-
reject(error);
|
|
2488
|
-
}
|
|
2489
|
-
else {
|
|
2490
|
-
resolve(stdout.trim());
|
|
2491
|
-
}
|
|
2492
|
-
});
|
|
2493
|
-
});
|
|
2494
|
-
}
|
|
2495
|
-
/**
|
|
2496
|
-
* Logs the node and system information.
|
|
2497
|
-
*/
|
|
2498
|
-
async logNodeAndSystemInfo() {
|
|
2499
|
-
// IP address information
|
|
2500
|
-
const networkInterfaces = os.networkInterfaces();
|
|
2501
|
-
this.systemInformation.ipv4Address = '';
|
|
2502
|
-
this.systemInformation.ipv6Address = '';
|
|
2503
|
-
for (const [interfaceName, interfaceDetails] of Object.entries(networkInterfaces)) {
|
|
2504
|
-
if (!interfaceDetails) {
|
|
2505
|
-
break;
|
|
2506
|
-
}
|
|
2507
|
-
for (const detail of interfaceDetails) {
|
|
2508
|
-
if (detail.family === 'IPv4' && !detail.internal && this.systemInformation.ipv4Address === '') {
|
|
2509
|
-
this.systemInformation.interfaceName = interfaceName;
|
|
2510
|
-
this.systemInformation.ipv4Address = detail.address;
|
|
2511
|
-
this.systemInformation.macAddress = detail.mac;
|
|
2512
|
-
}
|
|
2513
|
-
else if (detail.family === 'IPv6' && !detail.internal && this.systemInformation.ipv6Address === '') {
|
|
2514
|
-
this.systemInformation.interfaceName = interfaceName;
|
|
2515
|
-
this.systemInformation.ipv6Address = detail.address;
|
|
2516
|
-
this.systemInformation.macAddress = detail.mac;
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
if (this.systemInformation.ipv4Address !== '' /* && this.systemInformation.ipv6Address !== ''*/) {
|
|
2520
|
-
this.log.debug(`Using interface: '${this.systemInformation.interfaceName}'`);
|
|
2521
|
-
this.log.debug(`- with MAC address: '${this.systemInformation.macAddress}'`);
|
|
2522
|
-
this.log.debug(`- with IPv4 address: '${this.systemInformation.ipv4Address}'`);
|
|
2523
|
-
this.log.debug(`- with IPv6 address: '${this.systemInformation.ipv6Address}'`);
|
|
2524
|
-
break;
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
// Node information
|
|
2528
|
-
this.systemInformation.nodeVersion = process.versions.node;
|
|
2529
|
-
const versionMajor = parseInt(this.systemInformation.nodeVersion.split('.')[0]);
|
|
2530
|
-
const versionMinor = parseInt(this.systemInformation.nodeVersion.split('.')[1]);
|
|
2531
|
-
const versionPatch = parseInt(this.systemInformation.nodeVersion.split('.')[2]);
|
|
2532
|
-
// Host system information
|
|
2533
|
-
this.systemInformation.hostname = os.hostname();
|
|
2534
|
-
this.systemInformation.user = os.userInfo().username;
|
|
2535
|
-
this.systemInformation.osType = os.type(); // "Windows_NT", "Darwin", etc.
|
|
2536
|
-
this.systemInformation.osRelease = os.release(); // Kernel version
|
|
2537
|
-
this.systemInformation.osPlatform = os.platform(); // "win32", "linux", "darwin", etc.
|
|
2538
|
-
this.systemInformation.osArch = os.arch(); // "x64", "arm", etc.
|
|
2539
|
-
this.systemInformation.totalMemory = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
2540
|
-
this.systemInformation.freeMemory = (os.freemem() / 1024 / 1024 / 1024).toFixed(2) + ' GB'; // Convert to GB
|
|
2541
|
-
this.systemInformation.systemUptime = (os.uptime() / 60 / 60).toFixed(2) + ' hours'; // Convert to hours
|
|
2542
|
-
// Log the system information
|
|
2543
|
-
this.log.debug('Host System Information:');
|
|
2544
|
-
this.log.debug(`- Hostname: ${this.systemInformation.hostname}`);
|
|
2545
|
-
this.log.debug(`- User: ${this.systemInformation.user}`);
|
|
2546
|
-
this.log.debug(`- Interface: ${this.systemInformation.interfaceName}`);
|
|
2547
|
-
this.log.debug(`- MAC Address: ${this.systemInformation.macAddress}`);
|
|
2548
|
-
this.log.debug(`- IPv4 Address: ${this.systemInformation.ipv4Address}`);
|
|
2549
|
-
this.log.debug(`- IPv6 Address: ${this.systemInformation.ipv6Address}`);
|
|
2550
|
-
this.log.debug(`- Node.js: ${versionMajor}.${versionMinor}.${versionPatch}`);
|
|
2551
|
-
this.log.debug(`- OS Type: ${this.systemInformation.osType}`);
|
|
2552
|
-
this.log.debug(`- OS Release: ${this.systemInformation.osRelease}`);
|
|
2553
|
-
this.log.debug(`- Platform: ${this.systemInformation.osPlatform}`);
|
|
2554
|
-
this.log.debug(`- Architecture: ${this.systemInformation.osArch}`);
|
|
2555
|
-
this.log.debug(`- Total Memory: ${this.systemInformation.totalMemory}`);
|
|
2556
|
-
this.log.debug(`- Free Memory: ${this.systemInformation.freeMemory}`);
|
|
2557
|
-
this.log.debug(`- System Uptime: ${this.systemInformation.systemUptime}`);
|
|
2558
|
-
// Home directory
|
|
2559
|
-
this.homeDirectory = os.homedir();
|
|
2560
|
-
this.matterbridgeInformation.homeDirectory = this.homeDirectory;
|
|
2561
|
-
this.log.debug(`Home Directory: ${this.homeDirectory}`);
|
|
2562
|
-
// Package root directory
|
|
2563
|
-
const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
2564
|
-
this.rootDirectory = path.resolve(currentFileDirectory, '../');
|
|
2565
|
-
this.matterbridgeInformation.rootDirectory = this.rootDirectory;
|
|
2566
|
-
this.log.debug(`Root Directory: ${this.rootDirectory}`);
|
|
2567
|
-
// Global node_modules directory
|
|
2568
|
-
if (this.nodeContext)
|
|
2569
|
-
this.globalModulesDirectory = await this.nodeContext.get('globalModulesDirectory', '');
|
|
2570
|
-
// First run of Matterbridge so the node storage is empty
|
|
2571
|
-
if (this.globalModulesDirectory === '') {
|
|
2572
|
-
try {
|
|
2573
|
-
this.globalModulesDirectory = await this.getGlobalNodeModules();
|
|
2574
|
-
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
2575
|
-
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
2576
|
-
await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
|
|
2577
|
-
}
|
|
2578
|
-
catch (error) {
|
|
2579
|
-
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
else {
|
|
2583
|
-
this.getGlobalNodeModules()
|
|
2584
|
-
.then(async (globalModulesDirectory) => {
|
|
2585
|
-
this.globalModulesDirectory = globalModulesDirectory;
|
|
2586
|
-
this.matterbridgeInformation.globalModulesDirectory = this.globalModulesDirectory;
|
|
2587
|
-
this.log.debug(`Global node_modules Directory: ${this.globalModulesDirectory}`);
|
|
2588
|
-
await this.nodeContext?.set('globalModulesDirectory', this.globalModulesDirectory);
|
|
2589
|
-
})
|
|
2590
|
-
.catch((error) => {
|
|
2591
|
-
this.log.error(`Error getting global node_modules directory: ${error}`);
|
|
2592
|
-
});
|
|
2593
|
-
}
|
|
2594
|
-
// Create the data directory .matterbridge in the home directory
|
|
2595
|
-
this.matterbridgeDirectory = path.join(this.homeDirectory, '.matterbridge');
|
|
2596
|
-
this.matterbridgeInformation.matterbridgeDirectory = this.matterbridgeDirectory;
|
|
2597
|
-
try {
|
|
2598
|
-
await fs.access(this.matterbridgeDirectory);
|
|
2599
|
-
}
|
|
2600
|
-
catch (err) {
|
|
2601
|
-
if (err instanceof Error) {
|
|
2602
|
-
const nodeErr = err;
|
|
2603
|
-
if (nodeErr.code === 'ENOENT') {
|
|
2604
|
-
try {
|
|
2605
|
-
await fs.mkdir(this.matterbridgeDirectory, { recursive: true });
|
|
2606
|
-
this.log.info(`Created Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
2607
|
-
}
|
|
2608
|
-
catch (err) {
|
|
2609
|
-
this.log.error(`Error creating directory: ${err}`);
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
else {
|
|
2613
|
-
this.log.error(`Error accessing directory: ${err}`);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
this.log.debug(`Matterbridge Directory: ${this.matterbridgeDirectory}`);
|
|
2618
|
-
// Create the plugin directory Matterbridge in the home directory
|
|
2619
|
-
this.matterbridgePluginDirectory = path.join(this.homeDirectory, 'Matterbridge');
|
|
2620
|
-
this.matterbridgeInformation.matterbridgePluginDirectory = this.matterbridgePluginDirectory;
|
|
2621
|
-
try {
|
|
2622
|
-
await fs.access(this.matterbridgePluginDirectory);
|
|
2623
|
-
}
|
|
2624
|
-
catch (err) {
|
|
2625
|
-
if (err instanceof Error) {
|
|
2626
|
-
const nodeErr = err;
|
|
2627
|
-
if (nodeErr.code === 'ENOENT') {
|
|
2628
|
-
try {
|
|
2629
|
-
await fs.mkdir(this.matterbridgePluginDirectory, { recursive: true });
|
|
2630
|
-
this.log.info(`Created Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
2631
|
-
}
|
|
2632
|
-
catch (err) {
|
|
2633
|
-
this.log.error(`Error creating directory: ${err}`);
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
else {
|
|
2637
|
-
this.log.error(`Error accessing directory: ${err}`);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
this.log.debug(`Matterbridge Plugin Directory: ${this.matterbridgePluginDirectory}`);
|
|
2642
|
-
// Matterbridge version
|
|
2643
|
-
const packageJson = JSON.parse(await fs.readFile(path.join(this.rootDirectory, 'package.json'), 'utf-8'));
|
|
2644
|
-
this.matterbridgeVersion = packageJson.version;
|
|
2645
|
-
this.matterbridgeInformation.matterbridgeVersion = this.matterbridgeVersion;
|
|
2646
|
-
this.log.debug(`Matterbridge Version: ${this.matterbridgeVersion}`);
|
|
2647
|
-
// Matterbridge latest version
|
|
2648
|
-
if (this.nodeContext)
|
|
2649
|
-
this.matterbridgeLatestVersion = await this.nodeContext.get('matterbridgeLatestVersion', '');
|
|
2650
|
-
this.log.debug(`Matterbridge Latest Version: ${this.matterbridgeLatestVersion}`);
|
|
2651
|
-
this.getMatterbridgeLatestVersion();
|
|
2652
|
-
// Current working directory
|
|
2653
|
-
const currentDir = process.cwd();
|
|
2654
|
-
this.log.debug(`Current Working Directory: ${currentDir}`);
|
|
2655
|
-
// Command line arguments (excluding 'node' and the script name)
|
|
2656
|
-
const cmdArgs = process.argv.slice(2).join(' ');
|
|
2657
|
-
this.log.debug(`Command Line Arguments: ${cmdArgs}`);
|
|
2342
|
+
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2343
|
+
basicInformationCluster.setReachableAttribute(reachable);
|
|
2344
|
+
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2345
|
+
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2658
2346
|
}
|
|
2659
2347
|
/**
|
|
2660
|
-
*
|
|
2661
|
-
* @
|
|
2662
|
-
* @
|
|
2348
|
+
* Sets the reachability of the specified matter aggregator and its bridged devices and trigger.
|
|
2349
|
+
* @param {Aggregator} matterAggregator - The matter aggregator to set the reachability for.
|
|
2350
|
+
* @param {boolean} reachable - A boolean indicating the reachability status to set.
|
|
2663
2351
|
*/
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
}
|
|
2674
|
-
})
|
|
2675
|
-
.catch((error) => {
|
|
2676
|
-
this.log.error(`Error getting Matterbridge latest version: ${error.message}`);
|
|
2677
|
-
// error.stack && this.log.debug(error.stack);
|
|
2352
|
+
setAggregatorReachability(matterAggregator, reachable) {
|
|
2353
|
+
const basicInformationCluster = matterAggregator.getClusterServer(BasicInformationCluster);
|
|
2354
|
+
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2355
|
+
basicInformationCluster.setReachableAttribute(reachable);
|
|
2356
|
+
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2357
|
+
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2358
|
+
matterAggregator.getBridgedDevices().forEach((device) => {
|
|
2359
|
+
this.log.debug(`Setting reachability to true for bridged device: ${dev}${device.name}${nf}`);
|
|
2360
|
+
device.getClusterServer(BridgedDeviceBasicInformationCluster)?.setReachableAttribute(reachable);
|
|
2361
|
+
device.getClusterServer(BridgedDeviceBasicInformationCluster)?.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2678
2362
|
});
|
|
2679
2363
|
}
|
|
2680
2364
|
/**
|
|
2681
|
-
*
|
|
2682
|
-
* If the plugin's version is different from the latest version, logs a warning message.
|
|
2683
|
-
* If the plugin's version is the same as the latest version, logs an info message.
|
|
2684
|
-
* If there is an error retrieving the latest version, logs an error message.
|
|
2365
|
+
* Sets the reachability of a device and trigger.
|
|
2685
2366
|
*
|
|
2686
|
-
* @
|
|
2687
|
-
* @param {
|
|
2688
|
-
* @returns {Promise<void>} A promise that resolves when the latest version is retrieved and actions are performed.
|
|
2367
|
+
* @param {MatterbridgeDevice} device - The device to set the reachability for.
|
|
2368
|
+
* @param {boolean} reachable - The new reachability status of the device.
|
|
2689
2369
|
*/
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
this.log.warn(`The plugin ${plg}${plugin.name}${wr} is out of date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
|
|
2697
|
-
else
|
|
2698
|
-
this.log.info(`The plugin ${plg}${plugin.name}${nf} is up to date. Current version: ${plugin.version}, Latest version: ${latestVersion}`);
|
|
2699
|
-
})
|
|
2700
|
-
.catch((error) => {
|
|
2701
|
-
this.log.error(`Error getting ${plugin.name} latest version: ${error.message}`);
|
|
2702
|
-
// error.stack && this.log.debug(error.stack);
|
|
2703
|
-
});
|
|
2370
|
+
setDeviceReachability(device, reachable) {
|
|
2371
|
+
const basicInformationCluster = device.getClusterServer(BasicInformationCluster);
|
|
2372
|
+
if (basicInformationCluster && basicInformationCluster.attributes.reachable !== undefined)
|
|
2373
|
+
basicInformationCluster.setReachableAttribute(reachable);
|
|
2374
|
+
if (basicInformationCluster && basicInformationCluster.triggerReachableChangedEvent)
|
|
2375
|
+
basicInformationCluster.triggerReachableChangedEvent({ reachableNewValue: reachable });
|
|
2704
2376
|
}
|
|
2377
|
+
getVendorIdName = (vendorId) => {
|
|
2378
|
+
if (!vendorId)
|
|
2379
|
+
return '';
|
|
2380
|
+
let vendorName = '';
|
|
2381
|
+
switch (vendorId) {
|
|
2382
|
+
case 4937:
|
|
2383
|
+
vendorName = '(AppleHome)';
|
|
2384
|
+
break;
|
|
2385
|
+
case 4996:
|
|
2386
|
+
vendorName = '(AppleKeyChain)';
|
|
2387
|
+
break;
|
|
2388
|
+
case 4362:
|
|
2389
|
+
vendorName = '(SmartThings)';
|
|
2390
|
+
break;
|
|
2391
|
+
case 4939:
|
|
2392
|
+
vendorName = '(HomeAssistant)';
|
|
2393
|
+
break;
|
|
2394
|
+
case 24582:
|
|
2395
|
+
vendorName = '(GoogleHome)';
|
|
2396
|
+
break;
|
|
2397
|
+
case 4631:
|
|
2398
|
+
vendorName = '(Alexa)';
|
|
2399
|
+
break;
|
|
2400
|
+
case 4701:
|
|
2401
|
+
vendorName = '(Tuya)';
|
|
2402
|
+
break;
|
|
2403
|
+
case 4742:
|
|
2404
|
+
vendorName = '(eWeLink)';
|
|
2405
|
+
break;
|
|
2406
|
+
case 65521:
|
|
2407
|
+
vendorName = '(PythonMatterServer)';
|
|
2408
|
+
break;
|
|
2409
|
+
default:
|
|
2410
|
+
vendorName = '(unknown)';
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
return vendorName;
|
|
2414
|
+
};
|
|
2705
2415
|
/**
|
|
2706
|
-
* Retrieves
|
|
2707
|
-
*
|
|
2708
|
-
* @returns {BaseRegisteredPlugin[]}
|
|
2416
|
+
* Retrieves the base registered plugins sanitized for res.json().
|
|
2417
|
+
* @param {boolean} includeAll - Whether to include all information for each plugin.
|
|
2418
|
+
* @returns {BaseRegisteredPlugin[]} A promise that resolves to an array of BaseRegisteredPlugin objects.
|
|
2709
2419
|
*/
|
|
2710
|
-
async getBaseRegisteredPlugins(
|
|
2420
|
+
async getBaseRegisteredPlugins(includeAll = false) {
|
|
2711
2421
|
const baseRegisteredPlugins = [];
|
|
2712
|
-
for (const plugin of this.
|
|
2422
|
+
for (const plugin of this.plugins) {
|
|
2713
2423
|
baseRegisteredPlugins.push({
|
|
2714
2424
|
path: plugin.path,
|
|
2715
2425
|
type: plugin.type,
|
|
@@ -2726,14 +2436,14 @@ export class Matterbridge extends EventEmitter {
|
|
|
2726
2436
|
configured: plugin.configured,
|
|
2727
2437
|
paired: plugin.paired,
|
|
2728
2438
|
connected: plugin.connected,
|
|
2729
|
-
fabricInformations: plugin.fabricInformations,
|
|
2730
|
-
sessionInformations: plugin.sessionInformations,
|
|
2439
|
+
fabricInformations: includeAll ? plugin.fabricInformations : undefined,
|
|
2440
|
+
sessionInformations: includeAll ? plugin.sessionInformations : undefined,
|
|
2731
2441
|
registeredDevices: plugin.registeredDevices,
|
|
2732
2442
|
addedDevices: plugin.addedDevices,
|
|
2733
2443
|
qrPairingCode: plugin.qrPairingCode,
|
|
2734
2444
|
manualPairingCode: plugin.manualPairingCode,
|
|
2735
|
-
configJson:
|
|
2736
|
-
schemaJson:
|
|
2445
|
+
configJson: includeAll ? plugin.configJson : undefined,
|
|
2446
|
+
schemaJson: includeAll ? plugin.schemaJson : undefined,
|
|
2737
2447
|
});
|
|
2738
2448
|
}
|
|
2739
2449
|
return baseRegisteredPlugins;
|
|
@@ -2835,9 +2545,10 @@ export class Matterbridge extends EventEmitter {
|
|
|
2835
2545
|
/**
|
|
2836
2546
|
* Initializes the frontend of Matterbridge.
|
|
2837
2547
|
*
|
|
2838
|
-
* @param port The port number to run the frontend server on. Default is
|
|
2548
|
+
* @param port The port number to run the frontend server on. Default is 8283.
|
|
2839
2549
|
*/
|
|
2840
2550
|
async initializeFrontend(port = 8283) {
|
|
2551
|
+
let initializeError = false;
|
|
2841
2552
|
this.log.debug(`Initializing the frontend ${hasParameter('ssl') ? 'https' : 'http'} server on port ${YELLOW}${port}${db}`);
|
|
2842
2553
|
// Create the express app that serves the frontend
|
|
2843
2554
|
this.expressApp = express();
|
|
@@ -2863,7 +2574,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
2863
2574
|
this.log.error(`Port ${port} is already in use`);
|
|
2864
2575
|
break;
|
|
2865
2576
|
}
|
|
2866
|
-
|
|
2577
|
+
initializeError = true;
|
|
2578
|
+
return;
|
|
2867
2579
|
});
|
|
2868
2580
|
}
|
|
2869
2581
|
else {
|
|
@@ -2874,8 +2586,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
2874
2586
|
this.log.info(`Loaded certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}`);
|
|
2875
2587
|
}
|
|
2876
2588
|
catch (error) {
|
|
2877
|
-
this.log.error(`Error reading certificate file: ${error}`);
|
|
2878
|
-
|
|
2589
|
+
this.log.error(`Error reading certificate file ${path.join(this.matterbridgeDirectory, 'certs/cert.pem')}: ${error}`);
|
|
2590
|
+
return;
|
|
2879
2591
|
}
|
|
2880
2592
|
let key;
|
|
2881
2593
|
try {
|
|
@@ -2883,8 +2595,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
2883
2595
|
this.log.info(`Loaded key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}`);
|
|
2884
2596
|
}
|
|
2885
2597
|
catch (error) {
|
|
2886
|
-
this.log.error(`Error reading key file: ${error}`);
|
|
2887
|
-
|
|
2598
|
+
this.log.error(`Error reading key file ${path.join(this.matterbridgeDirectory, 'certs/key.pem')}: ${error}`);
|
|
2599
|
+
return;
|
|
2888
2600
|
}
|
|
2889
2601
|
let ca;
|
|
2890
2602
|
try {
|
|
@@ -2915,9 +2627,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
2915
2627
|
this.log.error(`Port ${port} is already in use`);
|
|
2916
2628
|
break;
|
|
2917
2629
|
}
|
|
2918
|
-
|
|
2630
|
+
initializeError = true;
|
|
2631
|
+
return;
|
|
2919
2632
|
});
|
|
2920
2633
|
}
|
|
2634
|
+
if (initializeError)
|
|
2635
|
+
return;
|
|
2921
2636
|
// Createe a WebSocket server and attach it to the http server
|
|
2922
2637
|
const wssPort = port;
|
|
2923
2638
|
const wssHost = hasParameter('ssl') ? `wss://${this.systemInformation.ipv4Address}:${wssPort}` : `ws://${this.systemInformation.ipv4Address}:${wssPort}`;
|
|
@@ -2926,7 +2641,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
2926
2641
|
const clientIp = request.socket.remoteAddress;
|
|
2927
2642
|
this.log.info(`WebSocketServer client ${clientIp} connected`);
|
|
2928
2643
|
this.log.setGlobalCallback(this.wssSendMessage.bind(this));
|
|
2929
|
-
this.log.debug('WebSocketServer logger callback added');
|
|
2644
|
+
this.log.debug('WebSocketServer logger global callback added');
|
|
2930
2645
|
this.wssSendMessage('Matterbridge', 'info', `WebSocketServer client ${clientIp} connected to Matterbridge`);
|
|
2931
2646
|
ws.on('message', (message) => {
|
|
2932
2647
|
this.log.debug(`WebSocket client message: ${message}`);
|
|
@@ -2935,7 +2650,7 @@ export class Matterbridge extends EventEmitter {
|
|
|
2935
2650
|
this.log.info('WebSocket client disconnected');
|
|
2936
2651
|
if (this.webSocketServer?.clients.size === 0) {
|
|
2937
2652
|
this.log.setGlobalCallback(undefined);
|
|
2938
|
-
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger callback removed');
|
|
2653
|
+
this.log.debug('All WebSocket clients disconnected. WebSocketServer logger global callback removed');
|
|
2939
2654
|
}
|
|
2940
2655
|
});
|
|
2941
2656
|
ws.on('error', (error) => {
|
|
@@ -3004,6 +2719,12 @@ export class Matterbridge extends EventEmitter {
|
|
|
3004
2719
|
this.matterbridgeInformation.matterbridgeSessionInformations = this.matterbridgeSessionInformations;
|
|
3005
2720
|
const response = { wssHost, qrPairingCode, manualPairingCode, systemInformation: this.systemInformation, matterbridgeInformation: this.matterbridgeInformation };
|
|
3006
2721
|
// this.log.debug('Response:', debugStringify(response));
|
|
2722
|
+
this.log.debug(`WebSocketServer logger local callback: ${this.log.getCallback() ? 'active' : 'inactive'}`);
|
|
2723
|
+
this.log.debug(`WebSocketServer logger global callback: ${this.log.getGlobalCallback() ? 'active' : 'inactive'}`);
|
|
2724
|
+
if (this.webSocketServer && this.webSocketServer.clients.size > 0 && !this.log.getGlobalCallback()) {
|
|
2725
|
+
this.log.setGlobalCallback(this.wssSendMessage.bind(this));
|
|
2726
|
+
this.log.debug('WebSocketServer logger global callback added');
|
|
2727
|
+
}
|
|
3007
2728
|
res.json(response);
|
|
3008
2729
|
});
|
|
3009
2730
|
// Endpoint to provide plugins
|
|
@@ -3131,6 +2852,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
3131
2852
|
const password = param.slice(1, -1); // Remove the first and last characters
|
|
3132
2853
|
this.log.debug('setpassword', param, password);
|
|
3133
2854
|
await this.nodeContext?.set('password', password);
|
|
2855
|
+
res.json({ message: 'Command received' });
|
|
2856
|
+
return;
|
|
3134
2857
|
}
|
|
3135
2858
|
// Handle the command setmbloglevel from Settings
|
|
3136
2859
|
if (command === 'setmbloglevel') {
|
|
@@ -3143,6 +2866,8 @@ export class Matterbridge extends EventEmitter {
|
|
|
3143
2866
|
this.log.setLogDebug(false);
|
|
3144
2867
|
this.debugEnabled = false;
|
|
3145
2868
|
}
|
|
2869
|
+
res.json({ message: 'Command received' });
|
|
2870
|
+
return;
|
|
3146
2871
|
}
|
|
3147
2872
|
// Handle the command setmbloglevel from Settings
|
|
3148
2873
|
if (command === 'setmjloglevel') {
|
|
@@ -3165,184 +2890,122 @@ export class Matterbridge extends EventEmitter {
|
|
|
3165
2890
|
else if (param === 'Fatal') {
|
|
3166
2891
|
Logger.defaultLogLevel = Level.FATAL;
|
|
3167
2892
|
}
|
|
2893
|
+
res.json({ message: 'Command received' });
|
|
2894
|
+
return;
|
|
3168
2895
|
}
|
|
3169
2896
|
// Handle the command unregister from Settings
|
|
3170
2897
|
if (command === 'unregister') {
|
|
3171
2898
|
await this.unregisterAndShutdownProcess();
|
|
2899
|
+
res.json({ message: 'Command received' });
|
|
2900
|
+
return;
|
|
3172
2901
|
}
|
|
3173
2902
|
// Handle the command reset from Settings
|
|
3174
2903
|
if (command === 'reset') {
|
|
3175
|
-
this.shutdownProcessAndReset();
|
|
2904
|
+
await this.shutdownProcessAndReset();
|
|
2905
|
+
res.json({ message: 'Command received' });
|
|
2906
|
+
return;
|
|
3176
2907
|
}
|
|
3177
2908
|
// Handle the command factoryreset from Settings
|
|
3178
2909
|
if (command === 'factoryreset') {
|
|
3179
|
-
this.shutdownProcessAndFactoryReset();
|
|
2910
|
+
await this.shutdownProcessAndFactoryReset();
|
|
2911
|
+
res.json({ message: 'Command received' });
|
|
2912
|
+
return;
|
|
3180
2913
|
}
|
|
3181
2914
|
// Handle the command shutdown from Header
|
|
3182
2915
|
if (command === 'shutdown') {
|
|
3183
|
-
this.shutdownProcess();
|
|
2916
|
+
await this.shutdownProcess();
|
|
2917
|
+
res.json({ message: 'Command received' });
|
|
2918
|
+
return;
|
|
3184
2919
|
}
|
|
3185
2920
|
// Handle the command restart from Header
|
|
3186
2921
|
if (command === 'restart') {
|
|
3187
|
-
this.restartProcess();
|
|
2922
|
+
await this.restartProcess();
|
|
2923
|
+
res.json({ message: 'Command received' });
|
|
2924
|
+
return;
|
|
3188
2925
|
}
|
|
3189
2926
|
// Handle the command update from Header
|
|
3190
2927
|
if (command === 'update') {
|
|
3191
2928
|
this.log.info('Updating matterbridge...');
|
|
3192
2929
|
try {
|
|
3193
|
-
await this.spawnCommand('npm', ['install', '-g', 'matterbridge'
|
|
2930
|
+
await this.spawnCommand('npm', ['install', '-g', 'matterbridge']);
|
|
3194
2931
|
this.log.info('Matterbridge has been updated. Full restart required.');
|
|
3195
2932
|
}
|
|
3196
2933
|
catch (error) {
|
|
3197
2934
|
this.log.error('Error updating matterbridge');
|
|
3198
|
-
res.json({ message: 'Command received' });
|
|
3199
|
-
return;
|
|
3200
2935
|
}
|
|
3201
|
-
this.updateProcess();
|
|
2936
|
+
await this.updateProcess();
|
|
2937
|
+
res.json({ message: 'Command received' });
|
|
2938
|
+
return;
|
|
3202
2939
|
}
|
|
3203
2940
|
// Handle the command saveconfig from Home
|
|
3204
2941
|
if (command === 'saveconfig') {
|
|
3205
2942
|
param = param.replace(/\*/g, '\\');
|
|
3206
2943
|
this.log.info(`Saving config for plugin ${plg}${param}${nf}...`);
|
|
3207
2944
|
// console.log('Req.body:', JSON.stringify(req.body, null, 2));
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
2945
|
+
if (!this.plugins.has(param)) {
|
|
2946
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
2947
|
+
}
|
|
2948
|
+
else {
|
|
2949
|
+
const plugin = this.plugins.get(param);
|
|
2950
|
+
if (!plugin)
|
|
2951
|
+
return;
|
|
2952
|
+
this.plugins.saveConfigFromJson(plugin, req.body);
|
|
2953
|
+
}
|
|
2954
|
+
res.json({ message: 'Command received' });
|
|
2955
|
+
return;
|
|
3215
2956
|
}
|
|
3216
2957
|
// Handle the command installplugin from Home
|
|
3217
2958
|
if (command === 'installplugin') {
|
|
3218
2959
|
param = param.replace(/\*/g, '\\');
|
|
3219
2960
|
this.log.info(`Installing plugin ${plg}${param}${nf}...`);
|
|
3220
2961
|
try {
|
|
3221
|
-
await this.spawnCommand('npm', ['install', '-g', param
|
|
2962
|
+
await this.spawnCommand('npm', ['install', '-g', param]);
|
|
3222
2963
|
this.log.info(`Plugin ${plg}${param}${nf} installed. Full restart required.`);
|
|
3223
2964
|
}
|
|
3224
2965
|
catch (error) {
|
|
3225
2966
|
this.log.error(`Error installing plugin ${plg}${param}${er}`);
|
|
3226
|
-
res.json({ message: 'Command received' });
|
|
3227
|
-
return;
|
|
3228
2967
|
}
|
|
2968
|
+
// Also add the plugin to matterbridge
|
|
2969
|
+
// res.json({ message: 'Command received' });
|
|
2970
|
+
// return;
|
|
3229
2971
|
}
|
|
3230
2972
|
// Handle the command addplugin from Home
|
|
3231
2973
|
if (command === 'addplugin' || command === 'installplugin') {
|
|
3232
2974
|
param = param.replace(/\*/g, '\\');
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
this.log.error(`Error resolving plugin ${plg}${param}${er}`);
|
|
3241
|
-
res.json({ message: 'Command received' });
|
|
3242
|
-
return;
|
|
3243
|
-
}
|
|
3244
|
-
this.log.debug(`Loading plugin from ${plg}${packageJsonPath}${db}`);
|
|
3245
|
-
try {
|
|
3246
|
-
// Load the package.json of the plugin
|
|
3247
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
3248
|
-
const plugin = { path: packageJsonPath, type: '', name: packageJson.name, version: packageJson.version, description: packageJson.description, author: packageJson.author, enabled: true };
|
|
3249
|
-
if (await this.loadPlugin(plugin)) {
|
|
3250
|
-
this.registeredPlugins.push(plugin);
|
|
3251
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
3252
|
-
this.log.info(`Plugin ${plg}${packageJsonPath}${nf} type ${plugin.type} added to matterbridge. Restart required.`);
|
|
3253
|
-
this.startPlugin(plugin, 'The plugin has been added to Matterbridge', true);
|
|
3254
|
-
}
|
|
3255
|
-
else {
|
|
3256
|
-
this.log.error(`Error adding plugin ${plg}${packageJsonPath}${er}`);
|
|
2975
|
+
const plugin = await this.plugins.add(param);
|
|
2976
|
+
if (plugin) {
|
|
2977
|
+
this.plugins.load(plugin, true, 'The plugin has been added', true); // No await do it in the background
|
|
2978
|
+
/*
|
|
2979
|
+
plugin.platform = await this.plugins.load(plugin, false, 'The plugin has been added');
|
|
2980
|
+
if (plugin.platform) {
|
|
2981
|
+
await this.plugins.start(plugin, 'The plugin has been added', true);
|
|
3257
2982
|
}
|
|
2983
|
+
*/
|
|
3258
2984
|
}
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
res.json({ message: 'Command received' });
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
2985
|
+
res.json({ message: 'Command received' });
|
|
2986
|
+
return;
|
|
3264
2987
|
}
|
|
3265
2988
|
// Handle the command removeplugin from Home
|
|
3266
2989
|
if (command === 'removeplugin') {
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
if (this.registeredPlugins[index].platform) {
|
|
3270
|
-
await this.registeredPlugins[index].platform?.onShutdown('The plugin has been removed.');
|
|
3271
|
-
}
|
|
3272
|
-
// Remove all devices from the plugin
|
|
3273
|
-
await this.removeAllBridgedDevices(this.registeredPlugins[index].name);
|
|
3274
|
-
// Remove the plugin from the registered plugins
|
|
3275
|
-
this.registeredPlugins.splice(index, 1);
|
|
3276
|
-
await this.nodeContext?.set('plugins', await this.getBaseRegisteredPlugins());
|
|
3277
|
-
this.log.info(`Plugin ${plg}${param}${nf} removed from matterbridge`);
|
|
2990
|
+
if (!this.plugins.has(param)) {
|
|
2991
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
3278
2992
|
}
|
|
3279
2993
|
else {
|
|
3280
|
-
this.
|
|
2994
|
+
const plugin = this.plugins.get(param);
|
|
2995
|
+
await this.plugins.shutdown(plugin, 'The plugin has been removed.', true);
|
|
2996
|
+
await this.plugins.remove(param);
|
|
3281
2997
|
}
|
|
2998
|
+
res.json({ message: 'Command received' });
|
|
2999
|
+
return;
|
|
3282
3000
|
}
|
|
3283
3001
|
// Handle the command enableplugin from Home
|
|
3284
3002
|
if (command === 'enableplugin') {
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
return;
|
|
3288
|
-
const plugin = plugins.find((plugin) => plugin.name === param);
|
|
3289
|
-
if (plugin) {
|
|
3290
|
-
plugin.enabled = true;
|
|
3291
|
-
plugin.locked = undefined;
|
|
3292
|
-
plugin.error = undefined;
|
|
3293
|
-
plugin.loaded = undefined;
|
|
3294
|
-
plugin.started = undefined;
|
|
3295
|
-
plugin.configured = undefined;
|
|
3296
|
-
plugin.connected = undefined;
|
|
3297
|
-
plugin.platform = undefined;
|
|
3298
|
-
plugin.registeredDevices = undefined;
|
|
3299
|
-
plugin.addedDevices = undefined;
|
|
3300
|
-
await this.nodeContext?.set('plugins', plugins);
|
|
3301
|
-
this.log.info(`Enabled plugin ${plg}${param}${nf}`);
|
|
3302
|
-
}
|
|
3303
|
-
if (this.bridgeMode === 'bridge') {
|
|
3304
|
-
const pluginToEnable = this.findPlugin(param);
|
|
3305
|
-
if (pluginToEnable) {
|
|
3306
|
-
pluginToEnable.enabled = true;
|
|
3307
|
-
pluginToEnable.platform = await this.loadPlugin(pluginToEnable);
|
|
3308
|
-
if (pluginToEnable.platform) {
|
|
3309
|
-
await this.startPlugin(pluginToEnable, 'The plugin has been enabled', true);
|
|
3310
|
-
}
|
|
3311
|
-
else {
|
|
3312
|
-
pluginToEnable.enabled = false;
|
|
3313
|
-
pluginToEnable.error = true;
|
|
3314
|
-
this.log.error(`Error enabling plugin ${plg}${param}${er}`);
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3003
|
+
if (!this.plugins.has(param)) {
|
|
3004
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
3317
3005
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
const pluginToDisable = this.findPlugin(param);
|
|
3322
|
-
if (pluginToDisable) {
|
|
3323
|
-
if (pluginToDisable.platform) {
|
|
3324
|
-
await pluginToDisable.platform.onShutdown('The plugin has been disabled.');
|
|
3325
|
-
}
|
|
3326
|
-
// Remove all devices from the plugin
|
|
3327
|
-
this.log.info(`Unregistering devices for plugin ${plg}${pluginToDisable.name}${nf}...`);
|
|
3328
|
-
await this.removeAllBridgedDevices(pluginToDisable.name);
|
|
3329
|
-
pluginToDisable.enabled = false;
|
|
3330
|
-
pluginToDisable.locked = undefined;
|
|
3331
|
-
pluginToDisable.error = undefined;
|
|
3332
|
-
pluginToDisable.loaded = undefined;
|
|
3333
|
-
pluginToDisable.started = undefined;
|
|
3334
|
-
pluginToDisable.configured = undefined;
|
|
3335
|
-
pluginToDisable.connected = undefined;
|
|
3336
|
-
pluginToDisable.platform = undefined;
|
|
3337
|
-
pluginToDisable.registeredDevices = undefined;
|
|
3338
|
-
pluginToDisable.addedDevices = undefined;
|
|
3339
|
-
// Save the plugins state in node storage
|
|
3340
|
-
const plugins = await this.nodeContext?.get('plugins');
|
|
3341
|
-
if (!plugins)
|
|
3342
|
-
return;
|
|
3343
|
-
const plugin = plugins.find((plugin) => plugin.name === param);
|
|
3344
|
-
if (plugin) {
|
|
3345
|
-
plugin.enabled = false;
|
|
3006
|
+
else {
|
|
3007
|
+
const plugin = this.plugins.get(param);
|
|
3008
|
+
if (plugin && !plugin.enabled) {
|
|
3346
3009
|
plugin.locked = undefined;
|
|
3347
3010
|
plugin.error = undefined;
|
|
3348
3011
|
plugin.loaded = undefined;
|
|
@@ -3352,12 +3015,34 @@ export class Matterbridge extends EventEmitter {
|
|
|
3352
3015
|
plugin.platform = undefined;
|
|
3353
3016
|
plugin.registeredDevices = undefined;
|
|
3354
3017
|
plugin.addedDevices = undefined;
|
|
3355
|
-
await this.
|
|
3356
|
-
this.
|
|
3018
|
+
await this.plugins.enable(param);
|
|
3019
|
+
this.plugins.load(plugin, true, 'The plugin has been enabled', true); // No await do it in the background
|
|
3020
|
+
/*
|
|
3021
|
+
plugin.platform = await this.plugins.load(plugin, false, 'The plugin has been enabled');
|
|
3022
|
+
if (plugin.platform) {
|
|
3023
|
+
await this.plugins.start(plugin, 'The plugin has been enabled', true);
|
|
3024
|
+
}
|
|
3025
|
+
*/
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
res.json({ message: 'Command received' });
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
// Handle the command disableplugin from Home
|
|
3032
|
+
if (command === 'disableplugin') {
|
|
3033
|
+
if (!this.plugins.has(param)) {
|
|
3034
|
+
this.log.warn(`Plugin ${plg}${param}${wr} not found in matterbridge`);
|
|
3035
|
+
}
|
|
3036
|
+
else {
|
|
3037
|
+
const plugin = this.plugins.get(param);
|
|
3038
|
+
if (plugin && plugin.enabled) {
|
|
3039
|
+
await this.plugins.shutdown(plugin, 'The plugin has been disabled.', true);
|
|
3040
|
+
await this.plugins.disable(param);
|
|
3357
3041
|
}
|
|
3358
3042
|
}
|
|
3043
|
+
res.json({ message: 'Command received' });
|
|
3044
|
+
return;
|
|
3359
3045
|
}
|
|
3360
|
-
res.json({ message: 'Command received' });
|
|
3361
3046
|
});
|
|
3362
3047
|
// Fallback for routing (must be the last route)
|
|
3363
3048
|
this.expressApp.get('*', (req, res) => {
|
|
@@ -3425,5 +3110,104 @@ export class Matterbridge extends EventEmitter {
|
|
|
3425
3110
|
});
|
|
3426
3111
|
return attributes;
|
|
3427
3112
|
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Initializes the Matterbridge instance as extension for zigbee2mqtt.
|
|
3115
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
3116
|
+
*
|
|
3117
|
+
* @returns A Promise that resolves when the initialization is complete.
|
|
3118
|
+
*/
|
|
3119
|
+
async startExtension(dataPath, debugEnabled, extensionVersion, port = 5560) {
|
|
3120
|
+
// Set the bridge mode
|
|
3121
|
+
this.bridgeMode = 'bridge';
|
|
3122
|
+
// Set the first port to use
|
|
3123
|
+
this.port = port;
|
|
3124
|
+
// Set Matterbridge logger
|
|
3125
|
+
this.debugEnabled = debugEnabled;
|
|
3126
|
+
this.log = new AnsiLogger({ logName: 'Matterbridge', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: this.debugEnabled });
|
|
3127
|
+
this.log.debug('Matterbridge extension is starting...');
|
|
3128
|
+
// Initialize NodeStorage
|
|
3129
|
+
this.matterbridgeDirectory = dataPath;
|
|
3130
|
+
this.log.debug('Creating node storage manager dir: ' + path.join(this.matterbridgeDirectory, 'node_storage'));
|
|
3131
|
+
this.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridgeDirectory, 'node_storage'), logging: false });
|
|
3132
|
+
this.log.debug('Creating node storage context for matterbridge: matterbridge');
|
|
3133
|
+
this.nodeContext = await this.nodeStorage.createStorage('matterbridge');
|
|
3134
|
+
const plugin = {
|
|
3135
|
+
path: '',
|
|
3136
|
+
type: 'DynamicPlatform',
|
|
3137
|
+
name: 'MatterbridgeExtension',
|
|
3138
|
+
version: '1.0.0',
|
|
3139
|
+
description: 'Matterbridge extension',
|
|
3140
|
+
author: 'https://github.com/Luligu',
|
|
3141
|
+
enabled: false,
|
|
3142
|
+
registeredDevices: 0,
|
|
3143
|
+
addedDevices: 0,
|
|
3144
|
+
};
|
|
3145
|
+
this.plugins.set(plugin);
|
|
3146
|
+
this.plugins.saveToStorage();
|
|
3147
|
+
// Log system info and create .matterbridge directory
|
|
3148
|
+
await this.logNodeAndSystemInfo();
|
|
3149
|
+
this.matterbridgeDirectory = dataPath;
|
|
3150
|
+
// Set matter.js logger level and format
|
|
3151
|
+
Logger.defaultLogLevel = this.debugEnabled ? Level.DEBUG : Level.INFO;
|
|
3152
|
+
Logger.format = Format.ANSI;
|
|
3153
|
+
// Start the storage and create matterbridgeContext
|
|
3154
|
+
await this.startStorage('json', path.join(this.matterbridgeDirectory, this.matterStorageName));
|
|
3155
|
+
if (!this.storageManager)
|
|
3156
|
+
return false;
|
|
3157
|
+
this.matterbridgeContext = await this.createCommissioningServerContext('Matterbridge', 'Matterbridge zigbee2MQTT', DeviceTypes.AGGREGATOR.code, 0xfff1, 'Matterbridge', 0x8000, 'zigbee2MQTT Matter extension');
|
|
3158
|
+
if (!this.matterbridgeContext)
|
|
3159
|
+
return false;
|
|
3160
|
+
await this.matterbridgeContext.set('softwareVersion', 1);
|
|
3161
|
+
await this.matterbridgeContext.set('softwareVersionString', this.matterbridgeVersion);
|
|
3162
|
+
await this.matterbridgeContext.set('hardwareVersion', 1);
|
|
3163
|
+
await this.matterbridgeContext.set('hardwareVersionString', extensionVersion); // Update with the extension version
|
|
3164
|
+
this.matterServer = this.createMatterServer(this.storageManager);
|
|
3165
|
+
this.log.debug(`Creating commissioning server for ${plg}Matterbridge${db}`);
|
|
3166
|
+
this.commissioningServer = await this.createCommisioningServer(this.matterbridgeContext, 'Matterbridge');
|
|
3167
|
+
this.log.debug(`Creating matter aggregator for ${plg}Matterbridge${db}`);
|
|
3168
|
+
this.matterAggregator = await this.createMatterAggregator(this.matterbridgeContext, 'Matterbridge');
|
|
3169
|
+
this.log.debug('Adding matterbridge aggregator to commissioning server');
|
|
3170
|
+
this.commissioningServer.addDevice(this.matterAggregator);
|
|
3171
|
+
this.log.debug('Adding matterbridge commissioning server to matter server');
|
|
3172
|
+
await this.matterServer.addCommissioningServer(this.commissioningServer, { uniqueStorageKey: 'Matterbridge' });
|
|
3173
|
+
await this.startMatterServer();
|
|
3174
|
+
this.log.info('Matter server started');
|
|
3175
|
+
await this.showCommissioningQRCode(this.commissioningServer, this.matterbridgeContext, this.nodeContext, 'Matterbridge');
|
|
3176
|
+
// Set reachability to true and trigger event after 60 seconds
|
|
3177
|
+
setTimeout(() => {
|
|
3178
|
+
this.log.info(`Setting reachability to true for ${plg}Matterbridge${db}`);
|
|
3179
|
+
if (this.commissioningServer)
|
|
3180
|
+
this.setCommissioningServerReachability(this.commissioningServer, true);
|
|
3181
|
+
if (this.matterAggregator)
|
|
3182
|
+
this.setAggregatorReachability(this.matterAggregator, true);
|
|
3183
|
+
}, 60 * 1000);
|
|
3184
|
+
return this.commissioningServer.isCommissioned();
|
|
3185
|
+
}
|
|
3186
|
+
/**
|
|
3187
|
+
* Close the Matterbridge instance as extension for zigbee2mqtt.
|
|
3188
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
3189
|
+
*
|
|
3190
|
+
* @returns A Promise that resolves when the initialization is complete.
|
|
3191
|
+
*/
|
|
3192
|
+
async stopExtension() {
|
|
3193
|
+
// Closing matter
|
|
3194
|
+
await this.stopMatterServer();
|
|
3195
|
+
// Clearing the session manager
|
|
3196
|
+
// this.matterbridgeContext?.createContext('SessionManager').clear();
|
|
3197
|
+
// Closing storage
|
|
3198
|
+
await this.stopStorage();
|
|
3199
|
+
this.log.info('Matter server stopped');
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Checks if the extension is commissioned.
|
|
3203
|
+
* @deprecated This method is deprecated and will be removed in a future version.
|
|
3204
|
+
*
|
|
3205
|
+
* @returns {boolean} Returns true if the extension is commissioned, false otherwise.
|
|
3206
|
+
*/
|
|
3207
|
+
isExtensionCommissioned() {
|
|
3208
|
+
if (!this.commissioningServer)
|
|
3209
|
+
return false;
|
|
3210
|
+
return this.commissioningServer.isCommissioned();
|
|
3211
|
+
}
|
|
3428
3212
|
}
|
|
3429
3213
|
//# sourceMappingURL=matterbridge.js.map
|