matterbridge 3.1.7-dev-20250724-c3522e6 → 3.1.8-dev-20250725-5cc0474

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,11 +8,12 @@ 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.1.6] - 2025-07-??
11
+ ## [3.1.8] - 2025-07-??
12
12
 
13
13
  ### Added
14
14
 
15
- - [operationalState]: Improved documentation on createDefaultOperationalStateClusterServer() and added the optional attribute countdownTime. Thanks Ludovic BOUÉ (https://github.com/Luligu/matterbridge/pull/363).
15
+ - [certification]: Improved certification management in pairing.json.
16
+ - [workflow]: Update permissions and change GitHub token for Docker build trigger
16
17
 
17
18
  ### Changed
18
19
 
@@ -22,7 +23,7 @@ If you like this project and find it useful, please consider giving it a star on
22
23
  <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
23
24
  </a>
24
25
 
25
- ## [3.1.7] - 2025-07-24
26
+ ## [3.1.7] - 2025-07-25
26
27
 
27
28
  ### Added
28
29
 
@@ -32,6 +33,11 @@ If you like this project and find it useful, please consider giving it a star on
32
33
  - [docker]: Added on demand trigger for Build Docker Image dev from other plugins workflows.
33
34
  - [mdns]: Added bin mb_mdns.
34
35
  - [coap]: Added bin mb_coap.
36
+ - [operationalState]: Improved documentation on createDefaultOperationalStateClusterServer() and added the optional attribute countdownTime. Thanks Ludovic BOUÉ (https://github.com/Luligu/matterbridge/pull/363).
37
+ - [momentarySwitch]: Added createDefaultMomentarySwitchClusterServer(). It creates a single click only switch. It is supported by the Home app.
38
+ - [fixedLabels]: Improved documentation and added character length check.
39
+ - [userLabels]: Improved documentation and added character length check.
40
+ - [certification]: Improved certification management in pairing.json with new properties.
35
41
 
36
42
  ### Changed
37
43
 
package/README-SERVICE.md CHANGED
@@ -207,5 +207,6 @@ or if you prefers to only give access to npm without password try with (e.g. rad
207
207
  save the file and reload the settings with:
208
208
 
209
209
  ```bash
210
+ sudo chmod 0440 /etc/sudoers.d/matterbridge
210
211
  sudo visudo -c
211
212
  ```
@@ -128,8 +128,12 @@ export class Mdns extends Multicast {
128
128
  else {
129
129
  const ptr = result.answers?.find((record) => record.name === '_shelly._tcp.local' && record.type === 12) ||
130
130
  result.answers?.find((record) => record.name === '_http._tcp.local' && record.type === 12) ||
131
- result.answers?.find((record) => record.type === 12);
132
- this.deviceResponses.set(rinfo.address, { rinfo, response: result, dataPTR: ptr?.data });
131
+ result.answers?.find((record) => record.type === 12) ||
132
+ result.answers?.find((record) => record.type === 16) ||
133
+ result.answers
134
+ ? result.answers[0]
135
+ : undefined;
136
+ this.deviceResponses.set(rinfo.address, { rinfo, response: result, dataPTR: ptr?.type === 12 ? ptr?.data : ptr?.name });
133
137
  this.onResponse(rinfo, result);
134
138
  }
135
139
  this.logMdnsMessage(result);
@@ -136,6 +136,9 @@ export class Matterbridge extends EventEmitter {
136
136
  aggregatorVendorName = getParameter('vendorName') ?? 'Matterbridge';
137
137
  aggregatorProductId = getIntParameter('productId') ?? 0x8000;
138
138
  aggregatorProductName = getParameter('productName') ?? 'Matterbridge aggregator';
139
+ aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
140
+ aggregatorSerialNumber = getParameter('serialNumber');
141
+ aggregatorUniqueId = getParameter('uniqueId');
139
142
  static instance;
140
143
  constructor() {
141
144
  super();
@@ -286,29 +289,46 @@ export class Matterbridge extends EventEmitter {
286
289
  await fs.access(pairingFilePath, fs.constants.R_OK);
287
290
  const pairingFileContent = await fs.readFile(pairingFilePath, 'utf8');
288
291
  const pairingFileJson = JSON.parse(pairingFileContent);
289
- if (isValidNumber(pairingFileJson.vendorId))
292
+ if (isValidNumber(pairingFileJson.vendorId)) {
290
293
  this.aggregatorVendorId = VendorId(pairingFileJson.vendorId);
291
- if (isValidString(pairingFileJson.vendorName, 3))
294
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorId ${CYAN}${this.aggregatorVendorId}${nf} from pairing file.`);
295
+ }
296
+ if (isValidString(pairingFileJson.vendorName, 3)) {
292
297
  this.aggregatorVendorName = pairingFileJson.vendorName;
293
- if (isValidNumber(pairingFileJson.productId))
298
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using vendorName ${CYAN}${this.aggregatorVendorName}${nf} from pairing file.`);
299
+ }
300
+ if (isValidNumber(pairingFileJson.productId)) {
294
301
  this.aggregatorProductId = pairingFileJson.productId;
295
- if (isValidString(pairingFileJson.productName, 3))
302
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productId ${CYAN}${this.aggregatorProductId}${nf} from pairing file.`);
303
+ }
304
+ if (isValidString(pairingFileJson.productName, 3)) {
296
305
  this.aggregatorProductName = pairingFileJson.productName;
306
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using productName ${CYAN}${this.aggregatorProductName}${nf} from pairing file.`);
307
+ }
308
+ if (isValidNumber(pairingFileJson.deviceType)) {
309
+ this.aggregatorDeviceType = DeviceTypeId(pairingFileJson.deviceType);
310
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using deviceType ${CYAN}${this.aggregatorDeviceType}(0x${this.aggregatorDeviceType.toString(16).padStart(4, '0')})${nf} from pairing file.`);
311
+ }
312
+ if (isValidString(pairingFileJson.serialNumber, 3)) {
313
+ this.aggregatorSerialNumber = pairingFileJson.serialNumber;
314
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using serialNumber ${CYAN}${this.aggregatorSerialNumber}${nf} from pairing file.`);
315
+ }
316
+ if (isValidString(pairingFileJson.uniqueId, 3)) {
317
+ this.aggregatorUniqueId = pairingFileJson.uniqueId;
318
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using uniqueId ${CYAN}${this.aggregatorUniqueId}${nf} from pairing file.`);
319
+ }
297
320
  if (isValidNumber(pairingFileJson.passcode) && isValidNumber(pairingFileJson.discriminator)) {
298
321
  this.passcode = pairingFileJson.passcode;
299
322
  this.discriminator = pairingFileJson.discriminator;
300
323
  this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf} from pairing file.`);
301
324
  }
302
325
  if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
303
- const hexStringToUint8Array = (hexString) => {
304
- const matches = hexString.match(/.{1,2}/g);
305
- return matches ? new Uint8Array(matches.map((byte) => parseInt(byte, 16))) : new Uint8Array();
306
- };
326
+ const { hexToBuffer } = await import('./utils/hex.js');
307
327
  this.certification = {
308
- privateKey: hexStringToUint8Array(pairingFileJson.privateKey),
309
- certificate: hexStringToUint8Array(pairingFileJson.certificate),
310
- intermediateCertificate: hexStringToUint8Array(pairingFileJson.intermediateCertificate),
311
- declaration: hexStringToUint8Array(pairingFileJson.declaration),
328
+ privateKey: hexToBuffer(pairingFileJson.privateKey),
329
+ certificate: hexToBuffer(pairingFileJson.certificate),
330
+ intermediateCertificate: hexToBuffer(pairingFileJson.intermediateCertificate),
331
+ declaration: hexToBuffer(pairingFileJson.declaration),
312
332
  };
313
333
  this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`);
314
334
  }
@@ -647,6 +667,15 @@ export class Matterbridge extends EventEmitter {
647
667
  }
648
668
  try {
649
669
  await this.startMatterStorage();
670
+ if (this.aggregatorSerialNumber && this.aggregatorUniqueId && this.matterStorageService) {
671
+ const storageManager = await this.matterStorageService.open('Matterbridge');
672
+ const storageContext = storageManager?.createContext('persist');
673
+ if (this.aggregatorSerialNumber)
674
+ await storageContext?.set('serialNumber', this.aggregatorSerialNumber);
675
+ if (this.aggregatorUniqueId)
676
+ await storageContext?.set('uniqueId', this.aggregatorUniqueId);
677
+ this.matterbridgeInformation.matterbridgeSerialNumber = this.aggregatorSerialNumber;
678
+ }
650
679
  }
651
680
  catch (error) {
652
681
  this.log.fatal(`Fatal error creating matter storage: ${error instanceof Error ? error.message : error}`);
@@ -1200,7 +1229,7 @@ export class Matterbridge extends EventEmitter {
1200
1229
  async createDynamicPlugin(plugin) {
1201
1230
  if (!plugin.locked) {
1202
1231
  plugin.locked = true;
1203
- plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
1232
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, plugin.description);
1204
1233
  plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
1205
1234
  plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
1206
1235
  plugin.serialNumber = await plugin.storageContext.get('serialNumber', '');
@@ -1381,7 +1410,7 @@ export class Matterbridge extends EventEmitter {
1381
1410
  this.log.info(`Matter node storage service created: ${this.matterStorageService.location}`);
1382
1411
  this.matterStorageManager = await this.matterStorageService.open('Matterbridge');
1383
1412
  this.log.info('Matter node storage manager "Matterbridge" created');
1384
- this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', bridge.code, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName);
1413
+ this.matterbridgeContext = await this.createServerNodeContext('Matterbridge', 'Matterbridge', this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, this.aggregatorSerialNumber, this.aggregatorUniqueId);
1385
1414
  this.matterbridgeInformation.matterbridgeSerialNumber = await this.matterbridgeContext.get('serialNumber', '');
1386
1415
  this.log.info('Matter node storage started');
1387
1416
  await this.backupMatterStorage(path.join(this.matterbridgeDirectory, this.matterStorageName), path.join(this.matterbridgeDirectory, this.matterStorageName + '.backup'));
@@ -1404,7 +1433,7 @@ export class Matterbridge extends EventEmitter {
1404
1433
  this.matterbridgeContext = undefined;
1405
1434
  this.log.info('Matter node storage closed');
1406
1435
  }
1407
- async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber) {
1436
+ async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
1408
1437
  const { randomBytes } = await import('node:crypto');
1409
1438
  if (!this.matterStorageService)
1410
1439
  throw new Error('No storage service initialized');
@@ -1422,7 +1451,7 @@ export class Matterbridge extends EventEmitter {
1422
1451
  await storageContext.set('nodeLabel', productName.slice(0, 32));
1423
1452
  await storageContext.set('productLabel', productName.slice(0, 32));
1424
1453
  await storageContext.set('serialNumber', await storageContext.get('serialNumber', serialNumber ? serialNumber.slice(0, 32) : 'SN' + random));
1425
- await storageContext.set('uniqueId', await storageContext.get('uniqueId', 'UI' + random));
1454
+ await storageContext.set('uniqueId', await storageContext.get('uniqueId', uniqueId ? uniqueId.slice(0, 32) : 'UI' + random));
1426
1455
  await storageContext.set('softwareVersion', isValidNumber(parseVersionString(this.matterbridgeVersion), 0, UINT32_MAX) ? parseVersionString(this.matterbridgeVersion) : 1);
1427
1456
  await storageContext.set('softwareVersionString', isValidString(this.matterbridgeVersion, 5, 64) ? this.matterbridgeVersion : '1.0.0');
1428
1457
  await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.systemInformation.osRelease) : 1);
@@ -822,6 +822,15 @@ export class MatterbridgeEndpoint extends Endpoint {
822
822
  });
823
823
  return this;
824
824
  }
825
+ createOnOffFanControlClusterServer(fanMode = FanControl.FanMode.Off) {
826
+ this.behaviors.require(FanControlServer, {
827
+ fanMode,
828
+ fanModeSequence: FanControl.FanModeSequence.OffHigh,
829
+ percentSetting: 0,
830
+ percentCurrent: 0,
831
+ });
832
+ return this;
833
+ }
825
834
  createBaseFanControlClusterServer(fanMode = FanControl.FanMode.Off, fanModeSequence = FanControl.FanModeSequence.OffLowMedHigh, percentSetting = 0, percentCurrent = 0) {
826
835
  this.behaviors.require(FanControlServer, {
827
836
  fanMode,
@@ -381,15 +381,15 @@ export async function addFixedLabel(endpoint, label, value) {
381
381
  if (!endpoint.hasClusterServer(FixedLabel.Cluster.id)) {
382
382
  endpoint.log.debug(`addFixedLabel: add cluster ${hk}FixedLabel${db}:${hk}fixedLabel${db} with label ${CYAN}${label}${db} value ${CYAN}${value}${db}`);
383
383
  endpoint.behaviors.require(FixedLabelServer, {
384
- labelList: [{ label, value }],
384
+ labelList: [{ label: label.substring(0, 16), value: value.substring(0, 16) }],
385
385
  });
386
386
  return;
387
387
  }
388
388
  endpoint.log.debug(`addFixedLabel: add label ${CYAN}${label}${db} value ${CYAN}${value}${db}`);
389
389
  let labelList = endpoint.getAttribute(FixedLabel.Cluster.id, 'labelList', endpoint.log);
390
390
  if (isValidArray(labelList)) {
391
- labelList = labelList.filter((entry) => entry.label !== label);
392
- labelList.push({ label, value });
391
+ labelList = labelList.filter((entry) => entry.label !== label.substring(0, 16));
392
+ labelList.push({ label: label.substring(0, 16), value: value.substring(0, 16) });
393
393
  await endpoint.setAttribute(FixedLabel.Cluster.id, 'labelList', labelList, endpoint.log);
394
394
  }
395
395
  }
@@ -397,15 +397,15 @@ export async function addUserLabel(endpoint, label, value) {
397
397
  if (!endpoint.hasClusterServer(UserLabel.Cluster.id)) {
398
398
  endpoint.log.debug(`addUserLabel: add cluster ${hk}UserLabel${db}:${hk}userLabel${db} with label ${CYAN}${label}${db} value ${CYAN}${value}${db}`);
399
399
  endpoint.behaviors.require(UserLabelServer, {
400
- labelList: [{ label, value }],
400
+ labelList: [{ label: label.substring(0, 16), value: value.substring(0, 16) }],
401
401
  });
402
402
  return;
403
403
  }
404
404
  endpoint.log.debug(`addUserLabel: add label ${CYAN}${label}${db} value ${CYAN}${value}${db}`);
405
405
  let labelList = endpoint.getAttribute(UserLabel.Cluster.id, 'labelList', endpoint.log);
406
406
  if (isValidArray(labelList)) {
407
- labelList = labelList.filter((entry) => entry.label !== label);
408
- labelList.push({ label, value });
407
+ labelList = labelList.filter((entry) => entry.label !== label.substring(0, 16));
408
+ labelList.push({ label: label.substring(0, 16), value: value.substring(0, 16) });
409
409
  await endpoint.setAttribute(UserLabel.Cluster.id, 'labelList', labelList, endpoint.log);
410
410
  }
411
411
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.1.7-dev-20250724-c3522e6",
3
+ "version": "3.1.8-dev-20250725-5cc0474",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.1.7-dev-20250724-c3522e6",
9
+ "version": "3.1.8-dev-20250725-5cc0474",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.1.7-dev-20250724-c3522e6",
3
+ "version": "3.1.8-dev-20250725-5cc0474",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",