matterbridge 3.0.1-dev-20250507-a182295 → 3.0.2-dev-20250508-7214e17

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
@@ -13,6 +13,23 @@ If you like this project and find it useful, please consider giving it a star on
13
13
  If you want to run Matterbridge in Home Assistant please use the official add-on https://github.com/Luligu/matterbridge-home-assistant-addon that also has Ingress and side panel.
14
14
  It is also available the official Matterbridge Home Assistant plugin https://github.com/Luligu/matterbridge-hass.
15
15
 
16
+ ## [3.0.2] - 2025-05-??
17
+
18
+ ### Added
19
+
20
+ ### Changed
21
+
22
+ - [package]: Updated dependencies.
23
+ - [utils]: Refactor utils functions.
24
+ - [utils]: Updated Jest tests on utils functions.
25
+ - [devices]: Added RoboticVacuumCleaner class to create the Robotic Vacuum Cleaner device type.
26
+
27
+ ### Fixed
28
+
29
+ <a href="https://www.buymeacoffee.com/luligugithub">
30
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
31
+ </a>
32
+
16
33
  ## [3.0.1] - 2025-05-06
17
34
 
18
35
  ### Added
@@ -39,6 +56,10 @@ It is also available the official Matterbridge Home Assistant plugin https://git
39
56
  - [BasicInformation]: Fixed vulnerability in BasicInformation and BridgedDeviceBasicInformation cluster initialization attributes.
40
57
  - [frontend]: Fixed refresh and postfix for select in HomeDevices.
41
58
 
59
+ <a href="https://www.buymeacoffee.com/luligugithub">
60
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
61
+ </a>
62
+
42
63
  ## [3.0.0] - 2025-04-29
43
64
 
44
65
  ## Breaking changes
@@ -105,6 +126,10 @@ Modified clusters:
105
126
  - [matterbridge]: Fixed wrong message when advertising stops and the node has been paired.
106
127
  - [frontend]: Fixed download logs that broke with express v5.1.0.
107
128
 
129
+ <a href="https://www.buymeacoffee.com/luligugithub">
130
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
131
+ </a>
132
+
108
133
  ## [2.2.9] - 2025-04-18
109
134
 
110
135
  ### Added
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Docker Version](https://img.shields.io/docker/v/luligu/matterbridge?label=docker%20version&sort=semver)](https://hub.docker.com/r/luligu/matterbridge)
6
6
  [![Docker Pulls](https://img.shields.io/docker/pulls/luligu/matterbridge.svg)](https://hub.docker.com/r/luligu/matterbridge)
7
7
  ![Node.js CI](https://github.com/Luligu/matterbridge/actions/workflows/build.yml/badge.svg)
8
- ![Coverage](https://img.shields.io/badge/Jest%20coverage-84%25-brightgreen)
8
+ ![Coverage](https://img.shields.io/badge/Jest%20coverage-85%25-brightgreen)
9
9
 
10
10
  [![power by](https://img.shields.io/badge/powered%20by-matter--history-blue)](https://www.npmjs.com/package/matter-history)
11
11
  [![power by](https://img.shields.io/badge/powered%20by-node--ansi--logger-blue)](https://www.npmjs.com/package/node-ansi-logger)
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * from './matterbridgeDeviceTypes.js';
13
13
  export * from './matterbridgePlatform.js';
14
14
  export * from './matterbridgeAccessoryPlatform.js';
15
15
  export * from './matterbridgeDynamicPlatform.js';
16
+ export * from './roboticVacuumCleaner.js';
16
17
  const log = new AnsiLogger({ logName: 'Main', logTimestampFormat: 4, logLevel: hasParameter('debug') ? "debug" : "info" });
17
18
  async function main() {
18
19
  log.debug('***Matterbridge.loadInstance() called');
@@ -132,6 +132,7 @@ export class Matterbridge extends EventEmitter {
132
132
  port;
133
133
  passcode;
134
134
  discriminator;
135
+ certification;
135
136
  serverNode;
136
137
  aggregatorNode;
137
138
  aggregatorVendorId = VendorId(getIntParameter('vendorId') ?? 0xfff1);
@@ -267,6 +268,36 @@ export class Matterbridge extends EventEmitter {
267
268
  this.port = getIntParameter('port') ?? (await this.nodeContext.get('matterport', 5540)) ?? 5540;
268
269
  this.passcode = getIntParameter('passcode') ?? (await this.nodeContext.get('matterpasscode')) ?? PaseClient.generateRandomPasscode();
269
270
  this.discriminator = getIntParameter('discriminator') ?? (await this.nodeContext.get('matterdiscriminator')) ?? PaseClient.generateRandomDiscriminator();
271
+ const pairingFilePath = path.join(this.homeDirectory, '.mattercert', 'pairing.json');
272
+ try {
273
+ await fs.access(pairingFilePath, fs.constants.R_OK);
274
+ const pairingFileContent = await fs.readFile(pairingFilePath, 'utf8');
275
+ const pairingFileJson = JSON.parse(pairingFileContent);
276
+ if (pairingFileJson.passcode && pairingFileJson.discriminator) {
277
+ this.passcode = pairingFileJson.passcode;
278
+ this.discriminator = pairingFileJson.discriminator;
279
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using passcode ${CYAN}${this.passcode}${nf} and discriminator ${CYAN}${this.discriminator}${nf}`);
280
+ }
281
+ if (pairingFileJson.privateKey && pairingFileJson.certificate && pairingFileJson.intermediateCertificate && pairingFileJson.declaration) {
282
+ const hexStringToUint8Array = (hexString) => {
283
+ const matches = hexString.match(/.{1,2}/g);
284
+ return matches ? new Uint8Array(matches.map((byte) => parseInt(byte, 16))) : new Uint8Array();
285
+ };
286
+ this.certification = {
287
+ privateKey: hexStringToUint8Array(pairingFileJson.privateKey),
288
+ certificate: hexStringToUint8Array(pairingFileJson.certificate),
289
+ intermediateCertificate: hexStringToUint8Array(pairingFileJson.intermediateCertificate),
290
+ declaration: hexStringToUint8Array(pairingFileJson.declaration),
291
+ };
292
+ this.log.info(`Pairing file ${CYAN}${pairingFilePath}${nf} found. Using privateKey, certificate, intermediateCertificate and declaration from pairing file.`);
293
+ }
294
+ }
295
+ catch (error) {
296
+ this.log.debug(`Pairing file ${CYAN}${pairingFilePath}${db} not found: ${error instanceof Error ? error.message : error}`);
297
+ }
298
+ await this.nodeContext.set('matterport', this.port);
299
+ await this.nodeContext.set('matterpasscode', this.passcode);
300
+ await this.nodeContext.set('matterdiscriminator', this.discriminator);
270
301
  this.log.debug(`Initializing server node for Matterbridge... on port ${this.port} with passcode ${this.passcode} and discriminator ${this.discriminator}`);
271
302
  if (hasParameter('logger')) {
272
303
  const level = getParameter('logger');
@@ -1438,6 +1469,9 @@ export class Matterbridge extends EventEmitter {
1438
1469
  listeningAddressIpv6: this.ipv6address,
1439
1470
  port,
1440
1471
  },
1472
+ operationalCredentials: {
1473
+ certification: this.certification,
1474
+ },
1441
1475
  commissioning: {
1442
1476
  passcode,
1443
1477
  discriminator,
@@ -8,6 +8,9 @@ import { ValveConfigurationAndControl } from '@matter/main/clusters/valve-config
8
8
  import { SmokeCoAlarm } from '@matter/main/clusters/smoke-co-alarm';
9
9
  import { BooleanStateConfigurationServer } from '@matter/main/behaviors/boolean-state-configuration';
10
10
  import { OperationalState } from '@matter/main/clusters/operational-state';
11
+ import { ModeBase } from '@matter/main/clusters/mode-base';
12
+ import { RvcRunMode } from '@matter/main/clusters/rvc-run-mode';
13
+ import { RvcOperationalState } from '@matter/main/clusters/rvc-operational-state';
11
14
  import { IdentifyServer } from '@matter/main/behaviors/identify';
12
15
  import { OnOffServer } from '@matter/main/behaviors/on-off';
13
16
  import { LevelControlServer } from '@matter/main/behaviors/level-control';
@@ -21,6 +24,11 @@ import { ModeSelectServer } from '@matter/main/behaviors/mode-select';
21
24
  import { SmokeCoAlarmServer } from '@matter/main/behaviors/smoke-co-alarm';
22
25
  import { SwitchServer } from '@matter/main/behaviors/switch';
23
26
  import { OperationalStateServer } from '@matter/main/behaviors/operational-state';
27
+ import { RvcRunModeServer } from '@matter/main/behaviors/rvc-run-mode';
28
+ import { RvcCleanModeServer } from '@matter/main/behaviors/rvc-clean-mode';
29
+ import { RvcOperationalStateServer } from '@matter/main/behaviors/rvc-operational-state';
30
+ import { ServiceAreaServer } from '@matter/main/behaviors/service-area';
31
+ import { ServiceArea } from '@matter/main/clusters/service-area';
24
32
  export class MatterbridgeServerDevice {
25
33
  log;
26
34
  commandHandler;
@@ -158,6 +166,14 @@ export class MatterbridgeServerDevice {
158
166
  this.log.info(`Resume (endpoint ${this.endpointId}.${this.endpointNumber})`);
159
167
  this.commandHandler.executeHandler('resume', { request: {}, attributes: {}, endpoint: { number: this.endpointNumber, uniqueStorageKey: this.endpointId } });
160
168
  }
169
+ goHome() {
170
+ this.log.info(`GoHome (endpoint ${this.endpointId}.${this.endpointNumber})`);
171
+ this.commandHandler.executeHandler('goHome', { request: {}, attributes: {}, endpoint: { number: this.endpointNumber, uniqueStorageKey: this.endpointId } });
172
+ }
173
+ selectAreas({ newAreas }) {
174
+ this.log.info(`Selecting areas ${newAreas} (endpoint ${this.endpointId}.${this.endpointNumber})`);
175
+ this.commandHandler.executeHandler('selectAreas', { request: { newAreas }, attributes: {}, endpoint: { number: this.endpointNumber, uniqueStorageKey: this.endpointId } });
176
+ }
161
177
  }
162
178
  export class MatterbridgeServer extends Behavior {
163
179
  static id = 'matterbridge';
@@ -430,3 +446,95 @@ export class MatterbridgeOperationalStateServer extends OperationalStateServer {
430
446
  };
431
447
  }
432
448
  }
449
+ export class MatterbridgeRvcRunModeServer extends RvcRunModeServer {
450
+ changeToMode({ newMode }) {
451
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
452
+ const supported = this.state.supportedModes.find((mode) => mode.mode === newMode);
453
+ if (!supported) {
454
+ device.log.error(`MatterbridgeRvcRunModeServer changeToMode called with unsupported newMode: ${newMode}`);
455
+ return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: 'Unsupported mode' };
456
+ }
457
+ device.changeToMode({ newMode });
458
+ this.state.currentMode = newMode;
459
+ if (supported.modeTags.find((tag) => tag.value === RvcRunMode.ModeTag.Cleaning)) {
460
+ device.log.info('MatterbridgeRvcRunModeServer changeToMode called with newMode Cleaning => Running');
461
+ this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Running;
462
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Running' };
463
+ }
464
+ else if (supported.modeTags.find((tag) => tag.value === RvcRunMode.ModeTag.Idle)) {
465
+ device.log.info('MatterbridgeRvcRunModeServer changeToMode called with newMode Idle => Docked');
466
+ this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Docked;
467
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Docked' };
468
+ }
469
+ device.log.info(`MatterbridgeRvcRunModeServer changeToMode called with newMode ${newMode} => ${supported.label}`);
470
+ this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Running;
471
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
472
+ }
473
+ }
474
+ export class MatterbridgeRvcCleanModeServer extends RvcCleanModeServer {
475
+ changeToMode({ newMode }) {
476
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
477
+ const supported = this.state.supportedModes.find((mode) => mode.mode === newMode);
478
+ if (!supported) {
479
+ device.log.error(`MatterbridgeRvcCleanModeServer changeToMode called with unsupported newMode: ${newMode}`);
480
+ return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: 'Unsupported mode' };
481
+ }
482
+ device.changeToMode({ newMode });
483
+ this.state.currentMode = newMode;
484
+ device.log.info(`MatterbridgeRvcCleanModeServer changeToMode called with newMode ${newMode} => ${supported.label}`);
485
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
486
+ }
487
+ }
488
+ export class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateServer {
489
+ pause() {
490
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
491
+ device.log.info('MatterbridgeRvcOperationalStateServer: pause called setting operational state to Paused and currentMode to Idle');
492
+ device.pause();
493
+ this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 1;
494
+ this.state.operationalState = RvcOperationalState.OperationalState.Paused;
495
+ this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateLabel: 'No Error', errorStateDetails: 'Fully operational' };
496
+ return {
497
+ commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateLabel: 'No error', errorStateDetails: 'Fully operational' },
498
+ };
499
+ }
500
+ resume() {
501
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
502
+ device.log.info('MatterbridgeRvcOperationalStateServer: resume called setting operational state to Running and currentMode to Cleaning');
503
+ device.resume();
504
+ this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 2;
505
+ this.state.operationalState = RvcOperationalState.OperationalState.Running;
506
+ this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateLabel: 'No Error', errorStateDetails: 'Fully operational' };
507
+ return {
508
+ commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateLabel: 'No error', errorStateDetails: 'Fully operational' },
509
+ };
510
+ }
511
+ goHome() {
512
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
513
+ device.log.info('MatterbridgeRvcOperationalStateServer: goHome called setting operational state to Docked and currentMode to Idle');
514
+ device.goHome();
515
+ this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 1;
516
+ this.state.operationalState = RvcOperationalState.OperationalState.Docked;
517
+ this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateLabel: 'No Error', errorStateDetails: 'Fully operational' };
518
+ return {
519
+ commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateLabel: 'No error', errorStateDetails: 'Fully operational' },
520
+ };
521
+ }
522
+ }
523
+ export class MatterbridgeServiceAreaServer extends ServiceAreaServer {
524
+ selectAreas({ newAreas }) {
525
+ const device = this.endpoint.stateOf(MatterbridgeServer).deviceCommand;
526
+ for (const area of newAreas) {
527
+ const supportedArea = this.state.supportedAreas.find((supportedArea) => supportedArea.areaId === area);
528
+ if (!supportedArea) {
529
+ device.log.error(`MatterbridgeServiceAreaServer selectAreas called with unsupported area: ${area}`);
530
+ return { status: ServiceArea.SelectAreasStatus.UnsupportedArea, statusText: 'Unsupported areas' };
531
+ }
532
+ }
533
+ device.selectAreas({ newAreas });
534
+ this.state.selectedAreas = newAreas;
535
+ this.state.currentArea = newAreas[0];
536
+ device.log.info(`MatterbridgeServiceAreaServer selectAreas called with: ${newAreas.map((area) => area.toString()).join(', ')}`);
537
+ device.selectAreas({ newAreas });
538
+ return { status: ServiceArea.SelectAreasStatus.Success, statusText: 'Succesfully selected new areas' };
539
+ }
540
+ }
@@ -2,7 +2,7 @@ import { AnsiLogger, BLUE, CYAN, YELLOW, db, debugStringify, er, hk, or, zb } fr
2
2
  import { bridgedNode } from './matterbridgeDeviceTypes.js';
3
3
  import { isValidNumber, isValidObject, isValidString } from './utils/export.js';
4
4
  import { MatterbridgeServer, MatterbridgeServerDevice, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, } from './matterbridgeBehaviors.js';
5
- import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, capitalizeFirstLetter, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, } from './matterbridgeEndpointHelpers.js';
5
+ import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, capitalizeFirstLetter, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, } from './matterbridgeEndpointHelpers.js';
6
6
  import { Endpoint, Lifecycle, MutableEndpoint, NamedHandler, SupportedBehaviors, UINT16_MAX, UINT32_MAX, VendorId } from '@matter/main';
7
7
  import { getClusterNameById, MeasurementType } from '@matter/main/types';
8
8
  import { Descriptor } from '@matter/main/clusters/descriptor';
@@ -986,18 +986,7 @@ export class MatterbridgeEndpoint extends Endpoint {
986
986
  return true;
987
987
  }
988
988
  createDefaultOperationalStateClusterServer(operationalState = OperationalState.OperationalStateEnum.Stopped) {
989
- this.behaviors.require(MatterbridgeOperationalStateServer, {
990
- phaseList: [],
991
- currentPhase: null,
992
- operationalStateList: [
993
- { operationalStateId: OperationalState.OperationalStateEnum.Stopped, operationalStateLabel: 'Stopped' },
994
- { operationalStateId: OperationalState.OperationalStateEnum.Running, operationalStateLabel: 'Running' },
995
- { operationalStateId: OperationalState.OperationalStateEnum.Paused, operationalStateLabel: 'Paused' },
996
- { operationalStateId: OperationalState.OperationalStateEnum.Error, operationalStateLabel: 'Error' },
997
- ],
998
- operationalState,
999
- operationalError: { errorStateId: OperationalState.ErrorState.NoError, errorStateLabel: 'No error', errorStateDetails: 'Fully operational' },
1000
- });
989
+ this.behaviors.require(MatterbridgeOperationalStateServer, getDefaultOperationalStateClusterServer(operationalState));
1001
990
  return this;
1002
991
  }
1003
992
  createDefaultBooleanStateClusterServer(contact) {
@@ -1,7 +1,3 @@
1
- import { createHash } from 'node:crypto';
2
- import { BLUE, CYAN, db, debugStringify, er, hk, or, YELLOW, zb } from './logger/export.js';
3
- import { deepCopy, deepEqual, isValidArray } from './utils/export.js';
4
- import { MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeOperationalStateServer, } from './matterbridgeBehaviors.js';
5
1
  import { Lifecycle } from '@matter/main';
6
2
  import { getClusterNameById } from '@matter/main/types';
7
3
  import { PowerSource } from '@matter/main/clusters/power-source';
@@ -75,6 +71,10 @@ import { Pm25ConcentrationMeasurementServer } from '@matter/main/behaviors/pm25-
75
71
  import { Pm10ConcentrationMeasurementServer } from '@matter/main/behaviors/pm10-concentration-measurement';
76
72
  import { RadonConcentrationMeasurementServer } from '@matter/main/behaviors/radon-concentration-measurement';
77
73
  import { TotalVolatileOrganicCompoundsConcentrationMeasurementServer } from '@matter/main/behaviors/total-volatile-organic-compounds-concentration-measurement';
74
+ import { createHash } from 'node:crypto';
75
+ import { BLUE, CYAN, db, debugStringify, er, hk, or, YELLOW, zb } from 'node-ansi-logger';
76
+ import { deepCopy, deepEqual, isValidArray } from './utils/export.js';
77
+ import { MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeOperationalStateServer, } from './matterbridgeBehaviors.js';
78
78
  export function capitalizeFirstLetter(name) {
79
79
  if (!name)
80
80
  return name;
@@ -115,7 +115,7 @@ export function getBehaviourTypesFromClusterServerIds(clusterServerList) {
115
115
  }
116
116
  export function getBehaviourTypesFromClusterClientIds(clusterClientList) {
117
117
  const behaviorTypes = [];
118
- clusterClientList.forEach((clusterId) => {
118
+ clusterClientList.forEach((_clusterId) => {
119
119
  });
120
120
  return behaviorTypes;
121
121
  }
@@ -206,7 +206,7 @@ export function getBehaviourTypeFromClusterServerId(clusterId) {
206
206
  return TotalVolatileOrganicCompoundsConcentrationMeasurementServer.with('NumericMeasurement');
207
207
  return MatterbridgeIdentifyServer;
208
208
  }
209
- export function getBehaviourTypeFromClusterClientId(clusterId) {
209
+ export function getBehaviourTypeFromClusterClientId(_clusterId) {
210
210
  }
211
211
  export function getBehavior(endpoint, cluster) {
212
212
  let behavior;
@@ -518,6 +518,20 @@ export async function subscribeAttribute(endpoint, cluster, attribute, listener,
518
518
  log?.info(`${db}Subscribed endpoint ${or}${endpoint.id}${db}:${or}${endpoint.number}${db} attribute ${hk}${capitalizeFirstLetter(clusterName)}${db}.${hk}${attribute}${db}`);
519
519
  return true;
520
520
  }
521
+ export function getDefaultOperationalStateClusterServer(operationalState = OperationalState.OperationalStateEnum.Stopped) {
522
+ return optionsFor(MatterbridgeOperationalStateServer, {
523
+ phaseList: [],
524
+ currentPhase: null,
525
+ operationalStateList: [
526
+ { operationalStateId: OperationalState.OperationalStateEnum.Stopped, operationalStateLabel: 'Stopped' },
527
+ { operationalStateId: OperationalState.OperationalStateEnum.Running, operationalStateLabel: 'Running' },
528
+ { operationalStateId: OperationalState.OperationalStateEnum.Paused, operationalStateLabel: 'Paused' },
529
+ { operationalStateId: OperationalState.OperationalStateEnum.Error, operationalStateLabel: 'Error' },
530
+ ],
531
+ operationalState,
532
+ operationalError: { errorStateId: OperationalState.ErrorState.NoError, errorStateLabel: 'No error', errorStateDetails: 'Fully operational' },
533
+ });
534
+ }
521
535
  export function getDefaultTemperatureMeasurementClusterServer(measuredValue = null, minMeasuredValue = null, maxMeasuredValue = null) {
522
536
  return optionsFor(TemperatureMeasurementServer, {
523
537
  measuredValue,
@@ -0,0 +1,87 @@
1
+ import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
2
+ import { roboticVacuumCleaner } from './matterbridgeDeviceTypes.js';
3
+ import { MatterbridgeRvcCleanModeServer, MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer, MatterbridgeServiceAreaServer } from './matterbridgeBehaviors.js';
4
+ import { PowerSource, RvcRunMode, RvcCleanMode, RvcOperationalState } from '@matter/main/clusters';
5
+ export class RoboticVacuumCleaner extends MatterbridgeEndpoint {
6
+ constructor(name, serial) {
7
+ super(roboticVacuumCleaner, { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
8
+ this.createDefaultIdentifyClusterServer()
9
+ .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Robot Vacuum Cleaner')
10
+ .createDefaultPowerSourceRechargeableBatteryClusterServer(80, PowerSource.BatChargeLevel.Ok, 5900)
11
+ .createDefaultRvcRunModeClusterServer()
12
+ .createDefaultRvcCleanModeClusterServer()
13
+ .createDefaultRvcOperationalStateClusterServer()
14
+ .createDefaultServiceAreaClusterServer();
15
+ }
16
+ createDefaultRvcRunModeClusterServer(currentMode, supportedModes) {
17
+ this.behaviors.require(MatterbridgeRvcRunModeServer, {
18
+ supportedModes: supportedModes ?? [
19
+ { label: 'Idle', mode: 1, modeTags: [{ value: RvcRunMode.ModeTag.Idle }] },
20
+ { label: 'Cleaning', mode: 2, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }] },
21
+ { label: 'Mapping', mode: 3, modeTags: [{ value: RvcRunMode.ModeTag.Mapping }] },
22
+ { label: 'SpotCleaning', mode: 4, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }, { value: RvcRunMode.ModeTag.Max }] },
23
+ ],
24
+ currentMode: currentMode ?? 1,
25
+ });
26
+ return this;
27
+ }
28
+ createDefaultRvcCleanModeClusterServer(currentMode, supportedModes) {
29
+ this.behaviors.require(MatterbridgeRvcCleanModeServer, {
30
+ supportedModes: supportedModes ?? [
31
+ { label: 'Vacuum', mode: 1, modeTags: [{ value: RvcCleanMode.ModeTag.Vacuum }] },
32
+ { label: 'Mop', mode: 2, modeTags: [{ value: RvcCleanMode.ModeTag.Mop }] },
33
+ { label: 'Clean', mode: 3, modeTags: [{ value: RvcCleanMode.ModeTag.DeepClean }] },
34
+ ],
35
+ currentMode: currentMode ?? 1,
36
+ });
37
+ return this;
38
+ }
39
+ createDefaultServiceAreaClusterServer(supportedAreas, selectedAreas, currentArea) {
40
+ this.behaviors.require(MatterbridgeServiceAreaServer, {
41
+ supportedAreas: supportedAreas ?? [
42
+ {
43
+ areaId: 1,
44
+ mapId: null,
45
+ areaInfo: { locationInfo: { locationName: 'Living', floorNumber: null, areaType: null }, landmarkInfo: null },
46
+ },
47
+ {
48
+ areaId: 2,
49
+ mapId: null,
50
+ areaInfo: { locationInfo: { locationName: 'Kitchen', floorNumber: null, areaType: null }, landmarkInfo: null },
51
+ },
52
+ {
53
+ areaId: 3,
54
+ mapId: null,
55
+ areaInfo: { locationInfo: { locationName: 'Bedroom', floorNumber: null, areaType: null }, landmarkInfo: null },
56
+ },
57
+ {
58
+ areaId: 4,
59
+ mapId: null,
60
+ areaInfo: { locationInfo: { locationName: 'Bathroom', floorNumber: null, areaType: null }, landmarkInfo: null },
61
+ },
62
+ ],
63
+ selectedAreas: selectedAreas ?? [],
64
+ currentArea: currentArea ?? 1,
65
+ estimatedEndTime: null,
66
+ });
67
+ return this;
68
+ }
69
+ createDefaultRvcOperationalStateClusterServer(phaseList = null, currentPhase = null, operationalStateList, operationalState, operationalError) {
70
+ this.behaviors.require(MatterbridgeRvcOperationalStateServer, {
71
+ phaseList,
72
+ currentPhase,
73
+ operationalStateList: operationalStateList ?? [
74
+ { operationalStateId: RvcOperationalState.OperationalState.Stopped, operationalStateLabel: 'Stopped' },
75
+ { operationalStateId: RvcOperationalState.OperationalState.Running, operationalStateLabel: 'Running' },
76
+ { operationalStateId: RvcOperationalState.OperationalState.Paused, operationalStateLabel: 'Paused' },
77
+ { operationalStateId: RvcOperationalState.OperationalState.Error, operationalStateLabel: 'Error' },
78
+ { operationalStateId: RvcOperationalState.OperationalState.SeekingCharger, operationalStateLabel: 'SeekingCharger' },
79
+ { operationalStateId: RvcOperationalState.OperationalState.Charging, operationalStateLabel: 'Charging' },
80
+ { operationalStateId: RvcOperationalState.OperationalState.Docked, operationalStateLabel: 'Docked' },
81
+ ],
82
+ operationalState: operationalState ?? RvcOperationalState.OperationalState.Docked,
83
+ operationalError: operationalError ?? { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateLabel: 'No Error', errorStateDetails: 'Fully operational' },
84
+ });
85
+ return this;
86
+ }
87
+ }
package/dist/update.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { plg } from './matterbridgeTypes.js';
2
2
  import { db, nt, wr } from './logger/export.js';
3
3
  export async function checkUpdates(matterbridge) {
4
- const { hasParameter } = await import('./utils/parameter.js');
4
+ const { hasParameter } = await import('./utils/commandLine.js');
5
5
  getMatterbridgeLatestVersion(matterbridge);
6
6
  getMatterbridgeDevVersion(matterbridge);
7
7
  for (const plugin of matterbridge.plugins) {
@@ -8,11 +8,16 @@ export function deepCopy(value) {
8
8
  else if (value instanceof Date) {
9
9
  return new Date(value.getTime());
10
10
  }
11
+ else if (value instanceof RegExp) {
12
+ return new RegExp(value.source, value.flags);
13
+ }
11
14
  else if (value instanceof Map) {
12
15
  const mapCopy = new Map();
13
- value.forEach((val, key) => {
14
- mapCopy.set(key, deepCopy(val));
15
- });
16
+ for (const [origKey, origVal] of value.entries()) {
17
+ const clonedKey = deepCopy(origKey);
18
+ const clonedVal = deepCopy(origVal);
19
+ mapCopy.set(clonedKey, clonedVal);
20
+ }
16
21
  return mapCopy;
17
22
  }
18
23
  else if (value instanceof Set) {
@@ -1,9 +1,5 @@
1
1
  export function deepEqual(a, b, excludeProperties = []) {
2
- const debug = false;
3
2
  const debugLog = (...messages) => {
4
- if (debug) {
5
- console.log(...messages);
6
- }
7
3
  };
8
4
  if (a === b) {
9
5
  return true;
@@ -16,6 +12,12 @@ export function deepEqual(a, b, excludeProperties = []) {
16
12
  debugLog('deepEqual false for == null');
17
13
  return false;
18
14
  }
15
+ if (a instanceof Date && b instanceof Date) {
16
+ return a.getTime() === b.getTime();
17
+ }
18
+ if (a instanceof RegExp && b instanceof RegExp) {
19
+ return a.source === b.source && a.flags === b.flags;
20
+ }
19
21
  if (Array.isArray(a) && Array.isArray(b)) {
20
22
  if (a.length !== b.length) {
21
23
  debugLog(`deepEqual false for array a.length(${a.length}) !== b.length(${b.length})`);
@@ -1,5 +1,5 @@
1
1
  export * from './network.js';
2
- export * from './parameter.js';
2
+ export * from './commandLine.js';
3
3
  export * from './isvalid.js';
4
4
  export * from './colorUtils.js';
5
5
  export * from './deepCopy.js';
@@ -7,3 +7,4 @@ export * from './deepEqual.js';
7
7
  export * from './copyDirectory.js';
8
8
  export * from './createZip.js';
9
9
  export * from './wait.js';
10
+ export * from './hex.js';
@@ -0,0 +1,27 @@
1
+ export function bufferToHex(buffer) {
2
+ if (!(buffer instanceof ArrayBuffer || ArrayBuffer.isView(buffer))) {
3
+ throw new TypeError('Expected input to be an ArrayBuffer or ArrayBufferView');
4
+ }
5
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
6
+ return Array.from(bytes)
7
+ .map((byte) => byte.toString(16).padStart(2, '0'))
8
+ .join('');
9
+ }
10
+ export function hexToBuffer(hex) {
11
+ if (typeof hex !== 'string') {
12
+ throw new TypeError('Expected a string for hex input');
13
+ }
14
+ const cleaned = hex.trim();
15
+ if (cleaned.length % 2 !== 0) {
16
+ throw new Error('Invalid hex string length, must be even');
17
+ }
18
+ if (!/^[0-9a-fA-F]*$/.test(cleaned)) {
19
+ throw new Error('Invalid hex string, contains non-hex characters');
20
+ }
21
+ const length = cleaned.length / 2;
22
+ const result = new Uint8Array(length);
23
+ for (let i = 0; i < length; i++) {
24
+ result[i] = parseInt(cleaned.substr(i * 2, 2), 16);
25
+ }
26
+ return result;
27
+ }
@@ -23,6 +23,9 @@ export function isValidString(value, minLength, maxLength) {
23
23
  return false;
24
24
  return true;
25
25
  }
26
+ export function isValidRegExp(value) {
27
+ return value !== undefined && value !== null && value instanceof RegExp;
28
+ }
26
29
  export function isValidObject(value, minLength, maxLength) {
27
30
  if (value === undefined || value === null || typeof value !== 'object' || Array.isArray(value))
28
31
  return false;
@@ -44,7 +44,7 @@ export function getMacAddress() {
44
44
  break;
45
45
  }
46
46
  for (const detail of interfaceDetails) {
47
- if (detail.family === 'IPv6' && !detail.internal && macAddress === undefined) {
47
+ if (!detail.internal && macAddress === undefined) {
48
48
  macAddress = detail.mac;
49
49
  }
50
50
  }
@@ -83,7 +83,7 @@ export async function resolveHostname(hostname, family = 4) {
83
83
  }
84
84
  }
85
85
  export async function getNpmPackageVersion(packageName, tag = 'latest', timeout = 10000) {
86
- const https = await import('https');
86
+ const https = await import('node:https');
87
87
  return new Promise((resolve, reject) => {
88
88
  const url = `https://registry.npmjs.org/${packageName}`;
89
89
  const controller = new AbortController();
@@ -1,5 +1,5 @@
1
1
  import { AnsiLogger } from '../logger/export.js';
2
- const log = new AnsiLogger({ logName: 'MatterbridgeUtils', logTimestampFormat: 4, logLevel: "info" });
2
+ export const log = new AnsiLogger({ logName: 'MatterbridgeUtils', logTimestampFormat: 4, logLevel: "info" });
3
3
  export async function waiter(name, check, exitWithReject = false, resolveTimeout = 5000, resolveInterval = 500, debug = false) {
4
4
  if (check())
5
5
  return true;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.1-dev-20250507-a182295",
3
+ "version": "3.0.2-dev-20250508-7214e17",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.0.1-dev-20250507-a182295",
9
+ "version": "3.0.2-dev-20250508-7214e17",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.0.1-dev-20250507-a182295",
3
+ "version": "3.0.2-dev-20250508-7214e17",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",
@@ -1,3 +1,4 @@
1
+ import { isValidNumber } from './export.js';
1
2
  export function hasParameter(name) {
2
3
  const commandArguments = process.argv.slice(2);
3
4
  let markerIncluded = commandArguments.includes(`-${name}`);
@@ -5,7 +6,6 @@ export function hasParameter(name) {
5
6
  markerIncluded = commandArguments.includes(`--${name}`);
6
7
  return markerIncluded;
7
8
  }
8
- import { isValidNumber } from './export.js';
9
9
  export function getParameter(name) {
10
10
  const commandArguments = process.argv.slice(2);
11
11
  let markerIndex = commandArguments.indexOf(`-${name}`);