Fix operational state and vacuum state for matter vacuum (#147466)

This commit is contained in:
ocrease 2025-06-25 14:23:38 +01:00 committed by GitHub
parent c54ce7eabd
commit 977e8adbfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 49 additions and 23 deletions

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
@ -74,6 +74,11 @@ OPERATIONAL_STATE_MAP = {
clusters.OperationalState.Enums.OperationalStateEnum.kRunning: "running", clusters.OperationalState.Enums.OperationalStateEnum.kRunning: "running",
clusters.OperationalState.Enums.OperationalStateEnum.kPaused: "paused", clusters.OperationalState.Enums.OperationalStateEnum.kPaused: "paused",
clusters.OperationalState.Enums.OperationalStateEnum.kError: "error", 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.kSeekingCharger: "seeking_charger",
clusters.RvcOperationalState.Enums.OperationalStateEnum.kCharging: "charging", clusters.RvcOperationalState.Enums.OperationalStateEnum.kCharging: "charging",
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked", clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
@ -171,6 +176,10 @@ class MatterOperationalStateSensorEntityDescription(MatterSensorEntityDescriptio
state_list_attribute: type[ClusterAttributeDescriptor] = ( state_list_attribute: type[ClusterAttributeDescriptor] = (
clusters.OperationalState.Attributes.OperationalStateList 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): class MatterSensor(MatterEntity, SensorEntity):
@ -245,15 +254,15 @@ class MatterOperationalStateSensor(MatterSensor):
for state in operational_state_list: for state in operational_state_list:
# prefer translateable (known) state from mapping, # prefer translateable (known) state from mapping,
# fallback to the raw state label as given by the device/manufacturer # fallback to the raw state label as given by the device/manufacturer
states_map[state.operationalStateID] = OPERATIONAL_STATE_MAP.get( states_map[state.operationalStateID] = (
state.operationalStateID, slugify(state.operationalStateLabel) self.entity_description.state_map.get(
state.operationalStateID, slugify(state.operationalStateLabel)
)
) )
self.states_map = states_map self.states_map = states_map
self._attr_options = list(states_map.values()) self._attr_options = list(states_map.values())
self._attr_native_value = states_map.get( self._attr_native_value = states_map.get(
self.get_matter_attribute_value( self.get_matter_attribute_value(self.entity_description.state_attribute)
clusters.OperationalState.Attributes.OperationalState
)
) )
@ -999,6 +1008,8 @@ DISCOVERY_SCHEMAS = [
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
translation_key="operational_state", translation_key="operational_state",
state_list_attribute=clusters.RvcOperationalState.Attributes.OperationalStateList, state_list_attribute=clusters.RvcOperationalState.Attributes.OperationalStateList,
state_attribute=clusters.RvcOperationalState.Attributes.OperationalState,
state_map=RVC_OPERATIONAL_STATE_MAP,
), ),
entity_class=MatterOperationalStateSensor, entity_class=MatterOperationalStateSensor,
required_attributes=( required_attributes=(
@ -1016,6 +1027,7 @@ DISCOVERY_SCHEMAS = [
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
translation_key="operational_state", translation_key="operational_state",
state_list_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalStateList, state_list_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalStateList,
state_attribute=clusters.OvenCavityOperationalState.Attributes.OperationalState,
), ),
entity_class=MatterOperationalStateSensor, entity_class=MatterOperationalStateSensor,
required_attributes=( required_attributes=(

View File

@ -30,10 +30,10 @@ class OperationalState(IntEnum):
Combination of generic OperationalState and RvcOperationalState. Combination of generic OperationalState and RvcOperationalState.
""" """
NO_ERROR = 0x00 STOPPED = 0x00
UNABLE_TO_START_OR_RESUME = 0x01 RUNNING = 0x01
UNABLE_TO_COMPLETE_OPERATION = 0x02 PAUSED = 0x02
COMMAND_INVALID_IN_STATE = 0x03 ERROR = 0x03
SEEKING_CHARGER = 0x40 SEEKING_CHARGER = 0x40
CHARGING = 0x41 CHARGING = 0x41
DOCKED = 0x42 DOCKED = 0x42
@ -95,7 +95,7 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
async def async_pause(self) -> None: async def async_pause(self) -> None:
"""Pause the cleaning task.""" """Pause the cleaning task."""
await self.send_device_command(clusters.OperationalState.Commands.Pause()) await self.send_device_command(clusters.RvcOperationalState.Commands.Pause())
@callback @callback
def _update_from_device(self) -> None: def _update_from_device(self) -> None:
@ -120,11 +120,10 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
state = VacuumActivity.DOCKED state = VacuumActivity.DOCKED
elif operational_state == OperationalState.SEEKING_CHARGER: elif operational_state == OperationalState.SEEKING_CHARGER:
state = VacuumActivity.RETURNING state = VacuumActivity.RETURNING
elif operational_state in ( elif operational_state == OperationalState.ERROR:
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
OperationalState.UNABLE_TO_START_OR_RESUME,
):
state = VacuumActivity.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: elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
tags = {x.value for x in run_mode.modeTags} tags = {x.value for x in run_mode.modeTags}
if ModeTag.CLEANING in tags: if ModeTag.CLEANING in tags:
@ -201,7 +200,7 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterVacuum, entity_class=MatterVacuum,
required_attributes=( required_attributes=(
clusters.RvcRunMode.Attributes.CurrentMode, clusters.RvcRunMode.Attributes.CurrentMode,
clusters.RvcOperationalState.Attributes.CurrentPhase, clusters.RvcOperationalState.Attributes.OperationalState,
), ),
optional_attributes=( optional_attributes=(
clusters.RvcCleanMode.Attributes.CurrentMode, clusters.RvcCleanMode.Attributes.CurrentMode,

View File

@ -3775,7 +3775,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'running',
}) })
# --- # ---
# name: test_sensors[oven][sensor.mock_oven_temperature_2-entry] # name: test_sensors[oven][sensor.mock_oven_temperature_2-entry]
@ -6433,7 +6433,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unknown', 'state': 'stopped',
}) })
# --- # ---
# name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry] # name: test_sensors[window_covering_full][sensor.mock_full_window_covering_target_opening_position-entry]

View File

@ -93,7 +93,7 @@ async def test_vacuum_actions(
assert matter_client.send_device_command.call_args == call( assert matter_client.send_device_command.call_args == call(
node_id=matter_node.node_id, node_id=matter_node.node_id,
endpoint_id=1, endpoint_id=1,
command=clusters.OperationalState.Commands.Pause(), command=clusters.RvcOperationalState.Commands.Pause(),
) )
matter_client.send_device_command.reset_mock() matter_client.send_device_command.reset_mock()
@ -168,19 +168,26 @@ async def test_vacuum_updates(
assert state assert state
assert state.state == "returning" 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) set_node_attribute(matter_node, 1, 97, 4, 0x01)
await trigger_subscription_callback(hass, matter_client) await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state 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) set_node_attribute(matter_node, 1, 97, 4, 0x02)
await trigger_subscription_callback(hass, matter_client) await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "error" assert state.state == "paused"
# confirm state is 'cleaning' by setting; # confirm state is 'cleaning' by setting;
# - the operational state to 0x00 # - the operational state to 0x00
@ -211,3 +218,11 @@ async def test_vacuum_updates(
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == "unknown" 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"