From be492965474b7c4ab888837a223cb9357777ceea Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 26 Jun 2025 11:54:52 +0200 Subject: [PATCH] Deduplicate shared logic in Matter vacuum commands (#147578) Get the run mode by tag in a single place to avoid code duplication. Also raise an error if the run mode (unexpectedly) is not found. --- homeassistant/components/matter/vacuum.py | 47 ++++++++++++-------- tests/components/matter/test_vacuum.py | 53 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index 141400c384b..6ab687e060a 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -17,6 +17,7 @@ from homeassistant.components.vacuum import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .entity import MatterEntity @@ -67,20 +68,31 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): entity_description: StateVacuumEntityDescription _platform_translation_key = "vacuum" + def _get_run_mode_by_tag( + self, tag: ModeTag + ) -> clusters.RvcRunMode.Structs.ModeOptionStruct | None: + """Get the run mode by tag.""" + supported_run_modes = self._supported_run_modes or {} + for mode in supported_run_modes.values(): + for t in mode.modeTags: + if t.value == tag.value: + return mode + return None + async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" # We simply set the RvcRunMode to the first runmode # that has the idle tag to stop the vacuum cleaner. # this is compatible with both Matter 1.2 and 1.3+ devices. - supported_run_modes = self._supported_run_modes or {} - for mode in supported_run_modes.values(): - for tag in mode.modeTags: - if tag.value == ModeTag.IDLE: - # stop the vacuum by changing the run mode to idle - await self.send_device_command( - clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) - ) - return + mode = self._get_run_mode_by_tag(ModeTag.IDLE) + if mode is None: + raise HomeAssistantError( + "No supported run mode found to stop the vacuum cleaner." + ) + + await self.send_device_command( + clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) + ) async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" @@ -110,14 +122,15 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): # We simply set the RvcRunMode to the first runmode # that has the cleaning tag to start the vacuum cleaner. # this is compatible with both Matter 1.2 and 1.3+ devices. - supported_run_modes = self._supported_run_modes or {} - for mode in supported_run_modes.values(): - for tag in mode.modeTags: - if tag.value == ModeTag.CLEANING: - await self.send_device_command( - clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) - ) - return + mode = self._get_run_mode_by_tag(ModeTag.CLEANING) + if mode is None: + raise HomeAssistantError( + "No supported run mode found to start the vacuum cleaner." + ) + + await self.send_device_command( + clusters.RvcRunMode.Commands.ChangeToMode(newMode=mode.mode) + ) async def async_pause(self) -> None: """Pause the cleaning task.""" diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index b464e9f1cd3..cba4b9b59eb 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -9,6 +9,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -238,3 +239,55 @@ async def test_vacuum_updates( state = hass.states.get(entity_id) assert state assert state.state == "error" + + +@pytest.mark.parametrize("node_fixture", ["vacuum_cleaner"]) +async def test_vacuum_actions_no_supported_run_modes( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test vacuum entity actions when no supported run modes are available.""" + # Fetch translations + await async_setup_component(hass, "homeassistant", {}) + entity_id = "vacuum.mock_vacuum" + state = hass.states.get(entity_id) + assert state + + # Set empty supported modes to simulate no available run modes + # RvcRunMode cluster ID is 84, SupportedModes attribute ID is 0 + set_node_attribute(matter_node, 1, 84, 0, []) + # RvcOperationalState cluster ID is 97, AcceptedCommandList attribute ID is 65529 + set_node_attribute(matter_node, 1, 97, 65529, []) + await trigger_subscription_callback(hass, matter_client) + + # test start action fails when no supported run modes + with pytest.raises( + HomeAssistantError, + match="No supported run mode found to start the vacuum cleaner", + ): + await hass.services.async_call( + "vacuum", + "start", + { + "entity_id": entity_id, + }, + blocking=True, + ) + + # test stop action fails when no supported run modes + with pytest.raises( + HomeAssistantError, + match="No supported run mode found to stop the vacuum cleaner", + ): + await hass.services.async_call( + "vacuum", + "stop", + { + "entity_id": entity_id, + }, + blocking=True, + ) + + # Ensure no commands were sent to the device + assert matter_client.send_device_command.call_count == 0