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/plugins.js
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the Plugins class.
|
|
3
|
+
*
|
|
4
|
+
* @file plugins.ts
|
|
5
|
+
* @author Luca Liguori
|
|
6
|
+
* @date 2024-07-14
|
|
7
|
+
* @version 1.2.8
|
|
8
|
+
*
|
|
9
|
+
* Copyright 2024 Luca Liguori.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License. *
|
|
22
|
+
*/
|
|
23
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
24
|
+
import { AnsiLogger, BLUE, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, wr } from 'node-ansi-logger';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { promises as fs } from 'fs';
|
|
27
|
+
import { pathToFileURL } from 'url';
|
|
28
|
+
import { exec } from 'child_process';
|
|
29
|
+
import { shelly_config, somfytahoma_config, zigbee2mqtt_config } from './defaultConfigSchema.js';
|
|
30
|
+
// Default colors
|
|
31
|
+
const plg = '\u001B[38;5;33m';
|
|
32
|
+
const dev = '\u001B[38;5;79m';
|
|
33
|
+
const typ = '\u001B[38;5;207m';
|
|
34
|
+
export class Plugins {
|
|
35
|
+
_plugins = new Map();
|
|
36
|
+
nodeContext;
|
|
37
|
+
matterbridge;
|
|
38
|
+
log;
|
|
39
|
+
constructor(matterbridge) {
|
|
40
|
+
this.matterbridge = matterbridge;
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
this.nodeContext = matterbridge.nodeContext;
|
|
43
|
+
this.log = new AnsiLogger({ logName: 'MatterbridgePluginsManager', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: matterbridge.debugEnabled });
|
|
44
|
+
this.log.debug('Matterbridge plugin manager starting...');
|
|
45
|
+
}
|
|
46
|
+
get length() {
|
|
47
|
+
return this._plugins.size;
|
|
48
|
+
}
|
|
49
|
+
get size() {
|
|
50
|
+
return this._plugins.size;
|
|
51
|
+
}
|
|
52
|
+
has(name) {
|
|
53
|
+
return this._plugins.has(name);
|
|
54
|
+
}
|
|
55
|
+
get(name) {
|
|
56
|
+
return this._plugins.get(name);
|
|
57
|
+
}
|
|
58
|
+
set(plugin) {
|
|
59
|
+
this._plugins.set(plugin.name, plugin);
|
|
60
|
+
return plugin;
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this._plugins.clear();
|
|
64
|
+
}
|
|
65
|
+
array() {
|
|
66
|
+
return Array.from(this._plugins.values());
|
|
67
|
+
}
|
|
68
|
+
[Symbol.iterator]() {
|
|
69
|
+
return this._plugins.values();
|
|
70
|
+
}
|
|
71
|
+
async forEach(callback) {
|
|
72
|
+
const tasks = Array.from(this._plugins.values()).map(async (plugin) => {
|
|
73
|
+
await callback(plugin);
|
|
74
|
+
});
|
|
75
|
+
await Promise.all(tasks);
|
|
76
|
+
}
|
|
77
|
+
async loadFromStorage() {
|
|
78
|
+
// Load the array from storage and convert it to a map
|
|
79
|
+
const pluginsArray = await this.nodeContext.get('plugins', []);
|
|
80
|
+
for (const plugin of pluginsArray)
|
|
81
|
+
this._plugins.set(plugin.name, plugin);
|
|
82
|
+
return pluginsArray;
|
|
83
|
+
}
|
|
84
|
+
async saveToStorage() {
|
|
85
|
+
// Convert the map to an array
|
|
86
|
+
const plugins = [];
|
|
87
|
+
const pluginArrayFromMap = Array.from(this._plugins.values());
|
|
88
|
+
for (const plugin of pluginArrayFromMap) {
|
|
89
|
+
plugins.push({
|
|
90
|
+
name: plugin.name,
|
|
91
|
+
path: plugin.path,
|
|
92
|
+
type: plugin.type,
|
|
93
|
+
version: plugin.version,
|
|
94
|
+
description: plugin.description,
|
|
95
|
+
author: plugin.author,
|
|
96
|
+
enabled: plugin.enabled,
|
|
97
|
+
qrPairingCode: plugin.qrPairingCode,
|
|
98
|
+
manualPairingCode: plugin.manualPairingCode,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
await this.nodeContext.set('plugins', plugins);
|
|
102
|
+
this.log.debug(`Saved ${BLUE}${plugins.length}${db} plugins to storage`);
|
|
103
|
+
return plugins.length;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolves the name of a plugin by loading and parsing its package.json file.
|
|
107
|
+
* @param pluginPath - The path to the plugin or the path to the plugin's package.json file.
|
|
108
|
+
* @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.
|
|
109
|
+
*/
|
|
110
|
+
async resolve(pluginPath) {
|
|
111
|
+
if (!pluginPath.endsWith('package.json'))
|
|
112
|
+
pluginPath = path.join(pluginPath, 'package.json');
|
|
113
|
+
// Resolve the package.json of the plugin
|
|
114
|
+
let packageJsonPath = path.resolve(pluginPath);
|
|
115
|
+
this.log.debug(`Resolving plugin path ${plg}${packageJsonPath}${db}`);
|
|
116
|
+
// Check if the package.json file exists
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(packageJsonPath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.log.debug(`Package.json not found at ${plg}${packageJsonPath}${db}`);
|
|
122
|
+
packageJsonPath = path.join(this.matterbridge.globalModulesDirectory, pluginPath);
|
|
123
|
+
this.log.debug(`Trying at ${plg}${packageJsonPath}${db}`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
// Load the package.json of the plugin
|
|
127
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
128
|
+
if (!packageJson.name) {
|
|
129
|
+
this.log.debug(`Package.json name not found at ${packageJsonPath}`);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
this.log.debug(`Resolved plugin path ${plg}${pluginPath}${db}: ${packageJsonPath}`);
|
|
133
|
+
return packageJsonPath;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.log.error(`Failed to resolve plugin path ${plg}${pluginPath}${er}: ${err}`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Loads and parse the plugin package.json and returns it.
|
|
142
|
+
* @param plugin - The plugin to load the package from.
|
|
143
|
+
* @returns A Promise that resolves to the package.json object or undefined if the package.json could not be loaded.
|
|
144
|
+
*/
|
|
145
|
+
async parse(plugin) {
|
|
146
|
+
this.log.debug(`Parsing package.json of plugin ${plg}${plugin.name}${db}`);
|
|
147
|
+
try {
|
|
148
|
+
const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
|
|
149
|
+
if (!plugin.name)
|
|
150
|
+
this.log.warn(`Plugin ${plg}${plugin.name}${wr} has no name in package.json`);
|
|
151
|
+
if (!plugin.version)
|
|
152
|
+
this.log.warn(`Plugin ${plg}${plugin.name}${wr} has no version in package.json`);
|
|
153
|
+
plugin.name = packageJson.name || 'Unknown name';
|
|
154
|
+
plugin.version = packageJson.version || '1.0.0';
|
|
155
|
+
plugin.description = packageJson.description || 'Unknown description';
|
|
156
|
+
plugin.author = packageJson.author || 'Unknown author';
|
|
157
|
+
if (!plugin.path)
|
|
158
|
+
this.log.warn(`Plugin ${plg}${plugin.name}${wr} has no path`);
|
|
159
|
+
if (!plugin.type)
|
|
160
|
+
this.log.warn(`Plugin ${plg}${plugin.name}${wr} has no type`);
|
|
161
|
+
// await this.saveToStorage();
|
|
162
|
+
return packageJson;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
this.log.error(`Failed to parse package.json of plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
166
|
+
plugin.error = true;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async enable(nameOrPath) {
|
|
171
|
+
if (!nameOrPath || nameOrPath === '')
|
|
172
|
+
return null;
|
|
173
|
+
if (this._plugins.has(nameOrPath)) {
|
|
174
|
+
const plugin = this._plugins.get(nameOrPath);
|
|
175
|
+
plugin.enabled = true;
|
|
176
|
+
this.log.info(`Enabled plugin ${plg}${plugin.name}${nf}`);
|
|
177
|
+
await this.saveToStorage();
|
|
178
|
+
return plugin;
|
|
179
|
+
}
|
|
180
|
+
const packageJsonPath = await this.resolve(nameOrPath);
|
|
181
|
+
if (!packageJsonPath) {
|
|
182
|
+
this.log.error(`Failed to enable plugin ${plg}${nameOrPath}${er}: package.json not found`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
187
|
+
const plugin = this._plugins.get(packageJson.name);
|
|
188
|
+
if (!plugin) {
|
|
189
|
+
this.log.error(`Failed to enable plugin ${plg}${nameOrPath}${er}: plugin not registered`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
plugin.enabled = true;
|
|
193
|
+
this.log.info(`Enabled plugin ${plg}${plugin.name}${nf}`);
|
|
194
|
+
await this.saveToStorage();
|
|
195
|
+
return plugin;
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
this.log.error(`Failed to parse package.json of plugin ${plg}${nameOrPath}${er}: ${err}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async disable(nameOrPath) {
|
|
203
|
+
if (!nameOrPath || nameOrPath === '')
|
|
204
|
+
return null;
|
|
205
|
+
if (this._plugins.has(nameOrPath)) {
|
|
206
|
+
const plugin = this._plugins.get(nameOrPath);
|
|
207
|
+
plugin.enabled = false;
|
|
208
|
+
this.log.info(`Disabled plugin ${plg}${plugin.name}${nf}`);
|
|
209
|
+
await this.saveToStorage();
|
|
210
|
+
return plugin;
|
|
211
|
+
}
|
|
212
|
+
const packageJsonPath = await this.resolve(nameOrPath);
|
|
213
|
+
if (!packageJsonPath) {
|
|
214
|
+
this.log.error(`Failed to disable plugin ${plg}${nameOrPath}${er}: package.json not found`);
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
219
|
+
const plugin = this._plugins.get(packageJson.name);
|
|
220
|
+
if (!plugin) {
|
|
221
|
+
this.log.error(`Failed to disable plugin ${plg}${nameOrPath}${er}: plugin not registered`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
plugin.enabled = false;
|
|
225
|
+
this.log.info(`Disabled plugin ${plg}${plugin.name}${nf}`);
|
|
226
|
+
await this.saveToStorage();
|
|
227
|
+
return plugin;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
this.log.error(`Failed to parse package.json of plugin ${plg}${nameOrPath}${er}: ${err}`);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async remove(nameOrPath) {
|
|
235
|
+
if (!nameOrPath || nameOrPath === '')
|
|
236
|
+
return null;
|
|
237
|
+
if (this._plugins.has(nameOrPath)) {
|
|
238
|
+
const plugin = this._plugins.get(nameOrPath);
|
|
239
|
+
this._plugins.delete(nameOrPath);
|
|
240
|
+
this.log.info(`Removed plugin ${plg}${plugin.name}${nf}`);
|
|
241
|
+
await this.saveToStorage();
|
|
242
|
+
return plugin;
|
|
243
|
+
}
|
|
244
|
+
const packageJsonPath = await this.resolve(nameOrPath);
|
|
245
|
+
if (!packageJsonPath) {
|
|
246
|
+
this.log.error(`Failed to remove plugin ${plg}${nameOrPath}${er}: package.json not found`);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
251
|
+
const plugin = this._plugins.get(packageJson.name);
|
|
252
|
+
if (!plugin) {
|
|
253
|
+
this.log.error(`Failed to remove plugin ${plg}${nameOrPath}${er}: plugin not registered`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
this._plugins.delete(packageJson.name);
|
|
257
|
+
this.log.info(`Removed plugin ${plg}${plugin.name}${nf}`);
|
|
258
|
+
await this.saveToStorage();
|
|
259
|
+
return plugin;
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
this.log.error(`Failed to parse package.json of plugin ${plg}${nameOrPath}${er}: ${err}`);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async add(nameOrPath) {
|
|
267
|
+
if (!nameOrPath || nameOrPath === '')
|
|
268
|
+
return null;
|
|
269
|
+
const packageJsonPath = await this.resolve(nameOrPath);
|
|
270
|
+
if (!packageJsonPath) {
|
|
271
|
+
this.log.error(`Failed to add plugin ${plg}${nameOrPath}${er}: package.json not found`);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
276
|
+
if (this._plugins.get(packageJson.name)) {
|
|
277
|
+
this.log.warn(`Failed to add plugin ${plg}${nameOrPath}${wr}: plugin already registered`);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
this._plugins.set(packageJson.name, { name: packageJson.name, enabled: true, path: packageJsonPath, type: '', version: packageJson.version, description: packageJson.description, author: packageJson.author });
|
|
281
|
+
this.log.info(`Added plugin ${plg}${packageJson.name}${nf}`);
|
|
282
|
+
await this.saveToStorage();
|
|
283
|
+
const plugin = this._plugins.get(packageJson.name);
|
|
284
|
+
return plugin || null;
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
this.log.error(`Failed to parse package.json of plugin ${plg}${nameOrPath}${er}: ${err}`);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async install(name) {
|
|
292
|
+
this.log.info(`Installing plugin ${plg}${name}${nf}`);
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
exec(`npm install -g ${name}`, (error, stdout, stderr) => {
|
|
295
|
+
if (error) {
|
|
296
|
+
this.log.error(`Failed to install plugin ${plg}${name}${er}: ${error}`);
|
|
297
|
+
this.log.debug(`Failed to install plugin ${plg}${name}${db}: ${stderr}`);
|
|
298
|
+
resolve(undefined);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.log.info(`Installed plugin ${plg}${name}${nf}`);
|
|
302
|
+
this.log.debug(`Installed plugin ${plg}${name}${db}: ${stdout}`);
|
|
303
|
+
// Get the installed version
|
|
304
|
+
exec(`npm list -g ${name} --depth=0`, (listError, listStdout, listStderr) => {
|
|
305
|
+
if (listError) {
|
|
306
|
+
this.log.error(`List error: ${listError}`);
|
|
307
|
+
resolve(undefined);
|
|
308
|
+
}
|
|
309
|
+
// Clean the output to get only the package name and version
|
|
310
|
+
const lines = listStdout.split('\n');
|
|
311
|
+
const versionLine = lines.find((line) => line.includes(`${name}@`));
|
|
312
|
+
if (versionLine) {
|
|
313
|
+
const version = versionLine.split('@')[1].trim();
|
|
314
|
+
this.log.info(`Installed plugin ${plg}${name}@${version}${nf}`);
|
|
315
|
+
resolve(version);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
resolve(undefined);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async uninstall(name) {
|
|
326
|
+
this.log.info(`Uninstalling plugin ${plg}${name}${nf}`);
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
exec(`npm uninstall -g ${name}`, (error, stdout, stderr) => {
|
|
329
|
+
if (error) {
|
|
330
|
+
this.log.error(`Failed to uninstall plugin ${plg}${name}${er}: ${error}`);
|
|
331
|
+
this.log.debug(`Failed to uninstall plugin ${plg}${name}${db}: ${stderr}`);
|
|
332
|
+
// console.error(`Failed to uninstall plugin ${plg}${name}${er}: ${stderr}`);
|
|
333
|
+
resolve(undefined);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.log.info(`Uninstalled plugin ${plg}${name}${nf}`);
|
|
337
|
+
this.log.debug(`Uninstalled plugin ${plg}${name}${db}: ${stdout}`);
|
|
338
|
+
// console.error(`Uninstalled plugin ${plg}${name}${nf}: ${stdout}`);
|
|
339
|
+
resolve(name);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Loads a plugin and returns the corresponding MatterbridgePlatform instance.
|
|
346
|
+
* @param plugin - The plugin to load.
|
|
347
|
+
* @param start - Optional flag indicating whether to start the plugin after loading. Default is false.
|
|
348
|
+
* @param message - Optional message to pass to the plugin when starting.
|
|
349
|
+
* @returns A Promise that resolves to the loaded MatterbridgePlatform instance.
|
|
350
|
+
* @throws An error if the plugin is not enabled, already loaded, or fails to load.
|
|
351
|
+
*/
|
|
352
|
+
async load(plugin, start = false, message = '', configure = false) {
|
|
353
|
+
if (!plugin.enabled) {
|
|
354
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not enabled`);
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
if (plugin.platform) {
|
|
358
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} already loaded`);
|
|
359
|
+
return plugin.platform;
|
|
360
|
+
}
|
|
361
|
+
this.log.info(`Loading plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
362
|
+
try {
|
|
363
|
+
// Load the package.json of the plugin
|
|
364
|
+
const packageJson = JSON.parse(await fs.readFile(plugin.path, 'utf8'));
|
|
365
|
+
// Resolve the main module path relative to package.json
|
|
366
|
+
const pluginEntry = path.resolve(path.dirname(plugin.path), packageJson.main);
|
|
367
|
+
// Dynamically import the plugin
|
|
368
|
+
const pluginUrl = pathToFileURL(pluginEntry);
|
|
369
|
+
this.log.debug(`Importing plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
|
|
370
|
+
const pluginInstance = await import(pluginUrl.href);
|
|
371
|
+
this.log.debug(`Imported plugin ${plg}${plugin.name}${db} from ${pluginUrl.href}`);
|
|
372
|
+
// Call the default export function of the plugin, passing this MatterBridge instance, the log and the config
|
|
373
|
+
if (pluginInstance.default) {
|
|
374
|
+
const config = await this.loadConfig(plugin);
|
|
375
|
+
const log = new AnsiLogger({ logName: plugin.description ?? 'No description', logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logDebug: config.debug ?? false });
|
|
376
|
+
const platform = pluginInstance.default(this.matterbridge, log, config);
|
|
377
|
+
config.type = platform.type;
|
|
378
|
+
platform.name = packageJson.name;
|
|
379
|
+
platform.config = config;
|
|
380
|
+
platform.version = packageJson.version;
|
|
381
|
+
plugin.name = packageJson.name;
|
|
382
|
+
plugin.description = packageJson.description ?? 'No description';
|
|
383
|
+
plugin.version = packageJson.version;
|
|
384
|
+
plugin.author = packageJson.author ?? 'Unknown';
|
|
385
|
+
plugin.type = platform.type;
|
|
386
|
+
plugin.platform = platform;
|
|
387
|
+
plugin.loaded = true;
|
|
388
|
+
plugin.registeredDevices = 0;
|
|
389
|
+
plugin.addedDevices = 0;
|
|
390
|
+
plugin.configJson = config;
|
|
391
|
+
plugin.schemaJson = await this.loadSchema(plugin);
|
|
392
|
+
this.log.info(`Loaded plugin ${plg}${plugin.name}${nf} type ${typ}${platform.type}${db} (entrypoint ${UNDERLINE}${pluginEntry}${UNDERLINEOFF})`);
|
|
393
|
+
if (start)
|
|
394
|
+
await this.start(plugin, message, false);
|
|
395
|
+
if (configure)
|
|
396
|
+
await this.configure(plugin);
|
|
397
|
+
return platform;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} does not provide a default export`);
|
|
401
|
+
plugin.error = true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
this.log.error(`Failed to load plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
406
|
+
plugin.error = true;
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Starts a plugin.
|
|
412
|
+
*
|
|
413
|
+
* @param {RegisteredPlugin} plugin - The plugin to start.
|
|
414
|
+
* @param {string} [message] - Optional message to pass to the plugin's onStart method.
|
|
415
|
+
* @param {boolean} [configure] - Indicates whether to configure the plugin after starting (default false).
|
|
416
|
+
* @returns {Promise<RegisteredPlugin | undefined>} A promise that resolves when the plugin is started successfully, or rejects with an error if starting the plugin fails.
|
|
417
|
+
*/
|
|
418
|
+
async start(plugin, message, configure = false) {
|
|
419
|
+
if (!plugin.loaded) {
|
|
420
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded`);
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
if (!plugin.platform) {
|
|
424
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} no platform found`);
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
if (plugin.started) {
|
|
428
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} already started`);
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
this.log.info(`Starting plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
|
|
432
|
+
try {
|
|
433
|
+
await plugin.platform.onStart(message);
|
|
434
|
+
this.log.info(`Started plugin ${plg}${plugin.name}${db} type ${typ}${plugin.type}${db}`);
|
|
435
|
+
plugin.started = true;
|
|
436
|
+
if (configure)
|
|
437
|
+
await this.configure(plugin);
|
|
438
|
+
return plugin;
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
plugin.error = true;
|
|
442
|
+
this.log.error(`Failed to start plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Configures a plugin.
|
|
448
|
+
*
|
|
449
|
+
* @param {RegisteredPlugin} plugin - The plugin to configure.
|
|
450
|
+
* @returns {Promise<void>} A promise that resolves when the plugin is configured successfully, or rejects with an error if configuration fails.
|
|
451
|
+
*/
|
|
452
|
+
async configure(plugin) {
|
|
453
|
+
if (!plugin.loaded) {
|
|
454
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not loaded`);
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
if (!plugin.started) {
|
|
458
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} not started`);
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
if (!plugin.platform) {
|
|
462
|
+
this.log.error(`Plugin ${plg}${plugin.name}${er} no platform found`);
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
465
|
+
if (plugin.configured) {
|
|
466
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} already configured`);
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
this.log.info(`Configuring plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
470
|
+
try {
|
|
471
|
+
await plugin.platform.onConfigure();
|
|
472
|
+
this.log.info(`Configured plugin ${plg}${plugin.name}${nf} type ${typ}${plugin.type}${nf}`);
|
|
473
|
+
plugin.configured = true;
|
|
474
|
+
await this.saveConfigFromPlugin(plugin);
|
|
475
|
+
return plugin;
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
plugin.error = true;
|
|
479
|
+
this.log.error(`Failed to configure plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
480
|
+
}
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
async shutdown(plugin, reason, removeAllDevices = false, force = false) {
|
|
484
|
+
this.log.debug(`Shutting down plugin ${plg}${plugin.name}${db}`);
|
|
485
|
+
if (!plugin.loaded) {
|
|
486
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} not loaded`);
|
|
487
|
+
if (!force)
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
if (!plugin.started) {
|
|
491
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} not started`);
|
|
492
|
+
if (!force)
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
if (!plugin.configured) {
|
|
496
|
+
this.log.debug(`Plugin ${plg}${plugin.name}${db} not configured`);
|
|
497
|
+
}
|
|
498
|
+
if (!plugin.platform) {
|
|
499
|
+
this.log.debug(`*Plugin ${plg}${plugin.name}${db} no platform found`);
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
this.log.info(`Shutting down plugin ${plg}${plugin.name}${nf}: ${reason}...`);
|
|
503
|
+
try {
|
|
504
|
+
await plugin.platform.onShutdown(reason);
|
|
505
|
+
plugin.locked = undefined;
|
|
506
|
+
plugin.error = undefined;
|
|
507
|
+
plugin.loaded = undefined;
|
|
508
|
+
plugin.started = undefined;
|
|
509
|
+
plugin.configured = undefined;
|
|
510
|
+
plugin.connected = undefined;
|
|
511
|
+
plugin.platform = undefined;
|
|
512
|
+
if (removeAllDevices) {
|
|
513
|
+
this.log.info(`Removing all devices for plugin ${plg}${plugin.name}${nf}: ${reason}...`);
|
|
514
|
+
await this.matterbridge.removeAllBridgedDevices(plugin.name);
|
|
515
|
+
}
|
|
516
|
+
plugin.registeredDevices = undefined;
|
|
517
|
+
plugin.addedDevices = undefined;
|
|
518
|
+
this.log.info(`Shutdown of plugin ${plg}${plugin.name}${nf} completed`);
|
|
519
|
+
return plugin;
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
this.log.error(`Failed to shut down plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
523
|
+
}
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Loads the configuration for a plugin.
|
|
528
|
+
* If the configuration file exists, it reads the file and returns the parsed JSON data.
|
|
529
|
+
* If the configuration file does not exist, it creates a new file with default configuration and returns it.
|
|
530
|
+
* If any error occurs during file access or creation, it logs an error and return un empty config.
|
|
531
|
+
*
|
|
532
|
+
* @param plugin - The plugin for which to load the configuration.
|
|
533
|
+
* @returns A promise that resolves to the loaded or created configuration.
|
|
534
|
+
*/
|
|
535
|
+
async loadConfig(plugin) {
|
|
536
|
+
const configFile = path.join(this.matterbridge.matterbridgeDirectory, `${plugin.name}.config.json`);
|
|
537
|
+
try {
|
|
538
|
+
await fs.access(configFile);
|
|
539
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
540
|
+
const config = JSON.parse(data);
|
|
541
|
+
this.log.debug(`Loaded config file ${configFile} for plugin ${plg}${plugin.name}${db}.`);
|
|
542
|
+
// this.log.debug(`Loaded config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, config);
|
|
543
|
+
/* The first time a plugin is added to the system, the config file is created with the plugin name and type "".*/
|
|
544
|
+
config.name = plugin.name;
|
|
545
|
+
config.type = plugin.type;
|
|
546
|
+
if (config.debug === undefined)
|
|
547
|
+
config.debug = false;
|
|
548
|
+
if (config.unregisterOnShutdown === undefined)
|
|
549
|
+
config.unregisterOnShutdown = false;
|
|
550
|
+
return config;
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
if (err) {
|
|
554
|
+
const nodeErr = err;
|
|
555
|
+
if (nodeErr.code === 'ENOENT') {
|
|
556
|
+
let config;
|
|
557
|
+
if (plugin.name === 'matterbridge-zigbee2mqtt')
|
|
558
|
+
config = zigbee2mqtt_config;
|
|
559
|
+
else if (plugin.name === 'matterbridge-somfy-tahoma')
|
|
560
|
+
config = somfytahoma_config;
|
|
561
|
+
else if (plugin.name === 'matterbridge-shelly')
|
|
562
|
+
config = shelly_config;
|
|
563
|
+
else
|
|
564
|
+
config = { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
|
|
565
|
+
try {
|
|
566
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
567
|
+
this.log.debug(`Created config file ${configFile} for plugin ${plg}${plugin.name}${db}.`);
|
|
568
|
+
// this.log.debug(`Created config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, config);
|
|
569
|
+
return config;
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
this.log.error(`Error creating config file ${configFile} for plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
573
|
+
return config;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
this.log.error(`Error accessing config file ${configFile} for plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
578
|
+
return { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
this.log.error(`Error loading config file ${configFile} for plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
582
|
+
return { name: plugin.name, type: plugin.type, debug: false, unregisterOnShutdown: false };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async saveConfigFromPlugin(plugin) {
|
|
586
|
+
if (!plugin.platform?.config) {
|
|
587
|
+
this.log.error(`Error saving config file for plugin ${plg}${plugin.name}${er}: config not found`);
|
|
588
|
+
return Promise.reject(new Error(`Error saving config file for plugin ${plg}${plugin.name}${er}: config not found`));
|
|
589
|
+
}
|
|
590
|
+
const configFile = path.join(this.matterbridge.matterbridgeDirectory, `${plugin.name}.config.json`);
|
|
591
|
+
try {
|
|
592
|
+
await fs.writeFile(configFile, JSON.stringify(plugin.platform.config, null, 2), 'utf8');
|
|
593
|
+
this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}`);
|
|
594
|
+
// this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, plugin.platform.config);
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
this.log.error(`Error saving config file ${configFile} for plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
598
|
+
return Promise.reject(err);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async saveConfigFromJson(plugin, config) {
|
|
602
|
+
if (!config.name || !config.type || config.name !== plugin.name) {
|
|
603
|
+
this.log.error(`Error saving config file for plugin ${plg}${plugin.name}${er}. Wrong config data content:${rs}\n`, config);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const configFile = path.join(this.matterbridge.matterbridgeDirectory, `${plugin.name}.config.json`);
|
|
607
|
+
try {
|
|
608
|
+
await fs.writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
609
|
+
plugin.configJson = config;
|
|
610
|
+
this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}`);
|
|
611
|
+
// this.log.debug(`Saved config file ${configFile} for plugin ${plg}${plugin.name}${db}.\nConfig:${rs}\n`, config);
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
this.log.error(`Error saving config file ${configFile} for plugin ${plg}${plugin.name}${er}: ${err}`);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async loadSchema(plugin) {
|
|
619
|
+
let schemaFile = plugin.path.replace('package.json', `${plugin.name}.schema.json`);
|
|
620
|
+
try {
|
|
621
|
+
await fs.access(schemaFile);
|
|
622
|
+
const data = await fs.readFile(schemaFile, 'utf8');
|
|
623
|
+
const schema = JSON.parse(data);
|
|
624
|
+
schema.title = plugin.description;
|
|
625
|
+
schema.description = plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author;
|
|
626
|
+
this.log.debug(`Loaded schema file ${schemaFile} for plugin ${plg}${plugin.name}${db}.`);
|
|
627
|
+
// this.log.debug(`Loaded schema file ${schemaFile} for plugin ${plg}${plugin.name}${db}.\nSchema:${rs}\n`, schema);
|
|
628
|
+
// Delete the schema file from old position
|
|
629
|
+
schemaFile = path.join(this.matterbridge.matterbridgeDirectory, `${plugin.name}.schema.json`);
|
|
630
|
+
try {
|
|
631
|
+
await fs.unlink(schemaFile);
|
|
632
|
+
this.log.debug(`Schema file ${schemaFile} deleted.`);
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
this.log.debug(`Schema file ${schemaFile} to delete not found.`);
|
|
636
|
+
}
|
|
637
|
+
return schema;
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
this.log.debug(`Schema file ${schemaFile} for plugin ${plg}${plugin.name}${db} not found. Loading default schema.`);
|
|
641
|
+
return this.getDefaultSchema(plugin);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
getDefaultSchema(plugin) {
|
|
645
|
+
return {
|
|
646
|
+
title: plugin.description,
|
|
647
|
+
description: plugin.name + ' v. ' + plugin.version + ' by ' + plugin.author,
|
|
648
|
+
type: 'object',
|
|
649
|
+
properties: {
|
|
650
|
+
name: {
|
|
651
|
+
description: 'Plugin name',
|
|
652
|
+
type: 'string',
|
|
653
|
+
readOnly: true,
|
|
654
|
+
},
|
|
655
|
+
type: {
|
|
656
|
+
description: 'Plugin type',
|
|
657
|
+
type: 'string',
|
|
658
|
+
readOnly: true,
|
|
659
|
+
},
|
|
660
|
+
debug: {
|
|
661
|
+
description: 'Enable the debug for the plugin (development only)',
|
|
662
|
+
type: 'boolean',
|
|
663
|
+
default: false,
|
|
664
|
+
},
|
|
665
|
+
unregisterOnShutdown: {
|
|
666
|
+
description: 'Unregister all devices on shutdown (development only)',
|
|
667
|
+
type: 'boolean',
|
|
668
|
+
default: false,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
//# sourceMappingURL=plugins.js.map
|