matterbridge 3.2.6-dev-20250904-a79f653 → 3.2.6-dev-20250906-4b022a0

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 CHANGED
@@ -8,7 +8,7 @@ If you like this project and find it useful, please consider giving it a star on
8
8
  <img src="bmc-button.svg" alt="Buy me a coffee" width="120">
9
9
  </a>
10
10
 
11
- ## [3.2.6] - 2025-09-??
11
+ ## [3.2.6] - 2025-09-05
12
12
 
13
13
  ### Added
14
14
 
@@ -27,6 +27,8 @@ If you like this project and find it useful, please consider giving it a star on
27
27
  - [AirConditioner]: Added AirConditioner() class and Jest test. It is not supported by the Home app. Improved createDefaultThermostatUserInterfaceConfigurationClusterServer().
28
28
  - [DeviceTypes]: Add Chapter 10. Media Device Types.
29
29
  - [Speaker]: Added Speaker() class and Jest test. Supported only by SmartThings.
30
+ - [mb_mdns]: Added help screen and the ability to filter mDNS packets. Useful to see all paired and commissionable Matter devices on the network.
31
+ - [matter.js]: Removed legacy and deprecated calls to Logger.setLogger etc. and use Logger.destinations.
30
32
 
31
33
  ### Changed
32
34
 
package/README.md CHANGED
@@ -291,7 +291,7 @@ An Accessory platform plugin only exposes one device.
291
291
 
292
292
  This is an example of a dynamic platform plugin.
293
293
 
294
- It exposes 56 virtual devices.
294
+ It exposes 57 virtual devices.
295
295
 
296
296
  All these virtual devices continuously change state and position. The plugin also shows how to use all the command handlers (you can control all the devices).
297
297
 
@@ -171,9 +171,11 @@ export class Dgram extends EventEmitter {
171
171
  }
172
172
  return interfaceNames;
173
173
  }
174
- getIpv6ScopeIdForAllInterfacesAddress() {
174
+ getIpv6ScopeId(interfaceName) {
175
175
  const interfaces = os.networkInterfaces();
176
176
  for (const name in interfaces) {
177
+ if (interfaceName && name !== interfaceName)
178
+ continue;
177
179
  const iface = interfaces[name];
178
180
  if (iface) {
179
181
  const ipv6Address = iface.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.scopeid);
@@ -1,9 +1,53 @@
1
+ import { getParameter, getStringArrayParameter, hasParameter } from '../utils/commandLine.js';
1
2
  import { MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT } from './multicast.js';
2
3
  import { Mdns } from './mdns.js';
3
4
  {
4
- const mdnsIpv4 = new Mdns('mDNS Server udp4', MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_PORT, 'udp4', true, undefined, '0.0.0.0');
5
- const mdnsIpv6 = new Mdns('mDNS Server udp6', MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT, 'udp6', true, undefined, '::');
5
+ if (hasParameter('h') || hasParameter('help')) {
6
+ console.log(`Usage: mb_mdns [options]
7
+
8
+ Options:
9
+ -h, --help Show this help message and exit.
10
+ --interfaceName <name> Network interface name to bind to (default all interfaces).
11
+ --ipv4InterfaceAddress <address> IPv4 address of the network interface to bind to (default: 0.0.0.0).
12
+ --ipv6InterfaceAddress <address> IPv6 address of the network interface to bind to (default: ::).
13
+ --outgoingIpv4InterfaceAddress <address> Outgoing IPv4 address (default first external address).
14
+ --outgoingIpv6InterfaceAddress <address> Outgoing IPv6 address (default first external address).
15
+ --advertise Enable mDNS advertisement (default: disabled).
16
+ --query Enable mDNS query (default: disabled).
17
+ --filter <string> Filter string to match in the mDNS record name (can be repeated).
18
+ -v, --verbose Enable verbose logging (default: disabled).
19
+
20
+ Examples:
21
+ # List Matter device commissioner service records only on eth0 interface
22
+ mb_mdns --interfaceName eth0 --filter _matterc._udp
23
+
24
+ # List Matter device service records only on eth0 interface
25
+ mb_mdns --interfaceName eth0 --filter _matter._tcp
26
+
27
+ # List both Matter commissioner and device service records only on eth0 interface
28
+ mb_mdns --interfaceName eth0 --filter _matterc._udp _matter._tcp
29
+
30
+ # Query for mDNS devices every 10s on a specific interface
31
+ mb_mdns --interfaceName eth0 --query
32
+ `);
33
+ process.exit(0);
34
+ }
35
+ const mdnsIpv4 = new Mdns('mDNS Server udp4', MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_PORT, 'udp4', true, getParameter('interfaceName'), getParameter('ipv4InterfaceAddress') || '0.0.0.0', getParameter('outgoingIpv4InterfaceAddress'));
36
+ const mdnsIpv6 = new Mdns('mDNS Server udp6', MDNS_MULTICAST_IPV6_ADDRESS, MDNS_MULTICAST_PORT, 'udp6', true, getParameter('interfaceName'), getParameter('ipv6InterfaceAddress') || '::', getParameter('outgoingIpv6InterfaceAddress'));
37
+ if (hasParameter('v') || hasParameter('verbose')) {
38
+ mdnsIpv4.log.logLevel = "debug";
39
+ mdnsIpv6.log.logLevel = "debug";
40
+ }
41
+ else {
42
+ mdnsIpv4.log.logLevel = "info";
43
+ mdnsIpv6.log.logLevel = "info";
44
+ }
6
45
  mdnsIpv4.listNetworkInterfaces();
46
+ const filters = getStringArrayParameter('filter');
47
+ if (filters) {
48
+ mdnsIpv4.filters.push(...filters);
49
+ mdnsIpv6.filters.push(...filters);
50
+ }
7
51
  function cleanupAndLogAndExit() {
8
52
  mdnsIpv4.stop();
9
53
  mdnsIpv6.stop();
@@ -18,6 +62,8 @@ import { Mdns } from './mdns.js';
18
62
  { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: false },
19
63
  { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: false },
20
64
  ]);
65
+ };
66
+ const advertiseUdp4 = () => {
21
67
  const ptrRdata = mdnsIpv4.encodeDnsName('matterbridge._http._tcp.local');
22
68
  mdnsIpv4.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
23
69
  };
@@ -28,6 +74,8 @@ import { Mdns } from './mdns.js';
28
74
  { name: '_http._tcp.local', type: 12, class: 1, unicastResponse: true },
29
75
  { name: '_services._dns-sd._udp.local', type: 12, class: 1, unicastResponse: true },
30
76
  ]);
77
+ };
78
+ const advertiseUdp6 = () => {
31
79
  const ptrRdata = mdnsIpv6.encodeDnsName('matterbridge._http._tcp.local');
32
80
  mdnsIpv6.sendResponse('_http._tcp.local', 12, 1, 120, ptrRdata);
33
81
  };
@@ -37,22 +85,34 @@ import { Mdns } from './mdns.js';
37
85
  mdnsIpv4.start();
38
86
  mdnsIpv4.on('ready', (address) => {
39
87
  mdnsIpv4.log.info(`mdnsIpv4 server ready on ${address.family} ${address.address}:${address.port}`);
40
- if (!process.argv.includes('--query'))
41
- return;
42
- queryUdp4();
43
- setInterval(() => {
88
+ if (getParameter('advertise')) {
89
+ advertiseUdp4();
90
+ setInterval(() => {
91
+ advertiseUdp4();
92
+ }, 10000).unref();
93
+ }
94
+ if (getParameter('query')) {
44
95
  queryUdp4();
45
- }, 10000).unref();
96
+ setInterval(() => {
97
+ queryUdp4();
98
+ }, 10000).unref();
99
+ }
46
100
  });
47
101
  mdnsIpv6.start();
48
102
  mdnsIpv6.on('ready', (address) => {
49
103
  mdnsIpv6.log.info(`mdnsIpv6 server ready on ${address.family} ${address.address}:${address.port}`);
50
- if (!process.argv.includes('--query'))
51
- return;
52
- queryUdp6();
53
- setInterval(() => {
104
+ if (getParameter('advertise')) {
105
+ advertiseUdp6();
106
+ setInterval(() => {
107
+ advertiseUdp6();
108
+ }, 10000).unref();
109
+ }
110
+ if (getParameter('query')) {
54
111
  queryUdp6();
55
- }, 10000).unref();
112
+ setInterval(() => {
113
+ queryUdp6();
114
+ }, 10000).unref();
115
+ }
56
116
  });
57
117
  setTimeout(() => {
58
118
  cleanupAndLogAndExit();
@@ -108,8 +108,9 @@ export var DnsClassFlag;
108
108
  export class Mdns extends Multicast {
109
109
  deviceQueries = new Map();
110
110
  deviceResponses = new Map();
111
- constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
112
- super(name, multicastAddress, multicastPort, socketType, reuseAddr, interfaceName, interfaceAddress);
111
+ filters = [];
112
+ constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress, outgoingInterfaceAddress) {
113
+ super(name, multicastAddress, multicastPort, socketType, reuseAddr, interfaceName, interfaceAddress, outgoingInterfaceAddress);
113
114
  }
114
115
  onQuery(rinfo, _query) {
115
116
  this.log.debug(`mDNS query received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
@@ -118,7 +119,8 @@ export class Mdns extends Multicast {
118
119
  this.log.debug(`mDNS response received from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
119
120
  }
120
121
  onMessage(msg, rinfo) {
121
- this.log.info(`Dgram mDNS server received a mDNS message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
122
+ if (this.filters.length === 0)
123
+ this.log.info(`Dgram mDNS server received a mDNS message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
122
124
  try {
123
125
  const result = this.decodeMdnsMessage(msg);
124
126
  if (result.qr === 0) {
@@ -136,6 +138,21 @@ export class Mdns extends Multicast {
136
138
  this.deviceResponses.set(rinfo.address, { rinfo, response: result, dataPTR: ptr?.type === 12 ? ptr?.data : ptr?.name });
137
139
  this.onResponse(rinfo, result);
138
140
  }
141
+ if (this.filters.length > 0) {
142
+ this.log.debug(`mDNS message filtered out by filters: ${this.filters.join(', ')}`);
143
+ for (const filter of this.filters) {
144
+ const foundInQuestions = result.questions?.some((q) => q.name.includes(filter));
145
+ const foundInAnswers = result.answers?.some((a) => a.name.includes(filter) || a.data.includes(filter));
146
+ const foundInAdditionals = result.additionals?.some((a) => a.name.includes(filter) || a.data.includes(filter));
147
+ if (foundInQuestions || foundInAnswers || foundInAdditionals) {
148
+ this.log.info(`Dgram mDNS server received a mDNS message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
149
+ this.logMdnsMessage(result);
150
+ return;
151
+ }
152
+ }
153
+ this.log.debug(`mDNS message does not match any filter, ignoring.`);
154
+ return;
155
+ }
139
156
  this.logMdnsMessage(result);
140
157
  }
141
158
  catch (error) {
@@ -10,11 +10,13 @@ export const COAP_MULTICAST_PORT = 5683;
10
10
  export class Multicast extends Dgram {
11
11
  multicastAddress;
12
12
  multicastPort;
13
+ outgoingInterfaceAddress;
13
14
  joinedInterfaces = [];
14
- constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
15
+ constructor(name, multicastAddress, multicastPort, socketType, reuseAddr = true, interfaceName, interfaceAddress, outgoingInterfaceAddress) {
15
16
  super(name, socketType, reuseAddr, interfaceName, interfaceAddress);
16
17
  this.multicastAddress = multicastAddress;
17
18
  this.multicastPort = multicastPort;
19
+ this.outgoingInterfaceAddress = outgoingInterfaceAddress;
18
20
  }
19
21
  start() {
20
22
  if (this.socketType === 'udp4') {
@@ -86,9 +88,12 @@ export class Multicast extends Dgram {
86
88
  }
87
89
  }
88
90
  });
89
- let interfaceAddress = this.interfaceAddress;
90
- if (this.socketType === 'udp6' && this.interfaceAddress === '::') {
91
- interfaceAddress = '::' + this.getIpv6ScopeIdForAllInterfacesAddress();
91
+ let interfaceAddress = this.outgoingInterfaceAddress || this.interfaceAddress;
92
+ if (!this.outgoingInterfaceAddress && this.socketType === 'udp4' && this.interfaceAddress === '0.0.0.0') {
93
+ interfaceAddress = this.getIpv4InterfaceAddress(this.interfaceName);
94
+ }
95
+ if (!this.outgoingInterfaceAddress && this.socketType === 'udp6' && this.interfaceAddress === '::') {
96
+ interfaceAddress = '::' + this.getIpv6ScopeId(this.interfaceName);
92
97
  }
93
98
  this.log.debug(`Dgram multicast socket setting multicastInterface to ${BLUE}${interfaceAddress}${db} for ${BLUE}${address.family}${db} ${BLUE}${address.address}${db}:${BLUE}${address.port}${db}`);
94
99
  this.socket.setMulticastInterface(interfaceAddress);
package/dist/frontend.js CHANGED
@@ -8,7 +8,7 @@ import express from 'express';
8
8
  import WebSocket, { WebSocketServer } from 'ws';
9
9
  import multer from 'multer';
10
10
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
11
- import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle } from '@matter/main';
11
+ import { Logger, LogLevel as MatterLogLevel, Lifecycle } from '@matter/main';
12
12
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
13
13
  import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
14
14
  import { plg } from './matterbridgeTypes.js';
@@ -1443,23 +1443,10 @@ export class Frontend extends EventEmitter {
1443
1443
  this.matterbridge.matterbridgeInformation.matterFileLogger = data.params.value;
1444
1444
  await this.matterbridge.nodeContext?.set('matterFileLog', data.params.value);
1445
1445
  if (data.params.value) {
1446
- try {
1447
- Logger.addLogger('matterfilelogger', await this.matterbridge.createMatterFileLogger(path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile), true), {
1448
- defaultLogLevel: this.matterbridge.matterbridgeInformation.matterLoggerLevel,
1449
- logFormat: MatterLogFormat.PLAIN,
1450
- });
1451
- }
1452
- catch (error) {
1453
- this.log.debug(`Error adding the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1454
- }
1446
+ this.matterbridge.matterLog.logFilePath = path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile);
1455
1447
  }
1456
1448
  else {
1457
- try {
1458
- Logger.removeLogger('matterfilelogger');
1459
- }
1460
- catch (error) {
1461
- this.log.debug(`Error removing the matterfilelogger for file ${CYAN}${path.join(this.matterbridge.matterbridgeDirectory, this.matterbridge.matterLoggerFile)}${er}: ${error instanceof Error ? error.message : error}`);
1462
- }
1449
+ this.matterbridge.matterLog.logFilePath = undefined;
1463
1450
  }
1464
1451
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
1465
1452
  }
@@ -117,6 +117,7 @@ export class Matterbridge extends EventEmitter {
117
117
  sigtermHandler;
118
118
  exceptionHandler;
119
119
  rejectionHandler;
120
+ matterLog = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4, logLevel: "debug" });
120
121
  environment = Environment.default;
121
122
  matterStorageName = 'matterstorage' + (getParameter('profile') ? '.' + getParameter('profile') : '');
122
123
  matterStorageService;
@@ -408,15 +409,11 @@ export class Matterbridge extends EventEmitter {
408
409
  Logger.level = (await this.nodeContext.get('matterLogLevel', this.matterbridgeInformation.shellyBoard ? MatterLogLevel.NOTICE : MatterLogLevel.INFO));
409
410
  }
410
411
  Logger.format = MatterLogFormat.ANSI;
411
- Logger.setLogger('default', this.createMatterLogger());
412
- this.matterbridgeInformation.matterLoggerLevel = Logger.level;
413
412
  if (hasParameter('matterfilelogger') || (await this.nodeContext.get('matterFileLog', false))) {
414
413
  this.matterbridgeInformation.matterFileLogger = true;
415
- Logger.addLogger('matterfilelogger', await this.createMatterFileLogger(path.join(this.matterbridgeDirectory, this.matterLoggerFile), true), {
416
- defaultLogLevel: Logger.level,
417
- logFormat: MatterLogFormat.PLAIN,
418
- });
419
414
  }
415
+ Logger.destinations.default.write = this.createDestinationMatterLogger(this.matterbridgeInformation.matterFileLogger);
416
+ this.matterbridgeInformation.matterLoggerLevel = Logger.level;
420
417
  this.log.debug(`Matter logLevel: ${Logger.level} fileLoger: ${this.matterbridgeInformation.matterFileLogger}.`);
421
418
  const networkInterfaces = os.networkInterfaces();
422
419
  const availableAddresses = Object.entries(networkInterfaces);
@@ -921,77 +918,32 @@ export class Matterbridge extends EventEmitter {
921
918
  const cmdArgs = process.argv.slice(2).join(' ');
922
919
  this.log.debug(`Command Line Arguments: ${cmdArgs}`);
923
920
  }
924
- createMatterLogger() {
925
- const matterLogger = new AnsiLogger({ logName: 'Matter', logTimestampFormat: 4, logLevel: "debug" });
926
- return (level, formattedLog) => {
927
- const logger = formattedLog.slice(44, 44 + 20).trim();
928
- const message = formattedLog.slice(65);
929
- matterLogger.logName = logger;
930
- switch (level) {
931
- case MatterLogLevel.DEBUG:
932
- matterLogger.log("debug", message);
933
- break;
934
- case MatterLogLevel.INFO:
935
- matterLogger.log("info", message);
936
- break;
937
- case MatterLogLevel.NOTICE:
938
- matterLogger.log("notice", message);
939
- break;
940
- case MatterLogLevel.WARN:
941
- matterLogger.log("warn", message);
942
- break;
943
- case MatterLogLevel.ERROR:
944
- matterLogger.log("error", message);
945
- break;
946
- case MatterLogLevel.FATAL:
947
- matterLogger.log("fatal", message);
948
- break;
949
- }
950
- };
951
- }
952
- async createMatterFileLogger(filePath, unlink = false) {
953
- let fileSize = 0;
954
- if (unlink) {
955
- try {
956
- await fs.unlink(filePath);
957
- }
958
- catch (error) {
959
- this.log.debug(`Error unlinking the log file ${CYAN}${filePath}${db}: ${error instanceof Error ? error.message : error}`);
960
- }
961
- }
962
- return async (level, formattedLog) => {
963
- if (fileSize > 100000000) {
964
- return;
965
- }
966
- fileSize += formattedLog.length;
967
- if (fileSize > 100000000) {
968
- await fs.appendFile(filePath, `Logging on file has been stopped because the file size is greater than 100MB.` + os.EOL);
969
- return;
970
- }
971
- const now = new Date();
972
- const timestamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`;
973
- const message = formattedLog.slice(24);
974
- const parts = message.split(' ');
975
- const logger = parts[1];
976
- const finalMessage = parts.slice(2).join(' ') + os.EOL;
977
- switch (level) {
921
+ createDestinationMatterLogger(fileLogger) {
922
+ if (fileLogger) {
923
+ this.matterLog.logFilePath = path.join(this.matterbridgeDirectory, this.matterLoggerFile);
924
+ }
925
+ return (text, message) => {
926
+ const logger = text.slice(44, 44 + 20).trim();
927
+ const msg = text.slice(65);
928
+ this.matterLog.logName = logger;
929
+ switch (message.level) {
978
930
  case MatterLogLevel.DEBUG:
979
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [debug] ${finalMessage}`);
931
+ this.matterLog.log("debug", msg);
980
932
  break;
981
933
  case MatterLogLevel.INFO:
982
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [info] ${finalMessage}`);
934
+ this.matterLog.log("info", msg);
983
935
  break;
984
936
  case MatterLogLevel.NOTICE:
985
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [notice] ${finalMessage}`);
937
+ this.matterLog.log("notice", msg);
986
938
  break;
987
939
  case MatterLogLevel.WARN:
988
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [warn] ${finalMessage}`);
940
+ this.matterLog.log("warn", msg);
989
941
  break;
990
942
  case MatterLogLevel.ERROR:
991
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [error] ${finalMessage}`);
943
+ this.matterLog.log("error", msg);
992
944
  break;
993
945
  case MatterLogLevel.FATAL:
994
- await fs.appendFile(filePath, `[${timestamp}] [${logger}] [fatal] ${finalMessage}`);
946
+ this.matterLog.log("fatal", msg);
995
947
  break;
996
948
  }
997
949
  };
@@ -9,3 +9,4 @@ export * from './createDirectory.js';
9
9
  export * from './createZip.js';
10
10
  export * from './wait.js';
11
11
  export * from './hex.js';
12
+ export * from './error.js';
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.2.6-dev-20250904-a79f653",
3
+ "version": "3.2.6-dev-20250906-4b022a0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.2.6-dev-20250904-a79f653",
9
+ "version": "3.2.6-dev-20250906-4b022a0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.2.6-dev-20250904-a79f653",
3
+ "version": "3.2.6-dev-20250906-4b022a0",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",