matterbridge 3.4.0-dev-20251123-62db0d7 → 3.4.0-dev-20251126-5087664
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 +3 -2
- package/README.md +1 -1
- package/dist/broadcastServer.js +3 -3
- package/dist/jestutils/jestHelpers.js +34 -0
- package/dist/matterNode.js +933 -0
- package/dist/utils/tracker.js +1 -1
- package/marked.ps1 +10 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -28,7 +28,7 @@ Advantages:
|
|
|
28
28
|
- individual plugin isolation in childbridge mode;
|
|
29
29
|
- ability to update the plugin in childbridge mode without restarting matterbridge;
|
|
30
30
|
|
|
31
|
-
## [3.4.0] -
|
|
31
|
+
## [3.4.0] - 2025-11-26
|
|
32
32
|
|
|
33
33
|
### Development Breaking Changes
|
|
34
34
|
|
|
@@ -46,8 +46,9 @@ Removed the following long deprecated elements:
|
|
|
46
46
|
- [doorLock]: Added autoRelockTime attribute with default 0.
|
|
47
47
|
- [DevContainer]: Added instructions for testing a plugin with a paired controller when using DevContainer.
|
|
48
48
|
- [platform]: Made internal use methods and properties hard-private.
|
|
49
|
-
- [platform]: Added size(), getDeviceByName(), getDeviceByUniqueId(), getDeviceBySerialNumber(), getDeviceById(), getDeviceByOriginalId(), getDeviceByNumber() and hasDeviceUniqueId() methods.
|
|
49
|
+
- [platform]: Added size(), getDeviceByName(), getDeviceByUniqueId(), getDeviceBySerialNumber(), getDeviceById(), getDeviceByOriginalId(), getDeviceByNumber() and hasDeviceUniqueId() methods to retrieve a registered device.
|
|
50
50
|
- [platform]: Added isReady, isLoaded, isStarted and isConfigured properties.
|
|
51
|
+
- [matterbridge.io]: Updated website https://matterbridge.io.
|
|
51
52
|
|
|
52
53
|
### Changed
|
|
53
54
|
|
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Test the installation with:
|
|
|
108
108
|
matterbridge
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Now it is possible to open the frontend at the link provided in the log (e.g. http://
|
|
111
|
+
Now it is possible to open the frontend at the link provided in the log (e.g. http://MATTERBRIDGE-IPV4-ADDRESS:8283).
|
|
112
112
|
|
|
113
113
|
You can then change the bridge mode and other parameters from the frontend.
|
|
114
114
|
|
package/dist/broadcastServer.js
CHANGED
|
@@ -2,7 +2,7 @@ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
|
2
2
|
console.log('\u001B[32mBroadcastServer loaded.\u001B[40;0m');
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
4
|
import { BroadcastChannel } from 'node:worker_threads';
|
|
5
|
-
import { debugStringify } from 'node-ansi-logger';
|
|
5
|
+
import { CYAN, db, debugStringify } from 'node-ansi-logger';
|
|
6
6
|
import { hasParameter } from './utils/commandLine.js';
|
|
7
7
|
export class BroadcastServer extends EventEmitter {
|
|
8
8
|
name;
|
|
@@ -34,8 +34,8 @@ export class BroadcastServer extends EventEmitter {
|
|
|
34
34
|
}
|
|
35
35
|
broadcastMessageHandler(event) {
|
|
36
36
|
const data = event.data;
|
|
37
|
-
if (this.verbose)
|
|
38
|
-
this.log.debug(`
|
|
37
|
+
if (this.verbose && (data.dst === this.name || data.dst === 'all'))
|
|
38
|
+
this.log.debug(`Server ${CYAN}${this.name}${db} received broadcast message: ${debugStringify(data)}`);
|
|
39
39
|
this.emit('broadcast_message', data);
|
|
40
40
|
}
|
|
41
41
|
broadcast(message) {
|
|
@@ -325,6 +325,35 @@ export async function flushAsync(ticks = 3, microTurns = 10, pause = 250) {
|
|
|
325
325
|
if (pause)
|
|
326
326
|
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
327
327
|
}
|
|
328
|
+
export function logKeepAlives(log) {
|
|
329
|
+
const handles = process._getActiveHandles?.() ?? [];
|
|
330
|
+
const requests = process._getActiveRequests?.() ?? [];
|
|
331
|
+
const fmtHandle = (h, i) => {
|
|
332
|
+
const ctor = h?.constructor?.name ?? 'Unknown';
|
|
333
|
+
const hasRef = typeof h?.hasRef === 'function' ? h.hasRef() : undefined;
|
|
334
|
+
const isPort = h?.constructor?.name?.includes('MessagePort');
|
|
335
|
+
const fd = h?.fd ?? h?._handle?.fd;
|
|
336
|
+
return { i, type: ctor, hasRef, isPort, fd };
|
|
337
|
+
};
|
|
338
|
+
const fmtReq = (r, i) => {
|
|
339
|
+
const ctor = r?.constructor?.name ?? 'Unknown';
|
|
340
|
+
return { i, type: ctor };
|
|
341
|
+
};
|
|
342
|
+
const summary = {
|
|
343
|
+
handles: handles.map(fmtHandle),
|
|
344
|
+
requests: requests.map(fmtReq),
|
|
345
|
+
};
|
|
346
|
+
if (summary.handles.length === 0 && summary.requests.length === 0) {
|
|
347
|
+
log?.debug('KeepAlive: no active handles or requests.');
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
log?.debug(`KeepAlive:${rs}\n${inspect(summary, { depth: 5, colors: true })}`);
|
|
351
|
+
if (!log) {
|
|
352
|
+
process.stdout.write(`KeepAlive:\n${inspect(summary, { depth: 5, colors: true })}\n`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return summary.handles.length + summary.requests.length;
|
|
356
|
+
}
|
|
328
357
|
export async function flushAllEndpointNumberPersistence(targetServer, rounds = 2) {
|
|
329
358
|
const nodeStore = targetServer.env.get(ServerNodeStore);
|
|
330
359
|
for (let i = 0; i < rounds; i++) {
|
|
@@ -360,6 +389,11 @@ export async function assertAllEndpointNumbersPersisted(targetServer) {
|
|
|
360
389
|
}
|
|
361
390
|
return all.length;
|
|
362
391
|
}
|
|
392
|
+
export async function closeServerNodeStores(targetServer) {
|
|
393
|
+
if (!targetServer)
|
|
394
|
+
targetServer = server;
|
|
395
|
+
await targetServer?.env.get(ServerNodeStore)?.endpointStores.close();
|
|
396
|
+
}
|
|
363
397
|
export async function startServerNode(name, port, deviceType = bridge.code) {
|
|
364
398
|
const { randomBytes } = await import('node:crypto');
|
|
365
399
|
const random = randomBytes(8).toString('hex');
|
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import EventEmitter from 'node:events';
|
|
4
|
+
import { AnsiLogger, BLUE, CYAN, db, debugStringify, er, nf, or, zb } from 'node-ansi-logger';
|
|
5
|
+
import { NodeStorageManager } from 'node-persist-manager';
|
|
6
|
+
import '@matter/nodejs';
|
|
7
|
+
import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, StorageService, UINT32_MAX, UINT16_MAX, Environment } from '@matter/general';
|
|
8
|
+
import { FabricAction, MdnsService } from '@matter/protocol';
|
|
9
|
+
import { VendorId, DeviceTypeId } from '@matter/types';
|
|
10
|
+
import { ServerNode, Endpoint } from '@matter/node';
|
|
11
|
+
import { AggregatorEndpoint } from '@matter/node/endpoints/aggregator';
|
|
12
|
+
import { BasicInformationServer } from '@matter/node/behaviors/basic-information';
|
|
13
|
+
import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
|
|
14
|
+
import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_NAME, plg, NODE_STORAGE_DIR, MATTERBRIDGE_LOGGER_FILE } from './matterbridgeTypes.js';
|
|
15
|
+
import { bridge } from './matterbridgeDeviceTypes.js';
|
|
16
|
+
import { getIntParameter, getParameter, hasParameter } from './utils/commandLine.js';
|
|
17
|
+
import { copyDirectory } from './utils/copyDirectory.js';
|
|
18
|
+
import { isValidNumber, isValidString, parseVersionString } from './utils/isvalid.js';
|
|
19
|
+
import { wait, withTimeout } from './utils/wait.js';
|
|
20
|
+
import { inspectError } from './utils/error.js';
|
|
21
|
+
import { BroadcastServer } from './broadcastServer.js';
|
|
22
|
+
import { toBaseDevice } from './deviceManager.js';
|
|
23
|
+
import { PluginManager } from './pluginManager.js';
|
|
24
|
+
import { addVirtualDevice } from './helpers.js';
|
|
25
|
+
export class MatterNode extends EventEmitter {
|
|
26
|
+
matterbridge;
|
|
27
|
+
pluginName;
|
|
28
|
+
device;
|
|
29
|
+
log = new AnsiLogger({ logName: 'MatterNode', logTimestampFormat: 4, logLevel: "debug" });
|
|
30
|
+
matterLog = new AnsiLogger({ logName: 'MatterNode', logTimestampFormat: 4, logLevel: "debug" });
|
|
31
|
+
environment = Environment.default;
|
|
32
|
+
storeId;
|
|
33
|
+
matterMdnsService;
|
|
34
|
+
matterStorageService;
|
|
35
|
+
matterStorageManager;
|
|
36
|
+
matterStorageContext;
|
|
37
|
+
mdnsInterface;
|
|
38
|
+
ipv4Address;
|
|
39
|
+
ipv6Address;
|
|
40
|
+
port;
|
|
41
|
+
passcode;
|
|
42
|
+
discriminator;
|
|
43
|
+
certification;
|
|
44
|
+
serverNode;
|
|
45
|
+
aggregatorNode;
|
|
46
|
+
aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
|
|
47
|
+
aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
|
|
48
|
+
aggregatorProductId = getIntParameter('productId') ?? 0x8000;
|
|
49
|
+
aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
|
|
50
|
+
aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
|
|
51
|
+
aggregatorSerialNumber = getParameter('serialNumber');
|
|
52
|
+
aggregatorUniqueId = getParameter('uniqueId');
|
|
53
|
+
advertisingNodes = new Map();
|
|
54
|
+
pluginManager;
|
|
55
|
+
dependantMatterNodes = new Map();
|
|
56
|
+
server;
|
|
57
|
+
debug = hasParameter('debug') || hasParameter('verbose');
|
|
58
|
+
verbose = hasParameter('verbose');
|
|
59
|
+
constructor(matterbridge, pluginName, device) {
|
|
60
|
+
super();
|
|
61
|
+
this.matterbridge = matterbridge;
|
|
62
|
+
this.pluginName = pluginName;
|
|
63
|
+
this.device = device;
|
|
64
|
+
this.log.logNameColor = '\x1b[38;5;65m';
|
|
65
|
+
if (this.debug)
|
|
66
|
+
this.log.debug(`MatterNode ${this.pluginName ? 'for plugin ' + this.pluginName : 'bridge'} loading...`);
|
|
67
|
+
this.server = new BroadcastServer('matter', this.log);
|
|
68
|
+
this.server.on('broadcast_message', this.msgHandler.bind(this));
|
|
69
|
+
if (this.verbose)
|
|
70
|
+
this.log.debug(`BroadcastServer is ready`);
|
|
71
|
+
fs.mkdirSync(matterbridge.matterbridgeDirectory, { recursive: true });
|
|
72
|
+
this.pluginManager = new PluginManager(this.matterbridge);
|
|
73
|
+
this.pluginManager.logLevel = this.debug ? "debug" : "info";
|
|
74
|
+
this.pluginManager.server.close();
|
|
75
|
+
if (this.verbose)
|
|
76
|
+
this.log.debug(`PluginManager is ready`);
|
|
77
|
+
this.environment.vars.set('log.level', MatterLogLevel.DEBUG);
|
|
78
|
+
this.environment.vars.set('log.format', MatterLogFormat.ANSI);
|
|
79
|
+
this.environment.vars.set('path.root', path.join(matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
|
|
80
|
+
this.environment.vars.set('runtime.signals', false);
|
|
81
|
+
this.environment.vars.set('runtime.exitcode', false);
|
|
82
|
+
if (this.verbose)
|
|
83
|
+
this.log.debug(`Matter Environment is ready`);
|
|
84
|
+
if (this.matterbridge.fileLogger) {
|
|
85
|
+
AnsiLogger.setGlobalLogfile(path.join(this.matterbridge.matterbridgeDirectory, MATTERBRIDGE_LOGGER_FILE), this.matterbridge.logLevel);
|
|
86
|
+
}
|
|
87
|
+
Logger.destinations.default.write = this.createDestinationMatterLogger();
|
|
88
|
+
const levels = ['debug', 'info', 'notice', 'warn', 'error', 'fatal'];
|
|
89
|
+
if (this.verbose)
|
|
90
|
+
this.log.debug(`Matter logLevel: ${levels[Logger.level]} fileLogger: ${matterbridge.matterFileLogger}.`);
|
|
91
|
+
if (this.debug)
|
|
92
|
+
this.log.debug(`MatterNode ${this.pluginName ? 'for plugin ' + this.pluginName : 'bridge'} loaded`);
|
|
93
|
+
}
|
|
94
|
+
async msgHandler(msg) {
|
|
95
|
+
if (this.server.isWorkerRequest(msg, msg.type) && (msg.dst === 'all' || msg.dst === 'matter')) {
|
|
96
|
+
if (this.verbose)
|
|
97
|
+
this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
98
|
+
switch (msg.type) {
|
|
99
|
+
case 'get_log_level':
|
|
100
|
+
this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
|
|
101
|
+
break;
|
|
102
|
+
case 'set_log_level':
|
|
103
|
+
this.log.logLevel = msg.params.logLevel;
|
|
104
|
+
this.server.respond({ ...msg, response: { success: true, logLevel: this.log.logLevel } });
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
if (this.verbose)
|
|
108
|
+
this.log.debug(`Unknown broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (this.server.isWorkerResponse(msg, msg.type) && (msg.dst === 'all' || msg.dst === 'matter')) {
|
|
112
|
+
if (this.verbose)
|
|
113
|
+
this.log.debug(`Received broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
|
|
114
|
+
switch (msg.type) {
|
|
115
|
+
case 'get_log_level':
|
|
116
|
+
case 'set_log_level':
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
if (this.verbose)
|
|
120
|
+
this.log.debug(`Unknown broadcast response ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async destroy(closeMdns = true) {
|
|
125
|
+
if (this.verbose)
|
|
126
|
+
this.log.debug(`Destroying MatterNode instance for ${this.storeId}...`);
|
|
127
|
+
if (closeMdns) {
|
|
128
|
+
if (this.verbose)
|
|
129
|
+
this.log.debug(`Closing Matter MdnsService for ${this.storeId}...`);
|
|
130
|
+
this.matterMdnsService = this.environment.get(MdnsService);
|
|
131
|
+
if (typeof this.matterMdnsService[Symbol.asyncDispose] === 'function')
|
|
132
|
+
await this.matterMdnsService[Symbol.asyncDispose]();
|
|
133
|
+
else
|
|
134
|
+
await this.matterMdnsService.close();
|
|
135
|
+
if (this.verbose)
|
|
136
|
+
this.log.debug(`Closed Matter MdnsService for ${this.storeId}`);
|
|
137
|
+
}
|
|
138
|
+
this.pluginManager.destroy();
|
|
139
|
+
this.server.close();
|
|
140
|
+
await this.yieldToNode();
|
|
141
|
+
if (this.verbose)
|
|
142
|
+
this.log.debug(`Destroyed MatterNode instance for ${this.storeId}`);
|
|
143
|
+
}
|
|
144
|
+
async create() {
|
|
145
|
+
this.log.info('Creating Matter node...');
|
|
146
|
+
await this.startMatterStorage();
|
|
147
|
+
this.pluginManager.matterbridge.nodeStorage = new NodeStorageManager({ dir: path.join(this.matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR), writeQueue: false, expiredInterval: undefined, logging: false });
|
|
148
|
+
this.pluginManager.matterbridge.nodeContext = await this.pluginManager.matterbridge.nodeStorage.createStorage('matterbridge');
|
|
149
|
+
await this.pluginManager.loadFromStorage();
|
|
150
|
+
if (this.pluginName && this.device && this.device.deviceName) {
|
|
151
|
+
this.log.debug(`Creating MatterNode instance for server node device ${CYAN}${this.device.deviceName}${db}...`);
|
|
152
|
+
await this.createDeviceServerNode(this.pluginName, this.device);
|
|
153
|
+
this.log.debug(`Created MatterNode instance for server node device ${CYAN}${this.device.deviceName}${db}`);
|
|
154
|
+
this.emit('ready', this.device.deviceName.replace(/[ .]/g, ''));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!this.pluginName) {
|
|
158
|
+
this.log.debug('Creating MatterNode instance for all plugins...');
|
|
159
|
+
await this.createMatterbridgeServerNode();
|
|
160
|
+
this.log.debug('Loading all plugins...');
|
|
161
|
+
const loadPromises = [];
|
|
162
|
+
for (const plugin of this.pluginManager.array().filter((p) => p.enabled)) {
|
|
163
|
+
loadPromises.push(this.pluginManager.load(plugin));
|
|
164
|
+
}
|
|
165
|
+
await Promise.all(loadPromises);
|
|
166
|
+
this.log.debug('Loaded all plugins');
|
|
167
|
+
this.log.debug('Created MatterNode instance for all plugins');
|
|
168
|
+
this.emit('ready', 'Matterbridge');
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.log.debug(`Creating MatterNode instance for plugin ${CYAN}${this.pluginName}${db}...`);
|
|
172
|
+
this.log.debug(`Loading plugin ${CYAN}${this.pluginName}${db}...`);
|
|
173
|
+
await this.pluginManager.load(this.pluginName);
|
|
174
|
+
this.log.debug(`Loaded plugin ${CYAN}${this.pluginName}${db}`);
|
|
175
|
+
this.log.debug(`Created MatterNode instance for plugin ${CYAN}${this.pluginName}${db}`);
|
|
176
|
+
this.emit('ready', this.pluginName);
|
|
177
|
+
}
|
|
178
|
+
this.log.info('Created Matter node');
|
|
179
|
+
await this.yieldToNode();
|
|
180
|
+
}
|
|
181
|
+
async start() {
|
|
182
|
+
if (!this.serverNode && !this.pluginName)
|
|
183
|
+
throw new Error('Matter server node not created yet. Call create() first.');
|
|
184
|
+
this.log.info('Starting MatterNode...');
|
|
185
|
+
if (this.pluginName && this.device && this.device.deviceName) {
|
|
186
|
+
this.log.debug(`Starting MatterNode for server device ${this.pluginName}:${this.device.deviceName}...`);
|
|
187
|
+
await this.startServerNode();
|
|
188
|
+
this.log.debug(`Started MatterNode for server device ${this.pluginName}:${this.device.deviceName}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!this.pluginName) {
|
|
192
|
+
this.log.debug('Starting all plugins...');
|
|
193
|
+
const startPromises = [];
|
|
194
|
+
for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.loaded)) {
|
|
195
|
+
startPromises.push(this.pluginManager.start(plugin, 'Starting MatterNode'));
|
|
196
|
+
}
|
|
197
|
+
await Promise.all(startPromises);
|
|
198
|
+
this.log.debug('Started all plugins');
|
|
199
|
+
this.log.debug('Starting MatterNode for all plugins...');
|
|
200
|
+
await this.startServerNode();
|
|
201
|
+
this.log.debug('Started MatterNode for all plugins');
|
|
202
|
+
this.log.debug('Configuring all plugins...');
|
|
203
|
+
const configurePromises = [];
|
|
204
|
+
for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.started)) {
|
|
205
|
+
configurePromises.push(this.pluginManager.configure(plugin));
|
|
206
|
+
}
|
|
207
|
+
await Promise.all(configurePromises);
|
|
208
|
+
this.log.debug('Configured all plugins');
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
await this.pluginManager.start(this.pluginName, 'Starting MatterNode');
|
|
212
|
+
this.log.debug(`Starting MatterNode for plugin ${this.pluginName}...`);
|
|
213
|
+
await this.startServerNode();
|
|
214
|
+
this.log.debug(`Started MatterNode for plugin ${this.pluginName}`);
|
|
215
|
+
await this.pluginManager.configure(this.pluginName);
|
|
216
|
+
}
|
|
217
|
+
this.log.debug(`Starting dependant MatterNodes...`);
|
|
218
|
+
for (const dependantMatterNode of this.dependantMatterNodes.values()) {
|
|
219
|
+
await dependantMatterNode.start();
|
|
220
|
+
}
|
|
221
|
+
this.log.debug(`Started dependant MatterNodes`);
|
|
222
|
+
this.log.info('Started MatterNode');
|
|
223
|
+
await this.yieldToNode();
|
|
224
|
+
}
|
|
225
|
+
async stop() {
|
|
226
|
+
if (!this.serverNode)
|
|
227
|
+
throw new Error('Matter server node not created yet. Call create() first.');
|
|
228
|
+
this.log.info('Stopping MatterNode...');
|
|
229
|
+
if (this.pluginName && this.device && this.device.deviceName) {
|
|
230
|
+
this.log.debug(`Stopping MatterNode for server device ${this.pluginName}:${this.device.deviceName}...`);
|
|
231
|
+
await this.stopServerNode();
|
|
232
|
+
this.serverNode = undefined;
|
|
233
|
+
this.aggregatorNode = undefined;
|
|
234
|
+
await this.stopMatterStorage();
|
|
235
|
+
await this.destroy(false);
|
|
236
|
+
this.log.debug(`Stopped MatterNode for server device ${this.pluginName}:${this.device.deviceName}`);
|
|
237
|
+
this.log.info('Stopped MatterNode');
|
|
238
|
+
await this.yieldToNode();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!this.pluginName) {
|
|
242
|
+
this.log.debug('Stopping all plugins...');
|
|
243
|
+
const shutdownPromises = [];
|
|
244
|
+
for (const plugin of this.pluginManager.array().filter((p) => p.enabled && p.started)) {
|
|
245
|
+
shutdownPromises.push(this.pluginManager.shutdown(plugin, 'Stopping MatterNode'));
|
|
246
|
+
}
|
|
247
|
+
await Promise.all(shutdownPromises);
|
|
248
|
+
this.log.debug('Stopped all plugins');
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
this.log.debug(`Stopping plugin ${this.pluginName}...`);
|
|
252
|
+
await this.pluginManager.shutdown(this.pluginName, 'Stopping MatterNode');
|
|
253
|
+
this.log.debug(`Stopped plugin ${this.pluginName}`);
|
|
254
|
+
}
|
|
255
|
+
this.log.debug(`Stopping dependant MatterNodes...`);
|
|
256
|
+
for (const dependantMatterNode of this.dependantMatterNodes.values()) {
|
|
257
|
+
await dependantMatterNode.stop();
|
|
258
|
+
}
|
|
259
|
+
this.log.debug(`Stopped dependant MatterNodes`);
|
|
260
|
+
await this.stopServerNode();
|
|
261
|
+
this.serverNode = undefined;
|
|
262
|
+
this.aggregatorNode = undefined;
|
|
263
|
+
await this.stopMatterStorage();
|
|
264
|
+
this.log.info('Stopped MatterNode');
|
|
265
|
+
await this.yieldToNode();
|
|
266
|
+
}
|
|
267
|
+
createDestinationMatterLogger() {
|
|
268
|
+
this.matterLog.logNameColor = '\x1b[34m';
|
|
269
|
+
if (this.matterbridge.matterFileLogger) {
|
|
270
|
+
this.matterLog.logFilePath = path.join(this.matterbridge.matterbridgeDirectory, MATTER_LOGGER_FILE);
|
|
271
|
+
}
|
|
272
|
+
return (text, message) => {
|
|
273
|
+
const logger = text.slice(44, 44 + 20).trim();
|
|
274
|
+
const msg = text.slice(65);
|
|
275
|
+
this.matterLog.logName = logger;
|
|
276
|
+
this.matterLog.log(MatterLogLevel.names[message.level], msg);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async startMatterStorage() {
|
|
280
|
+
this.log.info(`Starting matter node storage...`);
|
|
281
|
+
this.matterStorageService = this.environment.get(StorageService);
|
|
282
|
+
this.log.info(`Started matter node storage in ${CYAN}${this.matterStorageService.location}${nf}`);
|
|
283
|
+
await this.backupMatterStorage(path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME), path.join(this.matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME + '.backup'));
|
|
284
|
+
}
|
|
285
|
+
async backupMatterStorage(storageName, backupName) {
|
|
286
|
+
this.log.info(`Creating matter node storage backup from ${CYAN}${storageName}${nf} to ${CYAN}${backupName}${nf}...`);
|
|
287
|
+
try {
|
|
288
|
+
await copyDirectory(storageName, backupName);
|
|
289
|
+
this.log.info('Created matter node storage backup');
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error instanceof Error && error?.code === 'ENOENT') {
|
|
293
|
+
this.log.info(`No matter node storage found to backup from ${CYAN}${storageName}${nf} to ${CYAN}${backupName}${nf}`);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.log.error(`Error creating matter node storage backup from ${storageName} to ${backupName}:`, error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async stopMatterStorage() {
|
|
301
|
+
this.log.info('Closing matter node storage...');
|
|
302
|
+
await this.matterStorageManager?.close();
|
|
303
|
+
this.matterStorageService = undefined;
|
|
304
|
+
this.matterStorageManager = undefined;
|
|
305
|
+
this.matterStorageContext = undefined;
|
|
306
|
+
this.log.info('Closed matter node storage');
|
|
307
|
+
this.emit('closed');
|
|
308
|
+
}
|
|
309
|
+
async createServerNodeContext(storeId, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
|
|
310
|
+
if (!this.matterStorageService) {
|
|
311
|
+
throw new Error('No storage service initialized');
|
|
312
|
+
}
|
|
313
|
+
const { randomBytes } = await import('node:crypto');
|
|
314
|
+
this.log.info(`Creating server node storage context "${storeId}.persist" for ${storeId}...`);
|
|
315
|
+
const storageManager = await this.matterStorageService.open(storeId);
|
|
316
|
+
const storageContext = storageManager.createContext('persist');
|
|
317
|
+
const random = randomBytes(8).toString('hex');
|
|
318
|
+
await storageContext.set('storeId', storeId);
|
|
319
|
+
await storageContext.set('deviceName', deviceName);
|
|
320
|
+
await storageContext.set('deviceType', deviceType);
|
|
321
|
+
await storageContext.set('vendorId', vendorId);
|
|
322
|
+
await storageContext.set('vendorName', vendorName.slice(0, 32));
|
|
323
|
+
await storageContext.set('productId', productId);
|
|
324
|
+
await storageContext.set('productName', productName.slice(0, 32));
|
|
325
|
+
await storageContext.set('nodeLabel', productName.slice(0, 32));
|
|
326
|
+
await storageContext.set('productLabel', productName.slice(0, 32));
|
|
327
|
+
await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
|
|
328
|
+
await storageContext.set('uniqueId', await storageContext.get('uniqueId', uniqueId ? uniqueId.slice(0, 32) : 'UI' + random));
|
|
329
|
+
await storageContext.set('softwareVersion', isValidNumber(parseVersionString(this.matterbridge.matterbridgeVersion), 0, UINT32_MAX) ? parseVersionString(this.matterbridge.matterbridgeVersion) : 1);
|
|
330
|
+
await storageContext.set('softwareVersionString', isValidString(this.matterbridge.matterbridgeVersion, 5, 64) ? this.matterbridge.matterbridgeVersion : '1.0.0');
|
|
331
|
+
await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.matterbridge.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.matterbridge.systemInformation.osRelease) : 1);
|
|
332
|
+
await storageContext.set('hardwareVersionString', isValidString(this.matterbridge.systemInformation.osRelease, 5, 64) ? this.matterbridge.systemInformation.osRelease : '1.0.0');
|
|
333
|
+
this.log.debug(`Created server node storage context "${storeId}.persist" for ${storeId}:`);
|
|
334
|
+
this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
|
|
335
|
+
this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
|
|
336
|
+
this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
|
|
337
|
+
this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
|
|
338
|
+
this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
|
|
339
|
+
this.log.debug(`- productId: ${await storageContext.get('productId')}`);
|
|
340
|
+
this.log.debug(`- productName: ${await storageContext.get('productName')}`);
|
|
341
|
+
this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
|
|
342
|
+
this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
|
|
343
|
+
this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
|
|
344
|
+
this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
|
|
345
|
+
this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')}`);
|
|
346
|
+
this.log.debug(`- softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
|
|
347
|
+
this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')}`);
|
|
348
|
+
this.log.debug(`- hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
|
|
349
|
+
return storageContext;
|
|
350
|
+
}
|
|
351
|
+
async createServerNode(port = 5540, passcode = 20252026, discriminator = 3850) {
|
|
352
|
+
if (!this.matterStorageContext) {
|
|
353
|
+
throw new Error('Matter server node context not created yet. Call createServerNodeContext() first.');
|
|
354
|
+
}
|
|
355
|
+
const storeId = await this.matterStorageContext.get('storeId');
|
|
356
|
+
this.log.notice(`Creating server node for ${storeId} on port ${port} with passcode ${passcode} and discriminator ${discriminator}...`);
|
|
357
|
+
const serverNode = await ServerNode.create({
|
|
358
|
+
id: storeId,
|
|
359
|
+
environment: this.environment,
|
|
360
|
+
network: {
|
|
361
|
+
listeningAddressIpv4: this.ipv4Address,
|
|
362
|
+
listeningAddressIpv6: this.ipv6Address,
|
|
363
|
+
port,
|
|
364
|
+
},
|
|
365
|
+
operationalCredentials: {
|
|
366
|
+
certification: this.certification,
|
|
367
|
+
},
|
|
368
|
+
commissioning: {
|
|
369
|
+
passcode,
|
|
370
|
+
discriminator,
|
|
371
|
+
},
|
|
372
|
+
productDescription: {
|
|
373
|
+
name: await this.matterStorageContext.get('deviceName'),
|
|
374
|
+
deviceType: DeviceTypeId(await this.matterStorageContext.get('deviceType')),
|
|
375
|
+
vendorId: VendorId(await this.matterStorageContext.get('vendorId')),
|
|
376
|
+
productId: await this.matterStorageContext.get('productId'),
|
|
377
|
+
},
|
|
378
|
+
basicInformation: {
|
|
379
|
+
vendorId: VendorId(await this.matterStorageContext.get('vendorId')),
|
|
380
|
+
vendorName: await this.matterStorageContext.get('vendorName'),
|
|
381
|
+
productId: await this.matterStorageContext.get('productId'),
|
|
382
|
+
productName: await this.matterStorageContext.get('productName'),
|
|
383
|
+
productLabel: await this.matterStorageContext.get('productLabel'),
|
|
384
|
+
nodeLabel: await this.matterStorageContext.get('nodeLabel'),
|
|
385
|
+
serialNumber: await this.matterStorageContext.get('serialNumber'),
|
|
386
|
+
uniqueId: await this.matterStorageContext.get('uniqueId'),
|
|
387
|
+
softwareVersion: await this.matterStorageContext.get('softwareVersion'),
|
|
388
|
+
softwareVersionString: await this.matterStorageContext.get('softwareVersionString'),
|
|
389
|
+
hardwareVersion: await this.matterStorageContext.get('hardwareVersion'),
|
|
390
|
+
hardwareVersionString: await this.matterStorageContext.get('hardwareVersionString'),
|
|
391
|
+
reachable: true,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
serverNode.lifecycle.commissioned.on(() => {
|
|
395
|
+
this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`);
|
|
396
|
+
this.advertisingNodes.delete(storeId);
|
|
397
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
398
|
+
});
|
|
399
|
+
serverNode.lifecycle.decommissioned.on(() => {
|
|
400
|
+
this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`);
|
|
401
|
+
this.advertisingNodes.delete(storeId);
|
|
402
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
403
|
+
this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
|
|
404
|
+
});
|
|
405
|
+
serverNode.lifecycle.online.on(async () => {
|
|
406
|
+
this.log.notice(`Server node for ${storeId} is online`);
|
|
407
|
+
if (!serverNode.lifecycle.isCommissioned) {
|
|
408
|
+
this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
|
|
409
|
+
this.advertisingNodes.set(storeId, Date.now());
|
|
410
|
+
const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
|
|
411
|
+
this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
|
|
412
|
+
this.log.notice(`Manual pairing code: ${manualPairingCode}`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
|
|
416
|
+
this.advertisingNodes.delete(storeId);
|
|
417
|
+
}
|
|
418
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
419
|
+
this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is online`, timeout: 5, severity: 'success' } });
|
|
420
|
+
this.emit('online', storeId);
|
|
421
|
+
});
|
|
422
|
+
serverNode.lifecycle.offline.on(() => {
|
|
423
|
+
this.log.notice(`Server node for ${storeId} is offline`);
|
|
424
|
+
this.advertisingNodes.delete(storeId);
|
|
425
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
426
|
+
this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
|
|
427
|
+
this.emit('offline', storeId);
|
|
428
|
+
});
|
|
429
|
+
serverNode.events.commissioning.fabricsChanged.on((fabricIndex, fabricAction) => {
|
|
430
|
+
let action = '';
|
|
431
|
+
switch (fabricAction) {
|
|
432
|
+
case FabricAction.Added:
|
|
433
|
+
this.advertisingNodes.delete(storeId);
|
|
434
|
+
action = 'added';
|
|
435
|
+
break;
|
|
436
|
+
case FabricAction.Removed:
|
|
437
|
+
action = 'removed';
|
|
438
|
+
break;
|
|
439
|
+
case FabricAction.Updated:
|
|
440
|
+
action = 'updated';
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
|
|
444
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
445
|
+
});
|
|
446
|
+
serverNode.events.sessions.opened.on((session) => {
|
|
447
|
+
this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
|
|
448
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
449
|
+
});
|
|
450
|
+
serverNode.events.sessions.closed.on((session) => {
|
|
451
|
+
this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
452
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
453
|
+
});
|
|
454
|
+
serverNode.events.sessions.subscriptionsChanged.on((session) => {
|
|
455
|
+
this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
|
|
456
|
+
this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
|
|
457
|
+
});
|
|
458
|
+
this.storeId = storeId;
|
|
459
|
+
this.log.info(`Created server node for ${this.storeId}`);
|
|
460
|
+
return serverNode;
|
|
461
|
+
}
|
|
462
|
+
getServerNodeData(serverNode) {
|
|
463
|
+
const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
|
|
464
|
+
return {
|
|
465
|
+
id: serverNode.id,
|
|
466
|
+
online: serverNode.lifecycle.isOnline,
|
|
467
|
+
commissioned: serverNode.state.commissioning.commissioned,
|
|
468
|
+
advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
|
|
469
|
+
advertiseTime,
|
|
470
|
+
windowStatus: serverNode.state.administratorCommissioning.windowStatus,
|
|
471
|
+
qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
|
|
472
|
+
manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
|
|
473
|
+
fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
|
|
474
|
+
sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
|
|
475
|
+
serialNumber: serverNode.state.basicInformation.serialNumber,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async startServerNode(timeout = 30000) {
|
|
479
|
+
if (!this.serverNode) {
|
|
480
|
+
throw new Error('Matter server node not created yet. Call create() first.');
|
|
481
|
+
}
|
|
482
|
+
this.log.notice(`Starting ${this.serverNode.id} server node...`);
|
|
483
|
+
try {
|
|
484
|
+
await withTimeout(this.serverNode.start(), timeout);
|
|
485
|
+
this.log.notice(`Started ${this.serverNode.id} server node`);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
this.log.error(`Failed to start ${this.serverNode.id} server node: ${error instanceof Error ? error.message : error}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async stopServerNode(timeout = 30000) {
|
|
492
|
+
if (!this.serverNode) {
|
|
493
|
+
throw new Error('Matter server node not created yet. Call create() first.');
|
|
494
|
+
}
|
|
495
|
+
this.log.notice(`Closing ${this.serverNode.id} server node`);
|
|
496
|
+
try {
|
|
497
|
+
await withTimeout(this.serverNode.close(), timeout);
|
|
498
|
+
this.log.info(`Closed ${this.serverNode.id} server node`);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
this.log.error(`Failed to close ${this.serverNode.id} server node: ${error instanceof Error ? error.message : error}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async createAggregatorNode() {
|
|
505
|
+
if (!this.matterStorageContext) {
|
|
506
|
+
throw new Error('Matter server node context not created yet. Call createServerNodeContext() first.');
|
|
507
|
+
}
|
|
508
|
+
this.log.notice(`Creating ${await this.matterStorageContext.get('storeId')} aggregator...`);
|
|
509
|
+
const aggregatorNode = new Endpoint(AggregatorEndpoint, { id: `${await this.matterStorageContext.get('storeId')}` });
|
|
510
|
+
this.log.info(`Created ${await this.matterStorageContext.get('storeId')} aggregator`);
|
|
511
|
+
return aggregatorNode;
|
|
512
|
+
}
|
|
513
|
+
async createMatterbridgeServerNode() {
|
|
514
|
+
this.log.debug(`Creating ${plg}Matterbridge${db} server node...`);
|
|
515
|
+
this.matterStorageContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, this.aggregatorSerialNumber, this.aggregatorUniqueId);
|
|
516
|
+
this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
517
|
+
this.aggregatorNode = await this.createAggregatorNode();
|
|
518
|
+
this.log.debug(`Adding ${plg}Matterbridge${db} aggregator node...`);
|
|
519
|
+
await this.serverNode.add(this.aggregatorNode);
|
|
520
|
+
this.log.debug(`Added ${plg}Matterbridge${db} aggregator node`);
|
|
521
|
+
await this.serverNode.construction.ready;
|
|
522
|
+
await this.aggregatorNode.construction.ready;
|
|
523
|
+
this.log.debug(`Created ${plg}Matterbridge${db} server node`);
|
|
524
|
+
return this.serverNode;
|
|
525
|
+
}
|
|
526
|
+
async createAccessoryPlugin(plugin, device) {
|
|
527
|
+
if (typeof plugin === 'string') {
|
|
528
|
+
const _plugin = this.pluginManager.get(plugin);
|
|
529
|
+
if (!_plugin)
|
|
530
|
+
throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
|
|
531
|
+
plugin = _plugin;
|
|
532
|
+
}
|
|
533
|
+
if (!plugin.locked && device.deviceType && device.deviceName && device.vendorId && device.vendorName && device.productId && device.productName) {
|
|
534
|
+
plugin.locked = true;
|
|
535
|
+
this.log.debug(`Creating accessory plugin ${plg}${plugin.name}${db} server node...`);
|
|
536
|
+
this.matterStorageContext = await this.createServerNodeContext(plugin.name, device.deviceName, DeviceTypeId(device.deviceType), VendorId(device.vendorId), device.vendorName, device.productId, device.productName);
|
|
537
|
+
this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
538
|
+
this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node...`);
|
|
539
|
+
await this.serverNode.add(device);
|
|
540
|
+
this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to ${plg}${plugin.name}${db} server node`);
|
|
541
|
+
await this.serverNode.construction.ready;
|
|
542
|
+
await device.construction.ready;
|
|
543
|
+
this.log.debug(`Created accessory plugin ${plg}${plugin.name}${db} server node`);
|
|
544
|
+
}
|
|
545
|
+
return this.serverNode;
|
|
546
|
+
}
|
|
547
|
+
async createDynamicPlugin(plugin) {
|
|
548
|
+
if (typeof plugin === 'string') {
|
|
549
|
+
const _plugin = this.pluginManager.get(plugin);
|
|
550
|
+
if (!_plugin)
|
|
551
|
+
throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
|
|
552
|
+
plugin = _plugin;
|
|
553
|
+
}
|
|
554
|
+
if (!plugin.locked) {
|
|
555
|
+
plugin.locked = true;
|
|
556
|
+
this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
|
|
557
|
+
this.matterStorageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
|
|
558
|
+
this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
559
|
+
this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
|
|
560
|
+
this.aggregatorNode = await this.createAggregatorNode();
|
|
561
|
+
this.log.debug(`Adding dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
|
|
562
|
+
await this.serverNode.add(this.aggregatorNode);
|
|
563
|
+
this.log.debug(`Added dynamic plugin ${plg}${plugin.name}${db} aggregator node`);
|
|
564
|
+
await this.serverNode.construction.ready;
|
|
565
|
+
await this.aggregatorNode.construction.ready;
|
|
566
|
+
this.log.debug(`Created dynamic plugin ${plg}${plugin.name}${db} server node`);
|
|
567
|
+
}
|
|
568
|
+
return this.serverNode;
|
|
569
|
+
}
|
|
570
|
+
async createDeviceServerNode(plugin, device) {
|
|
571
|
+
if (typeof plugin === 'string') {
|
|
572
|
+
const _plugin = this.pluginManager.get(plugin);
|
|
573
|
+
if (!_plugin)
|
|
574
|
+
throw new Error(`Plugin ${BLUE}${this.pluginName}${er} not found`);
|
|
575
|
+
plugin = _plugin;
|
|
576
|
+
}
|
|
577
|
+
if (device.mode === 'server' && device.deviceType && device.deviceName && device.vendorId && device.vendorName && device.productId && device.productName) {
|
|
578
|
+
this.log.debug(`Creating device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node...`);
|
|
579
|
+
this.matterStorageContext = await this.createServerNodeContext(device.deviceName.replace(/[ .]/g, ''), device.deviceName, DeviceTypeId(device.deviceType), VendorId(device.vendorId), device.vendorName, device.productId, device.productName);
|
|
580
|
+
this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
|
|
581
|
+
this.log.debug(`Adding ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node...`);
|
|
582
|
+
await this.serverNode.add(device);
|
|
583
|
+
this.log.debug(`Added ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} to server node`);
|
|
584
|
+
await this.serverNode.construction.ready;
|
|
585
|
+
await device.construction.ready;
|
|
586
|
+
this.log.debug(`Created device ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db} server node`);
|
|
587
|
+
}
|
|
588
|
+
return this.serverNode;
|
|
589
|
+
}
|
|
590
|
+
async addBridgedEndpoint(pluginName, device) {
|
|
591
|
+
const plugin = this.pluginManager.get(pluginName);
|
|
592
|
+
if (!plugin)
|
|
593
|
+
throw new Error(`Error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): plugin not found`);
|
|
594
|
+
if (device.mode === 'server') {
|
|
595
|
+
try {
|
|
596
|
+
this.log.debug(`Creating MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
597
|
+
const matterNode = new MatterNode(this.matterbridge, pluginName, device);
|
|
598
|
+
this.dependantMatterNodes.set(device.id, matterNode);
|
|
599
|
+
await matterNode.create();
|
|
600
|
+
this.log.debug(`Created MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})`);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
inspectError(this.log, `Error creating MatterNode for device ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else if (this.matterbridge.bridgeMode === 'bridge') {
|
|
608
|
+
if (device.mode === 'matter') {
|
|
609
|
+
this.log.debug(`Adding matter endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
610
|
+
if (!this.serverNode)
|
|
611
|
+
throw new Error(`Server node not found for matter endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`);
|
|
612
|
+
try {
|
|
613
|
+
await this.serverNode.add(device);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
inspectError(this.log, `Matter error adding matter endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
622
|
+
if (!this.aggregatorNode)
|
|
623
|
+
throw new Error(`Aggregator node not found for endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`);
|
|
624
|
+
try {
|
|
625
|
+
await this.aggregatorNode.add(device);
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
inspectError(this.log, `Matter error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
634
|
+
if (plugin.type === 'AccessoryPlatform') {
|
|
635
|
+
try {
|
|
636
|
+
this.log.debug(`Adding accessory endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
637
|
+
if (this.serverNode) {
|
|
638
|
+
await this.serverNode.add(device);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
await this.createAccessoryPlugin(plugin, device);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (error) {
|
|
645
|
+
inspectError(this.log, `Matter error adding accessory endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (plugin.type === 'DynamicPlatform') {
|
|
650
|
+
try {
|
|
651
|
+
this.log.debug(`Adding bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
652
|
+
if (!this.serverNode) {
|
|
653
|
+
await this.createDynamicPlugin(plugin);
|
|
654
|
+
}
|
|
655
|
+
if (device.mode === 'matter')
|
|
656
|
+
await this.serverNode?.add(device);
|
|
657
|
+
else
|
|
658
|
+
await this.aggregatorNode?.add(device);
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
inspectError(this.log, `Matter error adding bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er})`, error);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (plugin.registeredDevices !== undefined)
|
|
667
|
+
plugin.registeredDevices++;
|
|
668
|
+
await this.server.fetch({ type: 'devices_set', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
|
|
669
|
+
await device.construction.ready;
|
|
670
|
+
await this.subscribeAttributeChanged(plugin, device);
|
|
671
|
+
this.log.info(`Added endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${device.name}${nf})`);
|
|
672
|
+
return device;
|
|
673
|
+
}
|
|
674
|
+
async removeBridgedEndpoint(pluginName, device) {
|
|
675
|
+
this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
|
|
676
|
+
const plugin = this.pluginManager.get(pluginName);
|
|
677
|
+
if (!plugin)
|
|
678
|
+
throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): plugin not found`);
|
|
679
|
+
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
680
|
+
if (!this.aggregatorNode)
|
|
681
|
+
throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): aggregator node not found`);
|
|
682
|
+
await device.delete();
|
|
683
|
+
}
|
|
684
|
+
else if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
685
|
+
if (plugin.type === 'AccessoryPlatform') {
|
|
686
|
+
if (!this.serverNode)
|
|
687
|
+
throw new Error(`Error removing endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): server node not found`);
|
|
688
|
+
await device.delete();
|
|
689
|
+
}
|
|
690
|
+
else if (plugin.type === 'DynamicPlatform') {
|
|
691
|
+
if (!this.aggregatorNode)
|
|
692
|
+
throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): aggregator node not found`);
|
|
693
|
+
await device.delete();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
this.log.info(`Removed bridged endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${device.name}${nf})`);
|
|
697
|
+
if (plugin.registeredDevices !== undefined)
|
|
698
|
+
plugin.registeredDevices--;
|
|
699
|
+
await this.server.fetch({ type: 'devices_remove', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
|
|
700
|
+
return device;
|
|
701
|
+
}
|
|
702
|
+
async removeAllBridgedEndpoints(pluginName, delay = 0) {
|
|
703
|
+
const plugin = this.pluginManager.get(pluginName);
|
|
704
|
+
if (!plugin)
|
|
705
|
+
throw new Error(`Error removing all bridged endpoints for plugin ${plg}${pluginName}${er}: plugin not found`);
|
|
706
|
+
this.log.debug(`Removing all #${plugin.registeredDevices} bridged endpoints for plugin ${plg}${pluginName}${db}${delay > 0 ? ` with delay ${delay} ms` : ''}...`);
|
|
707
|
+
const devices = (await this.server.fetch({ type: 'devices_basearray', src: this.server.name, dst: 'devices', params: { pluginName } })).response.devices;
|
|
708
|
+
for (const device of devices) {
|
|
709
|
+
const endpoint = (this.aggregatorNode?.parts.get(device.id || '') || this.serverNode?.parts.get(device.id || ''));
|
|
710
|
+
if (!endpoint)
|
|
711
|
+
throw new Error(`Endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} id ${device.id} not found removing all endpoints`);
|
|
712
|
+
this.log.debug(`Removing bridged endpoint ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${endpoint?.name}${db})...`);
|
|
713
|
+
await endpoint.delete();
|
|
714
|
+
this.log.info(`Removed bridged endpoint #${plugin.registeredDevices} ${plg}${pluginName}${nf}:${dev}${device.deviceName}${nf} (${zb}${endpoint?.name}${nf})`);
|
|
715
|
+
if (plugin.registeredDevices !== undefined)
|
|
716
|
+
plugin.registeredDevices--;
|
|
717
|
+
await this.server.fetch({ type: 'devices_remove', src: this.server.name, dst: 'devices', params: { device: toBaseDevice(device) } });
|
|
718
|
+
if (delay > 0)
|
|
719
|
+
await wait(delay);
|
|
720
|
+
}
|
|
721
|
+
if (delay > 0)
|
|
722
|
+
await wait(Number(process.env['MATTERBRIDGE_REMOVE_ALL_ENDPOINT_TIMEOUT_MS']) || 2000);
|
|
723
|
+
}
|
|
724
|
+
async addVirtualEndpoint(pluginName, name, type, callback) {
|
|
725
|
+
this.log.debug(`Creating virtual device ${plg}${pluginName}${db}:${dev}${name}${db}...`);
|
|
726
|
+
const plugin = this.pluginManager.get(pluginName);
|
|
727
|
+
if (!plugin) {
|
|
728
|
+
this.log.error(`Error adding virtual endpoint ${plg}${pluginName}${er}:${dev}${name}${er}: plugin not found`);
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type !== 'DynamicPlatform') {
|
|
732
|
+
this.log.error(`Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform`);
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
if (!this.aggregatorNode) {
|
|
736
|
+
this.log.error(`Aggregator node not found for plugin ${plg}${plugin.name}${er} adding virtual endpoint ${dev}${name}${er}`);
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
if (this.aggregatorNode.parts.has(name.replaceAll(' ', '') + ':' + type)) {
|
|
740
|
+
this.log.error(`Virtual device ${plg}${pluginName}${er}:${dev}${name}${er} already registered. Please use a different name.`);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
await addVirtualDevice(this.aggregatorNode, name.slice(0, 32), type, callback);
|
|
744
|
+
this.log.debug(`Created virtual device ${plg}${pluginName}${db}:${dev}${name}${db}`);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
async subscribeAttributeChanged(plugin, device) {
|
|
748
|
+
if (!plugin || !device || !device.plugin || !device.serialNumber || !device.uniqueId || !device.maybeNumber)
|
|
749
|
+
return;
|
|
750
|
+
this.log.debug(`Subscribing attributes for endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} (${zb}${device.name}${db})`);
|
|
751
|
+
if (this.matterbridge.bridgeMode === 'childbridge' && plugin.type === 'AccessoryPlatform' && this.serverNode) {
|
|
752
|
+
this.serverNode.eventsOf(BasicInformationServer).reachable$Changed?.on((reachable) => {
|
|
753
|
+
this.log.debug(`Accessory endpoint ${plg}${plugin.name}${nf}:${dev}${device.deviceName}${nf}:${or}${device.id}${nf}:${or}${device.number}${nf} is ${reachable ? 'reachable' : 'unreachable'}`);
|
|
754
|
+
this.server.request({
|
|
755
|
+
type: 'frontend_attributechanged',
|
|
756
|
+
src: 'matter',
|
|
757
|
+
dst: 'frontend',
|
|
758
|
+
params: { plugin: device.plugin, serialNumber: device.serialNumber, uniqueId: device.uniqueId, number: device.number, id: device.id, cluster: 'BasicInformation', attribute: 'reachable', value: reachable },
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const subscriptions = [
|
|
763
|
+
{ cluster: 'BridgedDeviceBasicInformation', attribute: 'reachable' },
|
|
764
|
+
{ cluster: 'OnOff', attribute: 'onOff' },
|
|
765
|
+
{ cluster: 'LevelControl', attribute: 'currentLevel' },
|
|
766
|
+
{ cluster: 'ColorControl', attribute: 'colorMode' },
|
|
767
|
+
{ cluster: 'ColorControl', attribute: 'currentHue' },
|
|
768
|
+
{ cluster: 'ColorControl', attribute: 'currentSaturation' },
|
|
769
|
+
{ cluster: 'ColorControl', attribute: 'currentX' },
|
|
770
|
+
{ cluster: 'ColorControl', attribute: 'currentY' },
|
|
771
|
+
{ cluster: 'ColorControl', attribute: 'colorTemperatureMireds' },
|
|
772
|
+
{ cluster: 'Thermostat', attribute: 'localTemperature' },
|
|
773
|
+
{ cluster: 'Thermostat', attribute: 'occupiedCoolingSetpoint' },
|
|
774
|
+
{ cluster: 'Thermostat', attribute: 'occupiedHeatingSetpoint' },
|
|
775
|
+
{ cluster: 'Thermostat', attribute: 'systemMode' },
|
|
776
|
+
{ cluster: 'WindowCovering', attribute: 'operationalStatus' },
|
|
777
|
+
{ cluster: 'WindowCovering', attribute: 'currentPositionLiftPercent100ths' },
|
|
778
|
+
{ cluster: 'DoorLock', attribute: 'lockState' },
|
|
779
|
+
{ cluster: 'PumpConfigurationAndControl', attribute: 'pumpStatus' },
|
|
780
|
+
{ cluster: 'FanControl', attribute: 'fanMode' },
|
|
781
|
+
{ cluster: 'FanControl', attribute: 'fanModeSequence' },
|
|
782
|
+
{ cluster: 'FanControl', attribute: 'percentSetting' },
|
|
783
|
+
{ cluster: 'ModeSelect', attribute: 'currentMode' },
|
|
784
|
+
{ cluster: 'RvcRunMode', attribute: 'currentMode' },
|
|
785
|
+
{ cluster: 'RvcCleanMode', attribute: 'currentMode' },
|
|
786
|
+
{ cluster: 'RvcOperationalState', attribute: 'operationalState' },
|
|
787
|
+
{ cluster: 'RvcOperationalState', attribute: 'operationalError' },
|
|
788
|
+
{ cluster: 'ServiceArea', attribute: 'currentArea' },
|
|
789
|
+
{ cluster: 'AirQuality', attribute: 'airQuality' },
|
|
790
|
+
{ cluster: 'TotalVolatileOrganicCompoundsConcentrationMeasurement', attribute: 'measuredValue' },
|
|
791
|
+
{ cluster: 'BooleanState', attribute: 'stateValue' },
|
|
792
|
+
{ cluster: 'OccupancySensing', attribute: 'occupancy' },
|
|
793
|
+
{ cluster: 'IlluminanceMeasurement', attribute: 'measuredValue' },
|
|
794
|
+
{ cluster: 'TemperatureMeasurement', attribute: 'measuredValue' },
|
|
795
|
+
{ cluster: 'RelativeHumidityMeasurement', attribute: 'measuredValue' },
|
|
796
|
+
{ cluster: 'PressureMeasurement', attribute: 'measuredValue' },
|
|
797
|
+
{ cluster: 'FlowMeasurement', attribute: 'measuredValue' },
|
|
798
|
+
{ cluster: 'SmokeCoAlarm', attribute: 'smokeState' },
|
|
799
|
+
{ cluster: 'SmokeCoAlarm', attribute: 'coState' },
|
|
800
|
+
];
|
|
801
|
+
for (const sub of subscriptions) {
|
|
802
|
+
if (device.hasAttributeServer(sub.cluster, sub.attribute)) {
|
|
803
|
+
this.log.debug(`Subscribing to endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
|
|
804
|
+
await device.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
|
|
805
|
+
this.log.debug(`Bridged endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${device.id}${db}:${or}${device.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${value}${db}`);
|
|
806
|
+
this.server.request({
|
|
807
|
+
type: 'frontend_attributechanged',
|
|
808
|
+
src: 'matter',
|
|
809
|
+
dst: 'frontend',
|
|
810
|
+
params: { plugin: device.plugin, serialNumber: device.serialNumber, uniqueId: device.uniqueId, number: device.number, id: device.id, cluster: sub.cluster, attribute: sub.attribute, value: value },
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
for (const child of device.getChildEndpoints()) {
|
|
815
|
+
if (child.hasAttributeServer(sub.cluster, sub.attribute)) {
|
|
816
|
+
this.log.debug(`Subscribing to child endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changes...`);
|
|
817
|
+
await child.subscribeAttribute(sub.cluster, sub.attribute, (value) => {
|
|
818
|
+
this.log.debug(`Bridged child endpoint ${plg}${plugin.name}${db}:${dev}${device.deviceName}${db}:${or}${child.id}${db}:${or}${child.number}${db} attribute ${dev}${sub.cluster}${db}.${dev}${sub.attribute}${db} changed to ${CYAN}${value}${db}`);
|
|
819
|
+
this.server.request({
|
|
820
|
+
type: 'frontend_attributechanged',
|
|
821
|
+
src: 'matter',
|
|
822
|
+
dst: 'frontend',
|
|
823
|
+
params: { plugin: device.plugin, serialNumber: device.serialNumber, uniqueId: device.uniqueId, number: child.number, id: child.id, cluster: sub.cluster, attribute: sub.attribute, value: value },
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
sanitizeFabricInformations(fabricInfo) {
|
|
831
|
+
return fabricInfo.map((info) => {
|
|
832
|
+
return {
|
|
833
|
+
fabricIndex: info.fabricIndex,
|
|
834
|
+
fabricId: info.fabricId.toString(),
|
|
835
|
+
nodeId: info.nodeId.toString(),
|
|
836
|
+
rootNodeId: info.rootNodeId.toString(),
|
|
837
|
+
rootVendorId: info.rootVendorId,
|
|
838
|
+
rootVendorName: this.getVendorIdName(info.rootVendorId),
|
|
839
|
+
label: info.label,
|
|
840
|
+
};
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
sanitizeSessionInformation(sessions) {
|
|
844
|
+
return sessions
|
|
845
|
+
.filter((session) => session.isPeerActive)
|
|
846
|
+
.map((session) => {
|
|
847
|
+
return {
|
|
848
|
+
name: session.name,
|
|
849
|
+
nodeId: session.nodeId.toString(),
|
|
850
|
+
peerNodeId: session.peerNodeId.toString(),
|
|
851
|
+
fabric: session.fabric
|
|
852
|
+
? {
|
|
853
|
+
fabricIndex: session.fabric.fabricIndex,
|
|
854
|
+
fabricId: session.fabric.fabricId.toString(),
|
|
855
|
+
nodeId: session.fabric.nodeId.toString(),
|
|
856
|
+
rootNodeId: session.fabric.rootNodeId.toString(),
|
|
857
|
+
rootVendorId: session.fabric.rootVendorId,
|
|
858
|
+
rootVendorName: this.getVendorIdName(session.fabric.rootVendorId),
|
|
859
|
+
label: session.fabric.label,
|
|
860
|
+
}
|
|
861
|
+
: undefined,
|
|
862
|
+
isPeerActive: session.isPeerActive,
|
|
863
|
+
lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
|
|
864
|
+
lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
|
|
865
|
+
numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
|
|
866
|
+
};
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
async setServerReachability(reachable) {
|
|
870
|
+
await this.serverNode?.setStateOf(BasicInformationServer, { reachable });
|
|
871
|
+
this.serverNode?.act((agent) => this.serverNode?.eventsOf(BasicInformationServer).reachableChanged?.emit({ reachableNewValue: reachable }, agent.context));
|
|
872
|
+
}
|
|
873
|
+
async setAggregatorReachability(aggregatorNode, reachable) {
|
|
874
|
+
for (const child of aggregatorNode.parts) {
|
|
875
|
+
this.log.debug(`Setting reachability of ${child?.deviceName} to ${reachable}`);
|
|
876
|
+
await child.setStateOf(BridgedDeviceBasicInformationServer, { reachable });
|
|
877
|
+
child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
getVendorIdName = (vendorId) => {
|
|
881
|
+
if (!vendorId)
|
|
882
|
+
return '';
|
|
883
|
+
let vendorName = '(Unknown vendorId)';
|
|
884
|
+
switch (vendorId) {
|
|
885
|
+
case 4937:
|
|
886
|
+
vendorName = '(AppleHome)';
|
|
887
|
+
break;
|
|
888
|
+
case 4996:
|
|
889
|
+
vendorName = '(AppleKeyChain)';
|
|
890
|
+
break;
|
|
891
|
+
case 4362:
|
|
892
|
+
vendorName = '(SmartThings)';
|
|
893
|
+
break;
|
|
894
|
+
case 4939:
|
|
895
|
+
vendorName = '(HomeAssistant)';
|
|
896
|
+
break;
|
|
897
|
+
case 24582:
|
|
898
|
+
vendorName = '(GoogleHome)';
|
|
899
|
+
break;
|
|
900
|
+
case 4631:
|
|
901
|
+
vendorName = '(Alexa)';
|
|
902
|
+
break;
|
|
903
|
+
case 4701:
|
|
904
|
+
vendorName = '(Tuya)';
|
|
905
|
+
break;
|
|
906
|
+
case 4718:
|
|
907
|
+
vendorName = '(Xiaomi)';
|
|
908
|
+
break;
|
|
909
|
+
case 4742:
|
|
910
|
+
vendorName = '(eWeLink)';
|
|
911
|
+
break;
|
|
912
|
+
case 5264:
|
|
913
|
+
vendorName = '(Shelly)';
|
|
914
|
+
break;
|
|
915
|
+
case 0x1488:
|
|
916
|
+
vendorName = '(ShortcutLabsFlic)';
|
|
917
|
+
break;
|
|
918
|
+
case 65521:
|
|
919
|
+
vendorName = '(MatterTest)';
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
return vendorName;
|
|
923
|
+
};
|
|
924
|
+
async yieldToNode(timeout = 100) {
|
|
925
|
+
await Promise.resolve();
|
|
926
|
+
await new Promise((resolve) => {
|
|
927
|
+
setImmediate(resolve);
|
|
928
|
+
});
|
|
929
|
+
await new Promise((resolve) => {
|
|
930
|
+
setTimeout(resolve, Math.min(timeout, 10));
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
package/dist/utils/tracker.js
CHANGED
|
@@ -40,7 +40,7 @@ export class Tracker extends EventEmitter {
|
|
|
40
40
|
this.name = name;
|
|
41
41
|
this.debug = debug;
|
|
42
42
|
this.verbose = verbose;
|
|
43
|
-
if (process.argv.includes('--debug') || process.argv.includes('-debug')) {
|
|
43
|
+
if (process.argv.includes('--debug') || process.argv.includes('-debug') || process.argv.includes('--verbose') || process.argv.includes('-verbose')) {
|
|
44
44
|
this.debug = true;
|
|
45
45
|
}
|
|
46
46
|
if (process.argv.includes('--verbose') || process.argv.includes('-verbose')) {
|
package/marked.ps1
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-DEV.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-DEV.html -Encoding utf8
|
|
2
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-DOCKER.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-DOCKER.html -Encoding utf8
|
|
3
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-MACOS-PLIST.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-MACOS-PLIST.html -Encoding utf8
|
|
4
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-NGINX.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-NGINX.html -Encoding utf8
|
|
5
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-PODMAN.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-PODMAN.html -Encoding utf8
|
|
6
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-SERVICE-LOCAL.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-SERVICE-LOCAL.html -Encoding utf8
|
|
7
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-SERVICE-OPT.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-SERVICE-OPT.html -Encoding utf8
|
|
8
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README-SERVICE.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README-SERVICE.html -Encoding utf8
|
|
9
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\README.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\README.html -Encoding utf8
|
|
10
|
+
(Get-Content .\docs\markedHeader.html) + (marked .\CHANGELOG.md) + (Get-Content .\docs\markedFooter.html) | Out-File .\docs\CHANGELOG.html -Encoding utf8
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge",
|
|
3
|
-
"version": "3.4.0-dev-
|
|
3
|
+
"version": "3.4.0-dev-20251126-5087664",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "matterbridge",
|
|
9
|
-
"version": "3.4.0-dev-
|
|
9
|
+
"version": "3.4.0-dev-20251126-5087664",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@matter/main": "0.15.6",
|
package/package.json
CHANGED