@matterbridge/core 3.10.7-dev-20260825-7ebac36 → 3.10.7-dev-20260827-957ddcc

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.
@@ -1,7 +1,7 @@
1
1
  import { BooleanStateConfigurationServer } from '@matter/node/behaviors/boolean-state-configuration';
2
2
  import { Status, StatusResponseError } from '@matter/types';
3
3
  import { BooleanStateConfiguration } from '@matter/types/clusters/boolean-state-configuration';
4
- import { debugStringify } from 'node-ansi-logger';
4
+ import { debugStringify, nf } from 'node-ansi-logger';
5
5
  import { MatterbridgeServer } from './matterbridgeServer.js';
6
6
  export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateConfigurationServer.with(BooleanStateConfiguration.Feature.Visual, BooleanStateConfiguration.Feature.Audible, BooleanStateConfiguration.Feature.AlarmSuppress, BooleanStateConfiguration.Feature.SensitivityLevel, BooleanStateConfiguration.Feature.FaultEvents) {
7
7
  initialize() {
@@ -44,7 +44,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
44
44
  }
45
45
  #assertAlarmModesSupported(alarms) {
46
46
  if ([Boolean(alarms.visual && !this.state.alarmsSupported.visual), Boolean(alarms.audible && !this.state.alarmsSupported.audible)].some(Boolean)) {
47
- throw new StatusResponseError('Requested alarm mode is not supported', Status.ConstraintError);
47
+ throw new StatusResponseError(`MatterbridgeBooleanStateConfigurationServer: requested alarm mode is not supported (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
48
48
  }
49
49
  }
50
50
  #assertSuppressAlarmAllowed(alarmsToSuppress) {
@@ -52,12 +52,12 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
52
52
  Boolean(alarmsToSuppress.visual && (!this.state.alarmsActive.visual || !this.state.alarmsEnabled?.visual)),
53
53
  Boolean(alarmsToSuppress.audible && (!this.state.alarmsActive.audible || !this.state.alarmsEnabled?.audible)),
54
54
  ].some(Boolean)) {
55
- throw new StatusResponseError('Requested alarm mode is not active', Status.InvalidInState);
55
+ throw new StatusResponseError(`MatterbridgeBooleanStateConfigurationServer: requested alarm mode is not active (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
56
56
  }
57
57
  }
58
58
  async suppressAlarm(request) {
59
59
  const device = this.endpoint.stateOf(MatterbridgeServer);
60
- device.log.info(`Suppressing alarm ${debugStringify(request.alarmsToSuppress)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
60
+ device.log.info(`MatterbridgeBooleanStateConfigurationServer: suppressing alarm ${debugStringify(request.alarmsToSuppress)}${nf} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
61
61
  await device.commandHandler.executeHandler('BooleanStateConfiguration.suppressAlarm', {
62
62
  command: 'suppressAlarm',
63
63
  request,
@@ -69,11 +69,11 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
69
69
  this.#assertAlarmModesSupported(request.alarmsToSuppress);
70
70
  this.#assertSuppressAlarmAllowed(request.alarmsToSuppress);
71
71
  this.state.alarmsSuppressed = this.#mergeAlarmsSuppressed(request.alarmsToSuppress);
72
- device.log.debug(`MatterbridgeBooleanStateConfigurationServer: suppressAlarm called`);
72
+ device.log.debug(`MatterbridgeBooleanStateConfigurationServer: suppressAlarm called (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
73
73
  }
74
74
  async enableDisableAlarm(request) {
75
75
  const device = this.endpoint.stateOf(MatterbridgeServer);
76
- device.log.info(`Enabling/disabling alarm ${debugStringify(request.alarmsToEnableDisable)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
76
+ device.log.info(`MatterbridgeBooleanStateConfigurationServer: enabling/disabling alarm ${debugStringify(request.alarmsToEnableDisable)}${nf} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
77
77
  await device.commandHandler.executeHandler('BooleanStateConfiguration.enableDisableAlarm', {
78
78
  command: 'enableDisableAlarm',
79
79
  request,
@@ -84,6 +84,6 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
84
84
  });
85
85
  this.#assertAlarmModesSupported(request.alarmsToEnableDisable);
86
86
  this.#applyAlarmsEnabled(request.alarmsToEnableDisable);
87
- device.log.debug(`MatterbridgeBooleanStateConfigurationServer: enableDisableAlarm called`);
87
+ device.log.debug(`MatterbridgeBooleanStateConfigurationServer: enableDisableAlarm called (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
88
88
  }
89
89
  }
package/dist/chipTests.js CHANGED
@@ -11,9 +11,14 @@ import { DeviceEnergyManagement } from '@matter/types/clusters/device-energy-man
11
11
  import { ElectricalEnergyMeasurement } from '@matter/types/clusters/electrical-energy-measurement';
12
12
  import { ElectricalPowerMeasurement } from '@matter/types/clusters/electrical-power-measurement';
13
13
  import { OperationalState } from '@matter/types/clusters/operational-state';
14
+ import { RefrigeratorAlarm } from '@matter/types/clusters/refrigerator-alarm';
15
+ import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
16
+ import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
14
17
  import { SmokeCoAlarm } from '@matter/types/clusters/smoke-co-alarm';
15
18
  import { MatterbridgeOccupancySensingServer } from './behaviors/occupancySensingServer.js';
16
19
  import { cliEmitter } from './cliEmitter.js';
20
+ import { MatterbridgeRefrigeratorAlarmServer } from './devices/refrigerator.js';
21
+ import { MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer } from './devices/roboticVacuumCleaner.js';
17
22
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
18
23
  const chipTestAppPipePath = '/tmp/matterbridge-chip-test-app-pipe';
19
24
  const chipTestValidEventTrigger = 0xfffffffffff10000n;
@@ -564,6 +569,73 @@ async function handleChipTestAppPipeCommand(matterbridge, command) {
564
569
  return;
565
570
  }
566
571
  switch (command.Name) {
572
+ case 'Reset': {
573
+ if (!endpoint.behaviors.has(MatterbridgeRvcRunModeServer) || !endpoint.behaviors.has(MatterbridgeRvcOperationalStateServer)) {
574
+ matterbridge.log.warn(`Ignoring RVC Reset CHIP test app pipe command on non-RVC endpoint ${endpointId}`);
575
+ return;
576
+ }
577
+ const runModeState = endpoint.stateOf(MatterbridgeRvcRunModeServer);
578
+ const idleMode = runModeState.supportedModes.find((mode) => mode.modeTags.some((tag) => tag.value === RvcRunMode.ModeTag.Idle));
579
+ if (!idleMode) {
580
+ matterbridge.log.warn(`Ignoring RVC Reset CHIP test app pipe command because endpoint ${endpointId} has no Idle run mode`);
581
+ return;
582
+ }
583
+ await endpoint.setStateOf(MatterbridgeRvcRunModeServer, { currentMode: idleMode.mode });
584
+ await endpoint.setStateOf(MatterbridgeRvcOperationalStateServer, {
585
+ operationalState: RvcOperationalState.OperationalState.Stopped,
586
+ operationalError: { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' },
587
+ });
588
+ matterbridge.log.info(`CHIP test app pipe reset RVC endpoint ${endpointId}`);
589
+ return;
590
+ }
591
+ case 'ErrorEvent': {
592
+ const errorStates = {
593
+ UnableToStartOrResume: RvcOperationalState.ErrorState.UnableToStartOrResume,
594
+ UnableToCompleteOperation: RvcOperationalState.ErrorState.UnableToCompleteOperation,
595
+ CommandInvalidInState: RvcOperationalState.ErrorState.CommandInvalidInState,
596
+ FailedToFindChargingDock: RvcOperationalState.ErrorState.FailedToFindChargingDock,
597
+ Stuck: RvcOperationalState.ErrorState.Stuck,
598
+ DustBinMissing: RvcOperationalState.ErrorState.DustBinMissing,
599
+ DustBinFull: RvcOperationalState.ErrorState.DustBinFull,
600
+ WaterTankEmpty: RvcOperationalState.ErrorState.WaterTankEmpty,
601
+ WaterTankMissing: RvcOperationalState.ErrorState.WaterTankMissing,
602
+ WaterTankLidOpen: RvcOperationalState.ErrorState.WaterTankLidOpen,
603
+ MopCleaningPadMissing: RvcOperationalState.ErrorState.MopCleaningPadMissing,
604
+ BatteryLow: RvcOperationalState.ErrorState.LowBattery,
605
+ CannotReachTargetArea: RvcOperationalState.ErrorState.CannotReachTargetArea,
606
+ DirtyWaterTankFull: RvcOperationalState.ErrorState.DirtyWaterTankFull,
607
+ DirtyWaterTankMissing: RvcOperationalState.ErrorState.DirtyWaterTankMissing,
608
+ WheelsJammed: RvcOperationalState.ErrorState.WheelsJammed,
609
+ BrushJammed: RvcOperationalState.ErrorState.BrushJammed,
610
+ NavigationSensorObscured: RvcOperationalState.ErrorState.NavigationSensorObscured,
611
+ };
612
+ const errorStateId = command.Error === undefined ? undefined : errorStates[command.Error];
613
+ if (errorStateId === undefined) {
614
+ matterbridge.log.warn(`Ignoring unsupported RVC ErrorEvent CHIP test app pipe command: ${JSON.stringify(command)}`);
615
+ return;
616
+ }
617
+ await endpoint.setStateOf(MatterbridgeRvcOperationalStateServer, {
618
+ operationalState: RvcOperationalState.OperationalState.Error,
619
+ operationalError: { errorStateId, errorStateDetails: 'Simulated CHIP test fault' },
620
+ });
621
+ matterbridge.log.info(`CHIP test app pipe set RVC OperationalError to ${command.Error} on endpoint ${endpointId}`);
622
+ return;
623
+ }
624
+ case 'ChargerFound':
625
+ case 'Charging':
626
+ await endpoint.setStateOf(MatterbridgeRvcOperationalStateServer, { operationalState: RvcOperationalState.OperationalState.Charging });
627
+ matterbridge.log.info(`CHIP test app pipe set RVC OperationalState to Charging on endpoint ${endpointId}`);
628
+ return;
629
+ case 'Charged':
630
+ case 'Docked': {
631
+ const runModeState = endpoint.stateOf(MatterbridgeRvcRunModeServer);
632
+ const idleMode = runModeState.supportedModes.find((mode) => mode.modeTags.some((tag) => tag.value === RvcRunMode.ModeTag.Idle));
633
+ if (idleMode)
634
+ await endpoint.setStateOf(MatterbridgeRvcRunModeServer, { currentMode: idleMode.mode });
635
+ await endpoint.setStateOf(MatterbridgeRvcOperationalStateServer, { operationalState: RvcOperationalState.OperationalState.Docked });
636
+ matterbridge.log.info(`CHIP test app pipe set RVC OperationalState to Docked on endpoint ${endpointId}`);
637
+ return;
638
+ }
567
639
  case 'SetBooleanState':
568
640
  await endpoint.setStateOf(endpoint.behaviors.supported.booleanState, { stateValue: Boolean(command.NewState) });
569
641
  matterbridge.log.info(`CHIP test app pipe set BooleanState.StateValue to ${Boolean(command.NewState)} on endpoint ${endpointId}`);
@@ -580,6 +652,26 @@ async function handleChipTestAppPipeCommand(matterbridge, command) {
580
652
  await endpoint.setStateOf(endpoint.behaviors.supported.soilMeasurement, { soilMoistureMeasuredValue: command.SoilMoistureValue ?? null });
581
653
  matterbridge.log.info(`CHIP test app pipe set SoilMeasurement.SoilMoistureMeasuredValue to ${command.SoilMoistureValue ?? null} on endpoint ${endpointId}`);
582
654
  return;
655
+ case 'SetRefrigeratorDoorStatus': {
656
+ const doorOpen = Boolean(command.DoorOpen);
657
+ const wasDoorOpen = Boolean(endpoint.stateOf(MatterbridgeRefrigeratorAlarmServer).state.doorOpen);
658
+ await endpoint.setCluster(RefrigeratorAlarm, { state: { doorOpen } }, matterbridge.log);
659
+ await endpoint.triggerEvent('RefrigeratorAlarm', 'notify', {
660
+ active: { doorOpen: doorOpen && !wasDoorOpen },
661
+ inactive: { doorOpen: !doorOpen && wasDoorOpen },
662
+ state: { doorOpen },
663
+ mask: endpoint.stateOf(MatterbridgeRefrigeratorAlarmServer).mask,
664
+ }, matterbridge.log);
665
+ matterbridge.log.info(`CHIP test app pipe set RefrigeratorAlarm.State.DoorOpen to ${doorOpen} on endpoint ${endpointId}`);
666
+ return;
667
+ }
668
+ case 'ModeChange':
669
+ if (command.Device === 'DishWasher' && command.Type === 'ToggleFailTransition') {
670
+ matterbridge.log.info(`CHIP test app pipe prepared DishwasherMode for a successful transition on endpoint ${endpointId}`);
671
+ return;
672
+ }
673
+ matterbridge.log.warn(`Ignoring unsupported ModeChange CHIP test app pipe command: ${JSON.stringify(command)}`);
674
+ return;
583
675
  case 'OperationalStateChange':
584
676
  switch (command.Operation) {
585
677
  case 'Stop':
@@ -1,8 +1,14 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import { ClosureTag, ClosureWindowTag, CommonNumberTag, CommonPositionTag, RefrigeratorTag } from '@matter/node';
2
4
  import { AirQuality } from '@matter/types/clusters/air-quality';
3
5
  import { FanControl } from '@matter/types/clusters/fan-control';
4
6
  import { PowerSource } from '@matter/types/clusters/power-source';
7
+ import { ResourceMonitoring } from '@matter/types/clusters/resource-monitoring';
8
+ import { RvcCleanMode } from '@matter/types/clusters/rvc-clean-mode';
9
+ import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
5
10
  import { EndpointNumber } from '@matter/types/datatype';
11
+ import { getErrorMessage } from '@matterbridge/utils/error';
6
12
  import { AirConditioner } from './devices/airConditioner.js';
7
13
  import { BasicVideoPlayer } from './devices/basicVideoPlayer.js';
8
14
  import { CastingVideoClient } from './devices/castingVideoClient.js';
@@ -24,32 +30,138 @@ import { VideoRemoteControl } from './devices/videoRemoteControl.js';
24
30
  import { getSupportedDeviceType } from './matterbridgeDeviceTypes.js';
25
31
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
26
32
  import { getSemtag } from './matterbridgeEndpointHelpers.js';
33
+ const demoPluginName = 'matterbridge-demo-devices';
34
+ const demoPluginType = 'DynamicPlatform';
35
+ const demoPluginVersion = '1.0.0';
36
+ const demoPluginSchema = {
37
+ title: 'Matterbridge Demo Devices',
38
+ description: `${demoPluginName} v. ${demoPluginVersion} by Matterbridge`,
39
+ type: 'object',
40
+ properties: {
41
+ name: {
42
+ 'description': 'Plugin name',
43
+ 'type': 'string',
44
+ 'readOnly': true,
45
+ 'ui:widget': 'hidden',
46
+ },
47
+ type: {
48
+ 'description': 'Plugin type',
49
+ 'type': 'string',
50
+ 'readOnly': true,
51
+ 'ui:widget': 'hidden',
52
+ },
53
+ version: {
54
+ 'description': 'Plugin version',
55
+ 'type': 'string',
56
+ 'readOnly': true,
57
+ 'default': demoPluginVersion,
58
+ 'ui:widget': 'hidden',
59
+ },
60
+ whiteList: {
61
+ description: 'Only the devices in the list will be exposed. If the list is empty, all devices will be exposed.',
62
+ type: 'array',
63
+ items: { type: 'string' },
64
+ default: [],
65
+ uniqueItems: true,
66
+ selectFrom: 'name',
67
+ },
68
+ blackList: {
69
+ description: 'The devices in the list will not be exposed. If the list is empty, no devices will be excluded.',
70
+ type: 'array',
71
+ items: { type: 'string' },
72
+ default: [],
73
+ uniqueItems: true,
74
+ selectFrom: 'name',
75
+ },
76
+ debug: {
77
+ description: 'Enable debug logging for the plugin.',
78
+ type: 'boolean',
79
+ default: false,
80
+ },
81
+ unregisterOnShutdown: {
82
+ description: 'Unregister all devices when the plugin is stopped.',
83
+ type: 'boolean',
84
+ default: false,
85
+ },
86
+ },
87
+ };
27
88
  export async function createDemoDevices(matterbridge) {
28
- if (matterbridge.bridgeMode !== 'bridge' || !matterbridge.serverNode || !matterbridge.aggregatorNode)
89
+ if (matterbridge.bridgeMode !== 'bridge') {
90
+ matterbridge.log.error('Demo devices can only be created in bridge mode');
29
91
  return;
92
+ }
30
93
  const serverNode = matterbridge.serverNode;
31
94
  const aggregator = matterbridge.aggregatorNode;
32
95
  if (!serverNode || !aggregator) {
33
96
  matterbridge.log.error('Demo devices can only be created when the server node and aggregator node are available');
34
97
  return;
35
98
  }
99
+ const configFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.config.json`);
100
+ const schemaFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.schema.json`);
101
+ const defaultConfig = {
102
+ name: demoPluginName,
103
+ type: demoPluginType,
104
+ version: demoPluginVersion,
105
+ debug: false,
106
+ unregisterOnShutdown: false,
107
+ whiteList: [],
108
+ blackList: [],
109
+ };
110
+ let config = defaultConfig;
111
+ try {
112
+ const storedConfig = JSON.parse(await readFile(configFile, 'utf8'));
113
+ config = {
114
+ ...defaultConfig,
115
+ ...storedConfig,
116
+ name: demoPluginName,
117
+ type: demoPluginType,
118
+ version: demoPluginVersion,
119
+ whiteList: Array.isArray(storedConfig.whiteList) ? storedConfig.whiteList : [],
120
+ blackList: Array.isArray(storedConfig.blackList) ? storedConfig.blackList : [],
121
+ };
122
+ }
123
+ catch (error) {
124
+ if (error.code !== 'ENOENT')
125
+ matterbridge.log.error(`Failed to read demo devices config ${configFile}: ${getErrorMessage(error)}`);
126
+ }
127
+ try {
128
+ await writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
129
+ await writeFile(schemaFile, JSON.stringify(demoPluginSchema, null, 2), 'utf8');
130
+ }
131
+ catch (error) {
132
+ matterbridge.log.error(`Failed to write demo devices config or schema: ${getErrorMessage(error)}`);
133
+ }
36
134
  let ep;
37
135
  matterbridge.plugins.set({
38
- name: 'matterbridge-demo-devices',
136
+ name: demoPluginName,
39
137
  path: '',
40
- type: 'DynamicPlatform',
41
- version: '1.0.0',
138
+ type: demoPluginType,
139
+ version: demoPluginVersion,
42
140
  description: 'Matterbridge demo devices',
43
141
  author: 'Matterbridge',
44
142
  enabled: false,
45
143
  private: true,
46
144
  registeredDevices: 0,
145
+ configJson: config,
146
+ schemaJson: demoPluginSchema,
147
+ hasWhiteList: true,
148
+ hasBlackList: true,
47
149
  });
48
150
  const registerDevice = async (device, deviceName, serialNumber) => {
151
+ const whiteList = config.whiteList;
152
+ const blackList = config.blackList;
153
+ if (blackList.includes(deviceName)) {
154
+ matterbridge.log.info(`Skipping demo device ${deviceName} because it is in the blacklist`);
155
+ return;
156
+ }
157
+ if (whiteList.length > 0 && !whiteList.includes(deviceName)) {
158
+ matterbridge.log.info(`Skipping demo device ${deviceName} because it is not in the whitelist`);
159
+ return;
160
+ }
49
161
  device.createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber);
50
162
  device.addRequiredClusters();
51
- device.plugin = 'matterbridge-demo-devices';
52
- await matterbridge.addBridgedEndpoint('matterbridge-demo-devices', device);
163
+ device.plugin = demoPluginName;
164
+ await matterbridge.addBridgedEndpoint(demoPluginName, device);
53
165
  };
54
166
  const bridgedNode = getSupportedDeviceType('BridgedNode');
55
167
  const powerSource = getSupportedDeviceType('PowerSource');
@@ -353,14 +465,37 @@ export async function createDemoDevices(matterbridge) {
353
465
  id: 'RoboticVacuumCleaner',
354
466
  number: EndpointNumber(12_01),
355
467
  tagList: [getSemtag(CommonNumberTag.One)],
468
+ currentRunMode: 0,
469
+ supportedRunModes: [
470
+ { label: 'Idle', mode: 0, modeTags: [{ value: RvcRunMode.ModeTag.Idle }] },
471
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }] },
472
+ { label: 'Mapping', mode: 2, modeTags: [{ value: RvcRunMode.ModeTag.Mapping }] },
473
+ { label: 'SpotCleaning', mode: 3, modeTags: [{ value: RvcRunMode.ModeTag.Cleaning }, { value: RvcRunMode.ModeTag.Max }] },
474
+ ],
475
+ currentCleanMode: 1,
476
+ supportedCleanModes: [
477
+ { label: 'Vacuum', mode: 1, modeTags: [{ value: RvcCleanMode.ModeTag.Vacuum }] },
478
+ { label: 'Mop', mode: 2, modeTags: [{ value: RvcCleanMode.ModeTag.Mop }] },
479
+ { label: 'DeepClean', mode: 3, modeTags: [{ value: RvcCleanMode.ModeTag.DeepClean }] },
480
+ ],
356
481
  });
357
482
  await registerDevice(ep, 'Robotic Vacuum Cleaner', 'ROBOTIC-12-01');
358
- ep = new LaundryWasher('Laundry Washer', 'APPLIANCE-13-01', {
483
+ ep = new LaundryWasher('Laundry Washer Level Temperature', 'APPLIANCE-13-01', {
359
484
  id: 'LaundryWasher',
360
485
  number: EndpointNumber(13_01),
361
486
  tagList: [getSemtag(CommonNumberTag.One)],
362
487
  });
363
488
  await registerDevice(ep, 'Laundry Washer', 'APPLIANCE-13-01');
489
+ ep = new LaundryWasher('Laundry Washer Number Temperature', 'APPLIANCE-13-01-2', {
490
+ id: 'LaundryWasherNumberTemperature',
491
+ number: EndpointNumber(13_01_2),
492
+ tagList: [getSemtag(CommonNumberTag.Two)],
493
+ temperatureSetpoint: 40 * 100,
494
+ minTemperature: 30 * 100,
495
+ maxTemperature: 60 * 100,
496
+ step: 10 * 100,
497
+ });
498
+ await registerDevice(ep, 'Laundry Washer Number Temperature', 'APPLIANCE-13-01-2');
364
499
  const refrigeratorDevice = new Refrigerator('Refrigerator', 'APPLIANCE-13-02', {
365
500
  id: 'Refrigerator',
366
501
  number: EndpointNumber(13_02),
@@ -461,6 +596,24 @@ export async function createDemoDevices(matterbridge) {
461
596
  id: 'ExtractorHood',
462
597
  number: EndpointNumber(13_10),
463
598
  tagList: [getSemtag(CommonNumberTag.One)],
599
+ hepaCondition: 30,
600
+ hepaChangeIndication: ResourceMonitoring.ChangeIndication.Warning,
601
+ hepaLastChangedTime: 1_735_689_600,
602
+ hepaReplacementProductList: [
603
+ {
604
+ productIdentifierType: ResourceMonitoring.ProductIdentifierType.Upc,
605
+ productIdentifierValue: '012345678905',
606
+ },
607
+ ],
608
+ activatedCarbonCondition: 30,
609
+ activatedCarbonChangeIndication: ResourceMonitoring.ChangeIndication.Warning,
610
+ activatedCarbonLastChangedTime: 1_735_689_600,
611
+ activatedCarbonReplacementProductList: [
612
+ {
613
+ productIdentifierType: ResourceMonitoring.ProductIdentifierType.Ean,
614
+ productIdentifierValue: '4006381333931',
615
+ },
616
+ ],
464
617
  });
465
618
  await registerDevice(ep, 'Extractor Hood', 'APPLIANCE-13-10');
466
619
  ep = new MicrowaveOven('Microwave Oven', 'APPLIANCE-13-11', {
@@ -1,3 +1,4 @@
1
+ import { DishwasherAlarmServer } from '@matter/node/behaviors/dishwasher-alarm';
1
2
  import { DishwasherModeServer } from '@matter/node/behaviors/dishwasher-mode';
2
3
  import type { EndpointNumber } from '@matter/types';
3
4
  import { DishwasherMode } from '@matter/types/clusters/dishwasher-mode';
@@ -31,3 +32,6 @@ export declare class MatterbridgeDishwasherModeServer extends DishwasherModeServ
31
32
  protected handleOnOffChange(onOff: boolean): void;
32
33
  changeToMode(request: ModeBase.ChangeToModeRequest): Promise<ModeBase.ChangeToModeResponse>;
33
34
  }
35
+ export declare class MatterbridgeDishwasherAlarmServer extends DishwasherAlarmServer {
36
+ static readonly schema: import("@matter/model").ClusterModel;
37
+ }
@@ -1,3 +1,4 @@
1
+ import { AttributeElement, EventElement, FieldElement } from '@matter/model';
1
2
  import { DishwasherAlarmServer } from '@matter/node/behaviors/dishwasher-alarm';
2
3
  import { DishwasherModeServer } from '@matter/node/behaviors/dishwasher-mode';
3
4
  import { DishwasherMode } from '@matter/types/clusters/dishwasher-mode';
@@ -53,10 +54,10 @@ export class Dishwasher extends MatterbridgeEndpoint {
53
54
  return this;
54
55
  }
55
56
  createDefaultDishwasherAlarmClusterServer() {
56
- this.behaviors.require(DishwasherAlarmServer, {
57
- mask: { inflowError: true, drainError: true, doorError: true, tempTooLow: true, tempTooHigh: true, waterLevelError: true },
57
+ this.behaviors.require(MatterbridgeDishwasherAlarmServer, {
58
+ mask: { inflowError: false, drainError: false, doorError: true, tempTooLow: false, tempTooHigh: false, waterLevelError: false },
58
59
  state: { inflowError: false, drainError: false, doorError: false, tempTooLow: false, tempTooHigh: false, waterLevelError: false },
59
- supported: { inflowError: true, drainError: true, doorError: true, tempTooLow: true, tempTooHigh: true, waterLevelError: true },
60
+ supported: { inflowError: false, drainError: false, doorError: true, tempTooLow: false, tempTooHigh: false, waterLevelError: false },
60
61
  });
61
62
  return this;
62
63
  }
@@ -92,8 +93,12 @@ export class MatterbridgeDishwasherModeServer extends DishwasherModeServer {
92
93
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
93
94
  }
94
95
  else {
95
- device.log.error(`DishwasherModeServer: changeToMode called with invalid mode ${request.newMode}`);
96
- return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Invalid mode' };
96
+ device.log.error(`DishwasherModeServer: changeToMode called with unsupported mode ${request.newMode}`);
97
+ return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: '' };
97
98
  }
98
99
  }
99
100
  }
101
+ const MatterbridgeDishwasherAlarmSchema = DishwasherAlarmServer.schema.extend({}, AttributeElement({ name: 'Mask', id: 0x0000, type: 'AlarmBitmap', access: 'R V', conformance: 'M' }), AttributeElement({ name: 'State', id: 0x0002, type: 'AlarmBitmap', access: 'R V', conformance: 'M' }), AttributeElement({ name: 'Supported', id: 0x0003, type: 'AlarmBitmap', access: 'R V', conformance: 'M', quality: 'F' }), EventElement({ name: 'Notify', id: 0x0000, access: 'V', conformance: 'M', priority: 'info' }, FieldElement({ name: 'Active', id: 0x0000, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'Inactive', id: 0x0001, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'State', id: 0x0002, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'Mask', id: 0x0003, type: 'AlarmBitmap', conformance: 'M' })));
102
+ export class MatterbridgeDishwasherAlarmServer extends DishwasherAlarmServer {
103
+ static schema = MatterbridgeDishwasherAlarmSchema;
104
+ }
@@ -1,5 +1,6 @@
1
+ import { LaundryWasherControlsServer } from '@matter/node/behaviors/laundry-washer-controls';
1
2
  import { LaundryWasherModeServer } from '@matter/node/behaviors/laundry-washer-mode';
2
- import type { EndpointNumber } from '@matter/types';
3
+ import { type EndpointNumber } from '@matter/types';
3
4
  import { LaundryWasherControls } from '@matter/types/clusters/laundry-washer-controls';
4
5
  import { LaundryWasherMode } from '@matter/types/clusters/laundry-washer-mode';
5
6
  import { ModeBase } from '@matter/types/clusters/mode-base';
@@ -31,8 +32,17 @@ export declare class LaundryWasher extends MatterbridgeEndpoint {
31
32
  createDefaultLaundryWasherModeClusterServer(currentMode?: number, supportedModes?: LaundryWasherMode.ModeOption[]): this;
32
33
  createDefaultLaundryWasherControlsClusterServer(spinSpeedCurrent?: number, spinSpeeds?: string[], numberOfRinses?: LaundryWasherControls.NumberOfRinses, supportedRinses?: LaundryWasherControls.NumberOfRinses[]): this;
33
34
  }
35
+ declare const MatterbridgeLaundryWasherControlsServer_base: import("@matter/node").ClusterBehavior.Type<typeof LaundryWasherControlsServer, import("@matter/types").ClusterType.WithSupportedFeatures<LaundryWasherControls, {
36
+ rinse: true;
37
+ spin: true;
38
+ }>, import("@matter/types").ClusterType.Concrete, new () => {}, "laundryWasherControls">;
39
+ export declare class MatterbridgeLaundryWasherControlsServer extends MatterbridgeLaundryWasherControlsServer_base {
40
+ #private;
41
+ initialize(): void;
42
+ }
34
43
  export declare class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer {
35
44
  initialize(): void;
36
45
  protected handleOnOffChange(onOff: boolean): void;
37
46
  changeToMode(request: ModeBase.ChangeToModeRequest): Promise<ModeBase.ChangeToModeResponse>;
38
47
  }
48
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { LaundryWasherControlsServer } from '@matter/node/behaviors/laundry-washer-controls';
2
2
  import { LaundryWasherModeServer } from '@matter/node/behaviors/laundry-washer-mode';
3
+ import { Status, StatusResponseError } from '@matter/types';
3
4
  import { LaundryWasherControls } from '@matter/types/clusters/laundry-washer-controls';
4
5
  import { LaundryWasherMode } from '@matter/types/clusters/laundry-washer-mode';
5
6
  import { ModeBase } from '@matter/types/clusters/mode-base';
@@ -64,7 +65,7 @@ export class LaundryWasher extends MatterbridgeEndpoint {
64
65
  LaundryWasherControls.NumberOfRinses.Max,
65
66
  LaundryWasherControls.NumberOfRinses.Extra,
66
67
  ]) {
67
- this.behaviors.require(LaundryWasherControlsServer.with(LaundryWasherControls.Feature.Spin, LaundryWasherControls.Feature.Rinse), {
68
+ this.behaviors.require(MatterbridgeLaundryWasherControlsServer, {
68
69
  spinSpeeds,
69
70
  spinSpeedCurrent,
70
71
  supportedRinses,
@@ -73,6 +74,16 @@ export class LaundryWasher extends MatterbridgeEndpoint {
73
74
  return this;
74
75
  }
75
76
  }
77
+ export class MatterbridgeLaundryWasherControlsServer extends LaundryWasherControlsServer.with(LaundryWasherControls.Feature.Spin, LaundryWasherControls.Feature.Rinse) {
78
+ initialize() {
79
+ this.reactTo(this.events.spinSpeedCurrent$Changing, this.#validateSpinSpeedCurrent);
80
+ }
81
+ #validateSpinSpeedCurrent(spinSpeedCurrent) {
82
+ if (spinSpeedCurrent !== null && spinSpeedCurrent >= this.state.spinSpeeds.length) {
83
+ throw new StatusResponseError(`SpinSpeedCurrent ${spinSpeedCurrent} is not a valid SpinSpeeds index`, Status.ConstraintError);
84
+ }
85
+ }
86
+ }
76
87
  export class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer {
77
88
  initialize() {
78
89
  const device = this.endpoint.stateOf(MatterbridgeServer);
@@ -98,14 +109,12 @@ export class MatterbridgeLaundryWasherModeServer extends LaundryWasherModeServer
98
109
  endpoint: this.endpoint,
99
110
  });
100
111
  const supportedMode = this.state.supportedModes.find((supportedMode) => supportedMode.mode === request.newMode);
101
- if (supportedMode) {
102
- device.log.debug(`MatterbridgeLaundryWasherModeServer: changeToMode called with mode ${supportedMode.mode} => ${supportedMode.label}`);
103
- this.state.currentMode = request.newMode;
104
- return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
105
- }
106
- else {
107
- device.log.error(`MatterbridgeLaundryWasherModeServer: changeToMode called with invalid mode ${request.newMode}`);
108
- return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Invalid mode' };
112
+ if (!supportedMode) {
113
+ device.log.error(`MatterbridgeLaundryWasherModeServer: changeToMode called with unsupported mode ${request.newMode}`);
114
+ return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: '' };
109
115
  }
116
+ device.log.debug(`MatterbridgeLaundryWasherModeServer: changeToMode called with mode ${supportedMode.mode} => ${supportedMode.label}`);
117
+ this.state.currentMode = request.newMode;
118
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
110
119
  }
111
120
  }
@@ -1,5 +1,5 @@
1
1
  import { MicrowaveOvenControlServer } from '@matter/node/behaviors/microwave-oven-control';
2
- import type { EndpointNumber } from '@matter/types';
2
+ import { type EndpointNumber } from '@matter/types';
3
3
  import { MicrowaveOvenControl } from '@matter/types/clusters/microwave-oven-control';
4
4
  import { MicrowaveOvenMode } from '@matter/types/clusters/microwave-oven-mode';
5
5
  import type { Semtag } from '@matter/types/globals';
@@ -1,5 +1,6 @@
1
1
  import { MicrowaveOvenControlServer } from '@matter/node/behaviors/microwave-oven-control';
2
2
  import { MicrowaveOvenModeServer } from '@matter/node/behaviors/microwave-oven-mode';
3
+ import { Status, StatusResponseError } from '@matter/types';
3
4
  import { MicrowaveOvenControl } from '@matter/types/clusters/microwave-oven-control';
4
5
  import { MicrowaveOvenMode } from '@matter/types/clusters/microwave-oven-mode';
5
6
  import { OperationalState } from '@matter/types/clusters/operational-state';
@@ -62,6 +63,13 @@ export class MatterbridgeMicrowaveOvenControlServer extends MicrowaveOvenControl
62
63
  async setCookingParameters(request) {
63
64
  const device = this.endpoint.stateOf(MatterbridgeServer);
64
65
  device.log.info(`MatterbridgeMicrowaveOvenControlServer: setCookingParameters (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
66
+ if (request.cookTime !== undefined && (request.cookTime < 1 || request.cookTime > this.state.maxCookTime)) {
67
+ throw new StatusResponseError(`CookTime ${request.cookTime} must be between 1 and MaxCookTime ${this.state.maxCookTime}`, Status.ConstraintError);
68
+ }
69
+ if (request.powerSetting !== undefined &&
70
+ (request.powerSetting < this.state.minPower || request.powerSetting > this.state.maxPower || (request.powerSetting - this.state.minPower) % this.state.powerStep !== 0)) {
71
+ throw new StatusResponseError(`PowerSetting ${request.powerSetting} must be between MinPower ${this.state.minPower} and MaxPower ${this.state.maxPower} in steps of ${this.state.powerStep}`, Status.ConstraintError);
72
+ }
65
73
  await device.commandHandler.executeHandler('MicrowaveOvenControl.setCookingParameters', {
66
74
  command: 'setCookingParameters',
67
75
  request,
@@ -79,7 +87,7 @@ export class MatterbridgeMicrowaveOvenControlServer extends MicrowaveOvenControl
79
87
  const normalMode = supportedModes.find((mode) => mode.modeTags.some((tag) => tag.value === MicrowaveOvenMode.ModeTag.Normal));
80
88
  await this.endpoint.setStateOf(MicrowaveOvenModeServer, { currentMode: normalMode?.mode });
81
89
  }
82
- if (request.cookTime !== undefined && request.cookTime >= 0 && request.cookTime <= this.state.maxCookTime) {
90
+ if (request.cookTime !== undefined) {
83
91
  device.log.info(`MatterbridgeMicrowaveOvenControlServer: setCookingParameters called setting cookTime to ${request.cookTime}`);
84
92
  this.state.cookTime = request.cookTime;
85
93
  }
@@ -87,7 +95,7 @@ export class MatterbridgeMicrowaveOvenControlServer extends MicrowaveOvenControl
87
95
  device.log.info(`MatterbridgeMicrowaveOvenControlServer: setCookingParameters called with no cookTime so set to 30sec.`);
88
96
  this.state.cookTime = 30;
89
97
  }
90
- if (request.powerSetting !== undefined && request.powerSetting >= this.state.minPower && request.powerSetting <= this.state.maxPower) {
98
+ if (request.powerSetting !== undefined) {
91
99
  device.log.info(`MatterbridgeMicrowaveOvenControlServer: setCookingParameters called setting powerSetting to ${request.powerSetting}`);
92
100
  this.state.powerSetting = request.powerSetting;
93
101
  }
@@ -6,6 +6,7 @@ import { ModeBase } from '@matter/types/clusters/mode-base';
6
6
  import { OperationalState } from '@matter/types/clusters/operational-state';
7
7
  import { OvenMode } from '@matter/types/clusters/oven-mode';
8
8
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
9
+ declare const MatterbridgeOvenCavityOperationalStateServerBase: import("@matter/node").ClusterBehavior.Type<typeof OvenCavityOperationalStateServer, import("@matter/types").ClusterType.WithEnabledAttributes<import("@matter/types").ClusterType.WithEnabledEvents<import("@matter/types/clusters/oven-cavity-operational-state").OvenCavityOperationalState, "operationCompletion">, never>, import("@matter/types").ClusterType.Concrete, new () => {}, "ovenCavityOperationalState">;
9
10
  export interface OvenOptions {
10
11
  mode?: 'server' | 'matter';
11
12
  id?: string;
@@ -38,8 +39,15 @@ export declare class MatterbridgeOvenModeServer extends OvenModeServer {
38
39
  initialize(): void;
39
40
  changeToMode(request: ModeBase.ChangeToModeRequest): MaybePromise<ModeBase.ChangeToModeResponse>;
40
41
  }
41
- export declare class MatterbridgeOvenCavityOperationalStateServer extends OvenCavityOperationalStateServer {
42
+ export declare class MatterbridgeOvenCavityOperationalStateServer extends MatterbridgeOvenCavityOperationalStateServerBase {
43
+ protected internal: MatterbridgeOvenCavityOperationalStateServer.Internal;
42
44
  initialize(): void;
43
45
  stop(): MaybePromise<OperationalState.OperationalCommandResponse>;
44
46
  start(): MaybePromise<OperationalState.OperationalCommandResponse>;
45
47
  }
48
+ export declare namespace MatterbridgeOvenCavityOperationalStateServer {
49
+ class Internal extends MatterbridgeOvenCavityOperationalStateServerBase.Internal {
50
+ operationStartedAt: number | undefined;
51
+ }
52
+ }
53
+ export {};
@@ -8,6 +8,7 @@ import { MatterbridgeServer } from '../behaviors/matterbridgeServer.js';
8
8
  import { oven, powerSource, temperatureControlledCabinetHeater } from '../matterbridgeDeviceTypes.js';
9
9
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
10
10
  import { createNumberTemperatureControlClusterServer } from './temperatureControl.js';
11
+ const MatterbridgeOvenCavityOperationalStateServerBase = OvenCavityOperationalStateServer.enable({ events: { operationCompletion: true } });
11
12
  export class Oven extends MatterbridgeEndpoint {
12
13
  constructor(name, serial, options = {}) {
13
14
  super([oven, powerSource], {
@@ -71,6 +72,7 @@ export class Oven extends MatterbridgeEndpoint {
71
72
  operationalStateList: [
72
73
  { operationalStateId: OperationalState.OperationalStateEnum.Stopped },
73
74
  { operationalStateId: OperationalState.OperationalStateEnum.Running },
75
+ { operationalStateId: OperationalState.OperationalStateEnum.Paused },
74
76
  { operationalStateId: OperationalState.OperationalStateEnum.Error },
75
77
  ],
76
78
  operationalState,
@@ -93,21 +95,31 @@ export class MatterbridgeOvenModeServer extends OvenModeServer {
93
95
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
94
96
  }
95
97
  else {
96
- device.log.error(`MatterbridgeOvenModeServer: changeToMode (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) called with invalid mode ${request.newMode}`);
97
- return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Invalid mode' };
98
+ device.log.error(`MatterbridgeOvenModeServer: changeToMode (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) called with unsupported mode ${request.newMode}`);
99
+ return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: '' };
98
100
  }
99
101
  }
100
102
  }
101
- export class MatterbridgeOvenCavityOperationalStateServer extends OvenCavityOperationalStateServer {
103
+ export class MatterbridgeOvenCavityOperationalStateServer extends MatterbridgeOvenCavityOperationalStateServerBase {
102
104
  initialize() {
103
105
  const device = this.endpoint.stateOf(MatterbridgeServer);
104
106
  device.log.info('MatterbridgeOvenCavityOperationalStateServer initialized: setting operational state to Stopped and operational error to No error');
105
107
  this.state.operationalState = OperationalState.OperationalStateEnum.Stopped;
106
108
  this.state.operationalError = { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
109
+ super.initialize();
107
110
  }
108
111
  stop() {
109
112
  const device = this.endpoint.stateOf(MatterbridgeServer);
110
113
  device.log.info(`MatterbridgeOvenCavityOperationalStateServer: stop (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) called setting operational state to Stopped and operational error to No error`);
114
+ if (this.internal.operationStartedAt !== undefined) {
115
+ const totalOperationalTime = Math.round((Date.now() - this.internal.operationStartedAt) / 1000);
116
+ this.events.operationCompletion.emit({
117
+ completionErrorCode: this.state.operationalError.errorStateId,
118
+ totalOperationalTime,
119
+ pausedTime: 0,
120
+ }, this.context);
121
+ this.internal.operationStartedAt = undefined;
122
+ }
111
123
  this.state.operationalState = OperationalState.OperationalStateEnum.Stopped;
112
124
  this.state.operationalError = { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
113
125
  return {
@@ -117,6 +129,7 @@ export class MatterbridgeOvenCavityOperationalStateServer extends OvenCavityOper
117
129
  start() {
118
130
  const device = this.endpoint.stateOf(MatterbridgeServer);
119
131
  device.log.info(`MatterbridgeOvenCavityOperationalStateServer: start (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) called setting operational state to Running and operational error to No error`);
132
+ this.internal.operationStartedAt ??= Date.now();
120
133
  this.state.operationalState = OperationalState.OperationalStateEnum.Running;
121
134
  this.state.operationalError = { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
122
135
  return {
@@ -124,3 +137,9 @@ export class MatterbridgeOvenCavityOperationalStateServer extends OvenCavityOper
124
137
  };
125
138
  }
126
139
  }
140
+ (function (MatterbridgeOvenCavityOperationalStateServer) {
141
+ class Internal extends MatterbridgeOvenCavityOperationalStateServerBase.Internal {
142
+ operationStartedAt;
143
+ }
144
+ MatterbridgeOvenCavityOperationalStateServer.Internal = Internal;
145
+ })(MatterbridgeOvenCavityOperationalStateServer || (MatterbridgeOvenCavityOperationalStateServer = {}));
@@ -1,4 +1,5 @@
1
1
  import type { MaybePromise } from '@matter/general';
2
+ import { RefrigeratorAlarmServer } from '@matter/node/behaviors/refrigerator-alarm';
2
3
  import { RefrigeratorAndTemperatureControlledCabinetModeServer } from '@matter/node/behaviors/refrigerator-and-temperature-controlled-cabinet-mode';
3
4
  import type { EndpointNumber, Semtag } from '@matter/types';
4
5
  import { ModeBase } from '@matter/types/clusters/mode-base';
@@ -36,3 +37,6 @@ export declare class MatterbridgeRefrigeratorAndTemperatureControlledCabinetMode
36
37
  initialize(): void;
37
38
  changeToMode(request: ModeBase.ChangeToModeRequest): MaybePromise<ModeBase.ChangeToModeResponse>;
38
39
  }
40
+ export declare class MatterbridgeRefrigeratorAlarmServer extends RefrigeratorAlarmServer {
41
+ static readonly schema: import("@matter/model").ClusterModel;
42
+ }
@@ -1,3 +1,4 @@
1
+ import { AttributeElement, EventElement, FieldElement } from '@matter/model';
1
2
  import { RefrigeratorAlarmServer } from '@matter/node/behaviors/refrigerator-alarm';
2
3
  import { RefrigeratorAndTemperatureControlledCabinetModeServer } from '@matter/node/behaviors/refrigerator-and-temperature-controlled-cabinet-mode';
3
4
  import { ModeBase } from '@matter/types/clusters/mode-base';
@@ -46,7 +47,7 @@ export class Refrigerator extends MatterbridgeEndpoint {
46
47
  return endpoint;
47
48
  }
48
49
  createDefaultRefrigeratorAlarmClusterServer(endpoint, doorOpen = false) {
49
- endpoint.behaviors.require(RefrigeratorAlarmServer, {
50
+ endpoint.behaviors.require(MatterbridgeRefrigeratorAlarmServer, {
50
51
  mask: { doorOpen: true },
51
52
  supported: { doorOpen: true },
52
53
  state: { doorOpen },
@@ -59,10 +60,20 @@ export class Refrigerator extends MatterbridgeEndpoint {
59
60
  }
60
61
  async triggerDoorOpenState(doorOpen) {
61
62
  if (doorOpen) {
62
- await this.triggerEvent('RefrigeratorAlarm', 'notify', { active: { doorOpen: true }, inactive: { doorOpen: false }, state: { doorOpen: true }, mask: { doorOpen: true } }, this.log);
63
+ await this.triggerEvent('RefrigeratorAlarm', 'notify', {
64
+ active: { doorOpen: true },
65
+ inactive: { doorOpen: false },
66
+ state: { doorOpen: true },
67
+ mask: { doorOpen: true },
68
+ }, this.log);
63
69
  }
64
70
  else {
65
- await this.triggerEvent('RefrigeratorAlarm', 'notify', { active: { doorOpen: false }, inactive: { doorOpen: true }, state: { doorOpen: false }, mask: { doorOpen: true } }, this.log);
71
+ await this.triggerEvent('RefrigeratorAlarm', 'notify', {
72
+ active: { doorOpen: false },
73
+ inactive: { doorOpen: true },
74
+ state: { doorOpen: false },
75
+ mask: { doorOpen: true },
76
+ }, this.log);
66
77
  }
67
78
  return this;
68
79
  }
@@ -86,3 +97,7 @@ export class MatterbridgeRefrigeratorAndTemperatureControlledCabinetModeServer e
86
97
  }
87
98
  }
88
99
  }
100
+ const MatterbridgeRefrigeratorAlarmSchema = RefrigeratorAlarmServer.schema.extend({}, AttributeElement({ name: 'Mask', id: 0x0000, type: 'AlarmBitmap', access: 'R V', conformance: 'M' }), AttributeElement({ name: 'State', id: 0x0002, type: 'AlarmBitmap', access: 'R V', conformance: 'M' }), AttributeElement({ name: 'Supported', id: 0x0003, type: 'AlarmBitmap', access: 'R V', conformance: 'M', quality: 'F' }), EventElement({ name: 'Notify', id: 0x0000, access: 'V', conformance: 'M', priority: 'info' }, FieldElement({ name: 'Active', id: 0x0000, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'Inactive', id: 0x0001, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'State', id: 0x0002, type: 'AlarmBitmap', conformance: 'M' }), FieldElement({ name: 'Mask', id: 0x0003, type: 'AlarmBitmap', conformance: 'M' })));
101
+ export class MatterbridgeRefrigeratorAlarmServer extends RefrigeratorAlarmServer {
102
+ static schema = MatterbridgeRefrigeratorAlarmSchema;
103
+ }
@@ -10,6 +10,7 @@ import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
10
10
  import { ServiceArea } from '@matter/types/clusters/service-area';
11
11
  import type { Semtag } from '@matter/types/globals';
12
12
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
13
+ declare const MatterbridgeRvcOperationalStateServerBase: import("@matter/node").ClusterBehavior.Type<typeof RvcOperationalStateServer, import("@matter/types").ClusterType.WithEnabledAttributes<import("@matter/types").ClusterType.WithEnabledEvents<RvcOperationalState, "operationCompletion">, "countdownTime">, import("@matter/types").ClusterType.Concrete, new () => {}, "rvcOperationalState">;
13
14
  export interface RoboticVacuumCleanerOptions {
14
15
  mode?: 'server' | 'matter';
15
16
  id?: string;
@@ -42,8 +43,20 @@ export declare class MatterbridgeRvcRunModeServer extends RvcRunModeServer {
42
43
  export declare class MatterbridgeRvcCleanModeServer extends RvcCleanModeServer {
43
44
  changeToMode(request: ModeBase.ChangeToModeRequest): Promise<ModeBase.ChangeToModeResponse>;
44
45
  }
45
- export declare class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateServer {
46
+ export declare class MatterbridgeRvcOperationalStateServer extends MatterbridgeRvcOperationalStateServerBase {
47
+ protected internal: MatterbridgeRvcOperationalStateServer.Internal;
48
+ beginOperation(): void;
49
+ completeOperation(): void;
46
50
  pause(): Promise<OperationalState.OperationalCommandResponse>;
47
51
  resume(): Promise<OperationalState.OperationalCommandResponse>;
48
52
  goHome(): Promise<OperationalState.OperationalCommandResponse>;
49
53
  }
54
+ export declare namespace MatterbridgeRvcOperationalStateServer {
55
+ class Internal extends MatterbridgeRvcOperationalStateServerBase.Internal {
56
+ operationalStateBeforePause: RvcOperationalState.OperationalState | OperationalState.OperationalStateEnum;
57
+ operationStartedAt: number | undefined;
58
+ pausedAccumulatedMs: number;
59
+ pausedSinceMs: number | undefined;
60
+ }
61
+ }
62
+ export {};
@@ -13,6 +13,10 @@ import { MatterbridgeServer } from '../behaviors/matterbridgeServer.js';
13
13
  import { MatterbridgeServiceAreaServer } from '../behaviors/serviceAreaServer.js';
14
14
  import { powerSource, roboticVacuumCleaner } from '../matterbridgeDeviceTypes.js';
15
15
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
16
+ const MatterbridgeRvcOperationalStateServerBase = RvcOperationalStateServer.enable({
17
+ attributes: { countdownTime: true },
18
+ events: { operationCompletion: true },
19
+ });
16
20
  export class RoboticVacuumCleaner extends MatterbridgeEndpoint {
17
21
  constructor(name, serial, modeOrOptions, currentRunMode, supportedRunModes, currentCleanMode, supportedCleanModes, currentPhase = null, phaseList = null, operationalState, operationalStateList, supportedAreas, selectedAreas, currentArea, supportedMaps) {
18
22
  const options = typeof modeOrOptions === 'object'
@@ -104,6 +108,7 @@ export class RoboticVacuumCleaner extends MatterbridgeEndpoint {
104
108
  this.behaviors.require(MatterbridgeRvcOperationalStateServer, {
105
109
  phaseList,
106
110
  currentPhase,
111
+ countdownTime: null,
107
112
  operationalStateList: operationalStateList ?? [
108
113
  { operationalStateId: RvcOperationalState.OperationalState.Stopped },
109
114
  { operationalStateId: RvcOperationalState.OperationalState.Running },
@@ -135,19 +140,34 @@ export class MatterbridgeRvcRunModeServer extends RvcRunModeServer {
135
140
  device.log.error(`MatterbridgeRvcRunModeServer changeToMode called with unsupported newMode: ${request.newMode}`);
136
141
  return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: 'Unsupported mode' };
137
142
  }
143
+ const currentIsIdle = this.state.supportedModes.some((mode) => mode.mode === this.state.currentMode && mode.modeTags.some((tag) => tag.value === RvcRunMode.ModeTag.Idle));
144
+ const requestedIsIdle = supported.modeTags.some((tag) => tag.value === RvcRunMode.ModeTag.Idle);
145
+ if (request.newMode !== this.state.currentMode && !this.features.directModeChange && !currentIsIdle && !requestedIsIdle) {
146
+ device.log.debug(`MatterbridgeRvcRunModeServer changeToMode rejected direct non-Idle mode change from ${this.state.currentMode} to ${request.newMode}`);
147
+ return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Direct mode change is not supported while operating' };
148
+ }
149
+ if (request.newMode === this.state.currentMode) {
150
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Already in requested mode' };
151
+ }
138
152
  this.state.currentMode = request.newMode;
139
153
  if (supported.modeTags.find((tag) => tag.value === RvcRunMode.ModeTag.Cleaning)) {
140
154
  device.log.debug('MatterbridgeRvcRunModeServer changeToMode called with newMode Cleaning => Running');
141
- this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Running;
155
+ const operationalState = this.agent.get(MatterbridgeRvcOperationalStateServer);
156
+ operationalState.beginOperation();
157
+ operationalState.state.operationalState = RvcOperationalState.OperationalState.Running;
142
158
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Running' };
143
159
  }
144
160
  else if (supported.modeTags.find((tag) => tag.value === RvcRunMode.ModeTag.Idle)) {
145
- device.log.debug('MatterbridgeRvcRunModeServer changeToMode called with newMode Idle => Docked');
146
- this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Docked;
147
- return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Docked' };
161
+ device.log.debug('MatterbridgeRvcRunModeServer changeToMode called with newMode Idle => SeekingCharger');
162
+ const operationalState = this.agent.get(MatterbridgeRvcOperationalStateServer);
163
+ operationalState.completeOperation();
164
+ operationalState.state.operationalState = RvcOperationalState.OperationalState.SeekingCharger;
165
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Seeking charger' };
148
166
  }
149
167
  device.log.debug(`MatterbridgeRvcRunModeServer changeToMode called with newMode ${request.newMode} => ${supported.label}`);
150
- this.agent.get(MatterbridgeRvcOperationalStateServer).state.operationalState = RvcOperationalState.OperationalState.Running;
168
+ const operationalState = this.agent.get(MatterbridgeRvcOperationalStateServer);
169
+ operationalState.beginOperation();
170
+ operationalState.state.operationalState = RvcOperationalState.OperationalState.Running;
151
171
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
152
172
  }
153
173
  }
@@ -167,12 +187,40 @@ export class MatterbridgeRvcCleanModeServer extends RvcCleanModeServer {
167
187
  device.log.error(`MatterbridgeRvcCleanModeServer changeToMode called with unsupported newMode: ${request.newMode}`);
168
188
  return { status: ModeBase.ModeChangeStatus.UnsupportedMode, statusText: 'Unsupported mode' };
169
189
  }
190
+ if (request.newMode === this.state.currentMode) {
191
+ return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Already in requested mode' };
192
+ }
193
+ const runModeState = this.agent.get(MatterbridgeRvcRunModeServer).state;
194
+ const runModeIsIdle = runModeState.supportedModes.some((mode) => mode.mode === runModeState.currentMode && mode.modeTags.some((tag) => tag.value === RvcRunMode.ModeTag.Idle));
195
+ if (!this.features.directModeChange && !runModeIsIdle) {
196
+ device.log.debug(`MatterbridgeRvcCleanModeServer changeToMode rejected while RVC Run Mode ${runModeState.currentMode} is non-Idle`);
197
+ return { status: ModeBase.ModeChangeStatus.InvalidInMode, statusText: 'Clean mode cannot change while operating' };
198
+ }
170
199
  this.state.currentMode = request.newMode;
171
200
  device.log.debug(`MatterbridgeRvcCleanModeServer changeToMode called with newMode ${request.newMode} => ${supported.label}`);
172
201
  return { status: ModeBase.ModeChangeStatus.Success, statusText: 'Success' };
173
202
  }
174
203
  }
175
- export class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateServer {
204
+ export class MatterbridgeRvcOperationalStateServer extends MatterbridgeRvcOperationalStateServerBase {
205
+ beginOperation() {
206
+ if (this.internal.operationStartedAt === undefined) {
207
+ this.internal.operationStartedAt = Date.now();
208
+ this.internal.pausedAccumulatedMs = 0;
209
+ }
210
+ }
211
+ completeOperation() {
212
+ if (this.internal.operationStartedAt === undefined)
213
+ return;
214
+ if (this.internal.pausedSinceMs !== undefined) {
215
+ this.internal.pausedAccumulatedMs += Date.now() - this.internal.pausedSinceMs;
216
+ this.internal.pausedSinceMs = undefined;
217
+ }
218
+ const totalOperationalTime = Math.round((Date.now() - this.internal.operationStartedAt) / 1000);
219
+ const pausedTime = Math.round(this.internal.pausedAccumulatedMs / 1000);
220
+ this.events.operationCompletion.emit({ completionErrorCode: OperationalState.ErrorState.NoError, totalOperationalTime, pausedTime }, this.context);
221
+ this.internal.operationStartedAt = undefined;
222
+ this.internal.pausedAccumulatedMs = 0;
223
+ }
176
224
  async pause() {
177
225
  const device = this.endpoint.stateOf(MatterbridgeServer);
178
226
  device.log.info(`Pause (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
@@ -183,8 +231,17 @@ export class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateSe
183
231
  attributes: this.state,
184
232
  endpoint: this.endpoint,
185
233
  });
186
- device.log.debug('MatterbridgeRvcOperationalStateServer: pause called setting operational state to Paused and currentMode to Idle');
187
- this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 1;
234
+ if (this.state.operationalState === RvcOperationalState.OperationalState.Paused) {
235
+ return { commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Already paused' } };
236
+ }
237
+ if (this.state.operationalState !== RvcOperationalState.OperationalState.Running && this.state.operationalState !== RvcOperationalState.OperationalState.SeekingCharger) {
238
+ return {
239
+ commandResponseState: { errorStateId: OperationalState.ErrorState.CommandInvalidInState, errorStateDetails: 'Not Pause-compatible in the current operational state' },
240
+ };
241
+ }
242
+ device.log.debug('MatterbridgeRvcOperationalStateServer: pause called setting operational state to Paused');
243
+ this.internal.operationalStateBeforePause = this.state.operationalState;
244
+ this.internal.pausedSinceMs = Date.now();
188
245
  this.state.operationalState = RvcOperationalState.OperationalState.Paused;
189
246
  this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
190
247
  return {
@@ -201,9 +258,20 @@ export class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateSe
201
258
  attributes: this.state,
202
259
  endpoint: this.endpoint,
203
260
  });
204
- device.log.debug('MatterbridgeRvcOperationalStateServer: resume called setting operational state to Running and currentMode to Cleaning');
205
- this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 2;
206
- this.state.operationalState = RvcOperationalState.OperationalState.Running;
261
+ if (this.state.operationalState === RvcOperationalState.OperationalState.Running) {
262
+ return { commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Already running' } };
263
+ }
264
+ if (this.state.operationalState !== RvcOperationalState.OperationalState.Paused) {
265
+ return {
266
+ commandResponseState: { errorStateId: OperationalState.ErrorState.CommandInvalidInState, errorStateDetails: 'Not Resume-compatible in the current operational state' },
267
+ };
268
+ }
269
+ device.log.debug(`MatterbridgeRvcOperationalStateServer: resume called restoring operational state to ${this.internal.operationalStateBeforePause}`);
270
+ if (this.internal.pausedSinceMs !== undefined) {
271
+ this.internal.pausedAccumulatedMs += Date.now() - this.internal.pausedSinceMs;
272
+ this.internal.pausedSinceMs = undefined;
273
+ }
274
+ this.state.operationalState = this.internal.operationalStateBeforePause;
207
275
  this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
208
276
  return {
209
277
  commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' },
@@ -219,12 +287,34 @@ export class MatterbridgeRvcOperationalStateServer extends RvcOperationalStateSe
219
287
  attributes: this.state,
220
288
  endpoint: this.endpoint,
221
289
  });
222
- device.log.debug('MatterbridgeRvcOperationalStateServer: goHome called setting operational state to Docked and currentMode to Idle');
223
- this.agent.get(MatterbridgeRvcRunModeServer).state.currentMode = 1;
224
- this.state.operationalState = RvcOperationalState.OperationalState.Docked;
290
+ if (this.state.operationalState === RvcOperationalState.OperationalState.SeekingCharger) {
291
+ return { commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Already seeking charger' } };
292
+ }
293
+ if (this.state.operationalState === RvcOperationalState.OperationalState.Error ||
294
+ this.state.operationalState === RvcOperationalState.OperationalState.Charging ||
295
+ this.state.operationalState === RvcOperationalState.OperationalState.Docked ||
296
+ this.state.operationalState === RvcOperationalState.OperationalState.EmptyingDustBin ||
297
+ this.state.operationalState === RvcOperationalState.OperationalState.CleaningMop ||
298
+ this.state.operationalState === RvcOperationalState.OperationalState.FillingWaterTank ||
299
+ this.state.operationalState === RvcOperationalState.OperationalState.UpdatingMaps) {
300
+ return {
301
+ commandResponseState: { errorStateId: OperationalState.ErrorState.CommandInvalidInState, errorStateDetails: 'Cannot seek the charger in the current operational state' },
302
+ };
303
+ }
304
+ device.log.debug('MatterbridgeRvcOperationalStateServer: goHome called setting operational state to SeekingCharger');
305
+ this.state.operationalState = RvcOperationalState.OperationalState.SeekingCharger;
225
306
  this.state.operationalError = { errorStateId: RvcOperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' };
226
307
  return {
227
308
  commandResponseState: { errorStateId: OperationalState.ErrorState.NoError, errorStateDetails: 'Fully operational' },
228
309
  };
229
310
  }
230
311
  }
312
+ (function (MatterbridgeRvcOperationalStateServer) {
313
+ class Internal extends MatterbridgeRvcOperationalStateServerBase.Internal {
314
+ operationalStateBeforePause = RvcOperationalState.OperationalState.Running;
315
+ operationStartedAt;
316
+ pausedAccumulatedMs = 0;
317
+ pausedSinceMs;
318
+ }
319
+ MatterbridgeRvcOperationalStateServer.Internal = Internal;
320
+ })(MatterbridgeRvcOperationalStateServer || (MatterbridgeRvcOperationalStateServer = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.10.7-dev-20260825-7ebac36",
3
+ "version": "3.10.7-dev-20260827-957ddcc",
4
4
  "description": "Matterbridge core library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -132,10 +132,10 @@
132
132
  },
133
133
  "dependencies": {
134
134
  "@matter/main": "0.17.9",
135
- "@matterbridge/dgram": "3.10.7-dev-20260825-7ebac36",
136
- "@matterbridge/thread": "3.10.7-dev-20260825-7ebac36",
137
- "@matterbridge/types": "3.10.7-dev-20260825-7ebac36",
138
- "@matterbridge/utils": "3.10.7-dev-20260825-7ebac36",
135
+ "@matterbridge/dgram": "3.10.7-dev-20260827-957ddcc",
136
+ "@matterbridge/thread": "3.10.7-dev-20260827-957ddcc",
137
+ "@matterbridge/types": "3.10.7-dev-20260827-957ddcc",
138
+ "@matterbridge/utils": "3.10.7-dev-20260827-957ddcc",
139
139
  "escape-html": "1.0.3",
140
140
  "express": "5.2.1",
141
141
  "express-rate-limit": "8.6.2",