From 977e8adbfba5c49a6b6f10d1ca53d609477aa212 Mon Sep 17 00:00:00 2001 From: ocrease <18347739+ocrease@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:23:38 +0100 Subject: [PATCH] Fix operational state and vacuum state for matter vacuum (#147466) --- homeassistant/components/matter/sensor.py | 24 +++++++++++++----- homeassistant/components/matter/vacuum.py | 19 +++++++------- .../matter/snapshots/test_sensor.ambr | 4 +-- tests/components/matter/test_vacuum.py | 25 +++++++++++++++---- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 0b4d3cc3330..f744ec8885a 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import datetime from typing import TYPE_CHECKING, cast @@ -74,6 +74,11 @@ OPERATIONAL_STATE_MAP = { clusters.OperationalState.Enums.OperationalStateEnum.kRunning: "running", clusters.OperationalState.Enums.OperationalStateEnum.kPaused: "paused", clusters.OperationalState.Enums.OperationalStateEnum.kError: "error", +} + +RVC_OPERATIONAL_STATE_MAP = { + # enum with known Operation state values which we can translate + **OPERATIONAL_STATE_MAP, clusters.RvcOperationalState.Enums.OperationalStateEnum.kSeekingCharger: "seeking_charger", clusters.RvcOperationalState.Enums.OperationalStateEnum.kCharging: "charging", clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked", @@ -171,6 +176,10 @@ class MatterOperationalStateSensorEntityDescription(MatterSensorEntityDescriptio state_list_attribute: type[ClusterAttributeDescriptor] = ( clusters.OperationalState.Attributes.OperationalStateList ) + state_attribute: type[ClusterAttributeDescriptor] = ( + clusters.OperationalState.Attributes.OperationalState + ) + state_map: dict[int, str] = field(default_factory=lambda: OPERATIONAL_STATE_MAP) class MatterSensor(MatterEntity, SensorEntity): @@ -245,15 +254,15 @@ class MatterOperationalStateSensor(MatterSensor): for state in operational_state_list: # prefer translateable (known) state from mapping, # fallback to the raw state label as given by the device/manufacturer - states_map[state.operationalStateID] = OPERATIONAL_STATE_MAP.get( - state.operationalStateID, slugify(state.operationalStateLabel) + states_map[state.operationalStateID] = ( + self.entity_description.state_map.get( + state.operationalStateID, slugify(state.operationalStateLabel) + ) ) self.states_map = states_map self._attr_options = list(states_map.values()) self._attr_native_value = states_map.get( - self.get_matter_attribute_value( - clusters.OperationalState.Attributes.OperationalState - ) + self.get_matter_attribute_value(self.entity_description.state_attribute) ) @@ -999,6 +1008,8 @@ DISCOVERY_SCHEMAS = [ device_class=SensorDeviceClass.ENUM, translation_key="operational_state", state_list_attribute=clusters.RvcOperationalState.Attributes.OperationalStateList, + state_attribute=clusters.RvcOperationalState.Attributes.OperationalState, + state_map=RVC_OPERATIONAL_STATE_MAP, ), entity_class=MatterOperationalStateSensor, required_attributes=( @@ -1016,6 +1027,7 @@ DISCOVERY_SCHEMAS = [ device_class=SensorDeviceClass.ENUM, translation_key="operational_state", state_list_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalStateList, + state_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalState, ), entity_class=MatterOperationalStateSensor, required_attributes=( diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index 5ea1716a37d..96c6ba212de 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -30,10 +30,10 @@ class OperationalState(IntEnum): Combination of generic OperationalState and RvcOperationalState. """ - NO_ERROR = 0x00 - UNABLE_TO_START_OR_RESUME = 0x01 - UNABLE_TO_COMPLETE_OPERATION = 0x02 - COMMAND_INVALID_IN_STATE = 0x03 + STOPPED = 0x00 + RUNNING = 0x01 + PAUSED = 0x02 + ERROR = 0x03 SEEKING_CHARGER = 0x40 CHARGING = 0x41 DOCKED = 0x42 @@ -95,7 +95,7 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): async def async_pause(self) -> None: """Pause the cleaning task.""" - await self.send_device_command(clusters.OperationalState.Commands.Pause()) + await self.send_device_command(clusters.RvcOperationalState.Commands.Pause()) @callback def _update_from_device(self) -> None: @@ -120,11 +120,10 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): state = VacuumActivity.DOCKED elif operational_state == OperationalState.SEEKING_CHARGER: state = VacuumActivity.RETURNING - elif operational_state in ( - OperationalState.UNABLE_TO_COMPLETE_OPERATION, - OperationalState.UNABLE_TO_START_OR_RESUME, - ): + elif operational_state == OperationalState.ERROR: state = VacuumActivity.ERROR + elif operational_state == OperationalState.PAUSED: + state = VacuumActivity.PAUSED elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None: tags = {x.value for x in run_mode.modeTags} if ModeTag.CLEANING in tags: @@ -201,7 +200,7 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterVacuum, required_attributes=( clusters.RvcRunMode.Attributes.CurrentMode, - clusters.RvcOperationalState.Attributes.CurrentPhase, + clusters.RvcOperationalState.Attributes.OperationalState, ), optional_attributes=( clusters.RvcCleanMode.Attributes.CurrentMode, diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 17841121445..8e459c0f573 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -3775,7 +3775,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'running', }) # --- # name: test_sensors[oven][sensor.mock_oven_temperature_2-entry] @@ -6433,7 +6433,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'stopped', }) # --- # name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry] diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index 5bd90ee1109..2642ff39ef8 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -93,7 +93,7 @@ async def test_vacuum_actions( assert matter_client.send_device_command.call_args == call( node_id=matter_node.node_id, endpoint_id=1, - command=clusters.OperationalState.Commands.Pause(), + command=clusters.RvcOperationalState.Commands.Pause(), ) matter_client.send_device_command.reset_mock() @@ -168,19 +168,26 @@ async def test_vacuum_updates( assert state assert state.state == "returning" - # confirm state is 'error' by setting the operational state to 0x01 + # confirm state is 'idle' by setting the operational state to 0x01 (running) but mode is idle set_node_attribute(matter_node, 1, 97, 4, 0x01) await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state - assert state.state == "error" + assert state.state == "idle" - # confirm state is 'error' by setting the operational state to 0x02 + # confirm state is 'idle' by setting the operational state to 0x01 (running) but mode is cleaning + set_node_attribute(matter_node, 1, 97, 4, 0x01) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state + assert state.state == "idle" + + # confirm state is 'paused' by setting the operational state to 0x02 set_node_attribute(matter_node, 1, 97, 4, 0x02) await trigger_subscription_callback(hass, matter_client) state = hass.states.get(entity_id) assert state - assert state.state == "error" + assert state.state == "paused" # confirm state is 'cleaning' by setting; # - the operational state to 0x00 @@ -211,3 +218,11 @@ async def test_vacuum_updates( state = hass.states.get(entity_id) assert state assert state.state == "unknown" + + # confirm state is 'error' by setting; + # - the operational state to 0x03 + set_node_attribute(matter_node, 1, 97, 4, 3) + await trigger_subscription_callback(hass, matter_client) + state = hass.states.get(entity_id) + assert state + assert state.state == "error"