matterbridge 3.4.0-dev-20251121-a354bec → 3.4.0-dev-20251125-6f11a4f
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/dist/broadcastServer.js +7 -3
- package/dist/jestutils/jestHelpers.js +40 -1
- package/dist/matterNode.js +933 -0
- package/dist/matterbridgePlatform.js +6 -17
- package/dist/pluginManager.js +36 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/fetch-chip.mjs +14 -14
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) {
|
|
@@ -88,6 +88,10 @@ export class BroadcastServer extends EventEmitter {
|
|
|
88
88
|
this.log.debug(`Fetch response: ${debugStringify(msg)}`);
|
|
89
89
|
resolve(msg);
|
|
90
90
|
}
|
|
91
|
+
else if (this.isWorkerResponse(msg, message.type) && msg.id !== message.id) {
|
|
92
|
+
if (this.verbose)
|
|
93
|
+
this.log.debug(`Fetch received unrelated response: ${debugStringify(msg)}`);
|
|
94
|
+
}
|
|
91
95
|
};
|
|
92
96
|
this.on('broadcast_message', responseHandler);
|
|
93
97
|
this.request(message);
|
|
@@ -184,7 +184,7 @@ export async function createMatterbridgeEnvironment(name) {
|
|
|
184
184
|
matterbridge = await Matterbridge.loadInstance(false);
|
|
185
185
|
expect(matterbridge).toBeDefined();
|
|
186
186
|
expect(matterbridge).toBeInstanceOf(Matterbridge);
|
|
187
|
-
matterbridge.matterbridgeVersion = '3.
|
|
187
|
+
matterbridge.matterbridgeVersion = '3.4.0';
|
|
188
188
|
matterbridge.bridgeMode = 'bridge';
|
|
189
189
|
matterbridge.rootDirectory = path.join('jest', name);
|
|
190
190
|
matterbridge.homeDirectory = path.join('jest', name);
|
|
@@ -310,6 +310,13 @@ export function createTestEnvironment(name) {
|
|
|
310
310
|
new MdnsService(environment);
|
|
311
311
|
return environment;
|
|
312
312
|
}
|
|
313
|
+
export async function destroyTestEnvironment() {
|
|
314
|
+
const mdns = environment.get(MdnsService);
|
|
315
|
+
if (mdns && typeof mdns[Symbol.asyncDispose] === 'function')
|
|
316
|
+
await mdns[Symbol.asyncDispose]();
|
|
317
|
+
if (mdns && typeof mdns.close === 'function')
|
|
318
|
+
await mdns.close();
|
|
319
|
+
}
|
|
313
320
|
export async function flushAsync(ticks = 3, microTurns = 10, pause = 250) {
|
|
314
321
|
for (let i = 0; i < ticks; i++)
|
|
315
322
|
await new Promise((resolve) => setImmediate(resolve));
|
|
@@ -318,6 +325,35 @@ export async function flushAsync(ticks = 3, microTurns = 10, pause = 250) {
|
|
|
318
325
|
if (pause)
|
|
319
326
|
await new Promise((resolve) => setTimeout(resolve, pause));
|
|
320
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
|
+
}
|
|
321
357
|
export async function flushAllEndpointNumberPersistence(targetServer, rounds = 2) {
|
|
322
358
|
const nodeStore = targetServer.env.get(ServerNodeStore);
|
|
323
359
|
for (let i = 0; i < rounds; i++) {
|
|
@@ -353,6 +389,9 @@ export async function assertAllEndpointNumbersPersisted(targetServer) {
|
|
|
353
389
|
}
|
|
354
390
|
return all.length;
|
|
355
391
|
}
|
|
392
|
+
export async function closeServerNodeStores(targetServer) {
|
|
393
|
+
await targetServer?.env.get(ServerNodeStore)?.endpointStores.close();
|
|
394
|
+
}
|
|
356
395
|
export async function startServerNode(name, port, deviceType = bridge.code) {
|
|
357
396
|
const { randomBytes } = await import('node:crypto');
|
|
358
397
|
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
|
+
}
|
|
@@ -122,25 +122,13 @@ export class MatterbridgePlatform {
|
|
|
122
122
|
this.log.debug(`The plugin ${CYAN}${config.name}${db} doesn't override onConfigChanged. Received new config.`);
|
|
123
123
|
}
|
|
124
124
|
saveConfig(config) {
|
|
125
|
-
|
|
126
|
-
if (!plugin) {
|
|
127
|
-
throw new Error(`Plugin ${this.name} not found`);
|
|
128
|
-
}
|
|
129
|
-
this.matterbridge.plugins.saveConfigFromJson(plugin, config);
|
|
125
|
+
this.#server.request({ type: 'plugins_saveconfigfromjson', src: 'platform', dst: 'plugins', params: { name: this.name, config } });
|
|
130
126
|
}
|
|
131
|
-
getSchema() {
|
|
132
|
-
|
|
133
|
-
if (!plugin || !isValidObject(plugin.schemaJson)) {
|
|
134
|
-
throw new Error(`Plugin ${this.name} not found`);
|
|
135
|
-
}
|
|
136
|
-
return plugin.schemaJson;
|
|
127
|
+
async getSchema() {
|
|
128
|
+
return (await this.#server.fetch({ type: 'plugins_getschema', src: 'platform', dst: 'plugins', params: { name: this.name } })).response.schema;
|
|
137
129
|
}
|
|
138
130
|
setSchema(schema) {
|
|
139
|
-
|
|
140
|
-
if (!plugin) {
|
|
141
|
-
throw new Error(`Plugin ${this.name} not found`);
|
|
142
|
-
}
|
|
143
|
-
plugin.schemaJson = schema;
|
|
131
|
+
this.#server.request({ type: 'plugins_setschema', src: 'platform', dst: 'plugins', params: { name: this.name, schema } });
|
|
144
132
|
}
|
|
145
133
|
wssSendRestartRequired(snackbar = true, fixed = false) {
|
|
146
134
|
this.#server.request({ type: 'frontend_restartrequired', src: 'platform', dst: 'frontend', params: { snackbar, fixed } });
|
|
@@ -183,7 +171,7 @@ export class MatterbridgePlatform {
|
|
|
183
171
|
if (this.matterbridge.bridgeMode === 'bridge') {
|
|
184
172
|
aggregator = this.matterbridge.aggregatorNode;
|
|
185
173
|
}
|
|
186
|
-
else if (this.matterbridge.bridgeMode === 'childbridge') {
|
|
174
|
+
else if (this.matterbridge.bridgeMode === 'childbridge' && this.type === 'DynamicPlatform') {
|
|
187
175
|
aggregator = this.matterbridge.plugins.get(this.name)?.aggregatorNode;
|
|
188
176
|
}
|
|
189
177
|
if (aggregator) {
|
|
@@ -197,6 +185,7 @@ export class MatterbridgePlatform {
|
|
|
197
185
|
return true;
|
|
198
186
|
}
|
|
199
187
|
}
|
|
188
|
+
this.log.warn(`Virtual device ${name} not created. Virtual devices are only supported in bridge mode and childbridge mode with a DynamicPlatform.`);
|
|
200
189
|
return false;
|
|
201
190
|
}
|
|
202
191
|
async registerDevice(device) {
|
package/dist/pluginManager.js
CHANGED
|
@@ -51,6 +51,7 @@ export class PluginManager extends EventEmitter {
|
|
|
51
51
|
this.server.respond({ ...msg, response: { plugin: this.toApiPlugin(plugin) } });
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
|
+
this.log.debug(`***Plugin ${plg}${msg.params.name}${db} not found in plugins_get`);
|
|
54
55
|
this.server.respond({ ...msg, response: { plugin: undefined } });
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -158,6 +159,41 @@ export class PluginManager extends EventEmitter {
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
break;
|
|
162
|
+
case 'plugins_getschema':
|
|
163
|
+
{
|
|
164
|
+
const plugin = this.get(msg.params.name);
|
|
165
|
+
if (plugin) {
|
|
166
|
+
this.server.respond({ ...msg, response: { schema: plugin.schemaJson } });
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
this.server.respond({ ...msg, response: { schema: undefined } });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'plugins_setschema':
|
|
174
|
+
{
|
|
175
|
+
const plugin = this.get(msg.params.name);
|
|
176
|
+
if (plugin) {
|
|
177
|
+
plugin.schemaJson = msg.params.schema;
|
|
178
|
+
this.server.respond({ ...msg, response: { success: true } });
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.server.respond({ ...msg, response: { success: false } });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case 'plugins_saveconfigfromjson':
|
|
186
|
+
{
|
|
187
|
+
const plugin = this.get(msg.params.name);
|
|
188
|
+
if (plugin) {
|
|
189
|
+
this.saveConfigFromJson(plugin, msg.params.config, msg.params.restartRequired);
|
|
190
|
+
this.server.respond({ ...msg, response: { success: true } });
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.server.respond({ ...msg, response: { success: false } });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
161
197
|
default:
|
|
162
198
|
if (this.verbose)
|
|
163
199
|
this.log.debug(`Unknown broadcast message ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}`);
|
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-20251125-6f11a4f",
|
|
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-20251125-6f11a4f",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@matter/main": "0.15.6",
|
package/package.json
CHANGED
package/scripts/fetch-chip.mjs
CHANGED
|
@@ -11,13 +11,14 @@ import https from 'node:https';
|
|
|
11
11
|
* - ZCL_OUT: output path for zcl.json (default: chip/zcl.json)
|
|
12
12
|
* - ZCL_BRANCH: connectedhomeip branch to fetch from (default: v1.4-branch)
|
|
13
13
|
*/
|
|
14
|
-
const OUT_PATH = process.env.ZCL_OUT || 'chip/zcl.json';
|
|
15
14
|
// Single branch strategy from online only
|
|
16
|
-
const BRANCH = process.env.ZCL_BRANCH || 'v1.4-branch';
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
15
|
+
const BRANCH = process.env.ZCL_BRANCH || 'v1.4.2-branch';
|
|
16
|
+
const OUT_ZCL_PATH = process.env.ZCL_OUT || `chip/${BRANCH}/zcl.json`;
|
|
17
|
+
const OUT_MANUFACTURERS_PATH = process.env.MANUFACTURERS_OUT || `chip/${BRANCH}/manufacturers.xml`;
|
|
18
|
+
const ZCL_BASE_URL = `https://raw.githubusercontent.com/project-chip/connectedhomeip/${BRANCH}/src/app/zap-templates/zcl/`;
|
|
19
|
+
const ZCL_JSON_URL = ZCL_BASE_URL + 'zcl.json';
|
|
20
|
+
const MANUFACTURERS_URL = ZCL_BASE_URL + 'data-model/manufacturers.xml';
|
|
21
|
+
const XML_BASE_URL = ZCL_BASE_URL + 'data-model/chip/';
|
|
21
22
|
|
|
22
23
|
function fetchUrl(url) {
|
|
23
24
|
return new Promise((resolve, reject) => {
|
|
@@ -37,7 +38,7 @@ function fetchUrl(url) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
async function main() {
|
|
40
|
-
await mkdir(dirname(
|
|
41
|
+
await mkdir(dirname(OUT_ZCL_PATH), { recursive: true });
|
|
41
42
|
// Always fetch online from the specified branch
|
|
42
43
|
const data = await fetchUrl(ZCL_JSON_URL);
|
|
43
44
|
|
|
@@ -52,15 +53,14 @@ async function main() {
|
|
|
52
53
|
process.stderr.write('Warning: zcl.json does not contain expected ZAP ZCL properties (xmlFile). Saving anyway for manual inspection.\n');
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
await writeFile(
|
|
56
|
-
process.stderr.write(`Saved ${
|
|
56
|
+
await writeFile(OUT_ZCL_PATH, JSON.stringify(parsed, null, 2));
|
|
57
|
+
process.stderr.write(`Saved ${OUT_ZCL_PATH}. Branch=${BRANCH}\n`);
|
|
57
58
|
|
|
58
59
|
// Also fetch manufacturers.xml to chip/manufacturers.xml from the same branch
|
|
59
60
|
try {
|
|
60
|
-
const outManu = 'chip/manufacturers.xml';
|
|
61
61
|
const manuContent = await fetchUrl(MANUFACTURERS_URL);
|
|
62
|
-
await writeFile(
|
|
63
|
-
process.stderr.write(
|
|
62
|
+
await writeFile(OUT_MANUFACTURERS_PATH, manuContent);
|
|
63
|
+
process.stderr.write(`Saved ${OUT_MANUFACTURERS_PATH}. Branch=${BRANCH}\n`);
|
|
64
64
|
} catch (e) {
|
|
65
65
|
process.stderr.write(`Warning: failed to fetch manufacturers.xml (${e.message}).\n`);
|
|
66
66
|
}
|
|
@@ -70,14 +70,14 @@ async function main() {
|
|
|
70
70
|
if (xmlFiles.length === 0) {
|
|
71
71
|
process.stderr.write('No xmlFile entries found; skipping XML fetch.\n');
|
|
72
72
|
} else {
|
|
73
|
-
const outXmlBase =
|
|
73
|
+
const outXmlBase = `chip/${BRANCH}/xml`;
|
|
74
74
|
await mkdir(outXmlBase, { recursive: true });
|
|
75
75
|
let ok = 0;
|
|
76
76
|
let fail = 0;
|
|
77
77
|
for (const fileName of xmlFiles) {
|
|
78
78
|
const relative = fileName.trim();
|
|
79
79
|
try {
|
|
80
|
-
const url =
|
|
80
|
+
const url = XML_BASE_URL + relative;
|
|
81
81
|
const content = await fetchUrl(url);
|
|
82
82
|
const outPath = pathJoin(outXmlBase, relative.replaceAll('/', pathSep));
|
|
83
83
|
await mkdir(dirname(outPath), { recursive: true });
|