diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 6a99548890885e..64a68e499713ae 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -127,7 +127,7 @@ class AirlySensorEntityDescription(SensorEntityDescription): ), AirlySensorEntityDescription( key=ATTR_API_CO, - translation_key="co", + device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=0, diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index f4f98afec27d0b..4c3a50b194b06d 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -23,9 +23,6 @@ "sensor": { "caqi": { "name": "Common air quality index" - }, - "co": { - "name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]" } } }, diff --git a/homeassistant/components/pyload/coordinator.py b/homeassistant/components/pyload/coordinator.py index a69ba0c67dd88d..a9d1ed2a3281c0 100644 --- a/homeassistant/components/pyload/coordinator.py +++ b/homeassistant/components/pyload/coordinator.py @@ -68,7 +68,9 @@ async def _async_update_data(self) -> PyLoadData: raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="setup_authentication_exception", - translation_placeholders={CONF_USERNAME: self.pyload.username}, + translation_placeholders={ + CONF_USERNAME: self.config_entry.data[CONF_USERNAME] + }, ) from e except CannotConnect as e: raise UpdateFailed( diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index fe36327cc75487..2a008128f86e02 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["pyloadapi"], "quality_scale": "platinum", - "requirements": ["PyLoadAPI==2.0.0"] + "requirements": ["PyLoadAPI==2.1.0"] } diff --git a/homeassistant/components/roborock/button.py b/homeassistant/components/roborock/button.py index dfeaba0026cc72..9e9c1c60669ea1 100644 --- a/homeassistant/components/roborock/button.py +++ b/homeassistant/components/roborock/button.py @@ -3,11 +3,13 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from dataclasses import dataclass import itertools import logging from typing import Any +from roborock.device_features import is_wash_n_fill_dock from roborock.devices.traits.v1.consumeable import ConsumableAttribute from roborock.exceptions import RoborockException from roborock.roborock_message import RoborockZeoProtocol @@ -43,6 +45,13 @@ class RoborockButtonDescription(ButtonEntityDescription): """Describes a Roborock button entity.""" attribute: ConsumableAttribute + is_dock_entity: bool = False + is_supported: Callable[[RoborockDataUpdateCoordinator], bool] = lambda _: True + + +def _supports_dock_consumables(coordinator: RoborockDataUpdateCoordinator) -> bool: + dock_type = coordinator.properties_api.status.dock_type + return dock_type is not None and is_wash_n_fill_dock(dock_type) CONSUMABLE_BUTTON_DESCRIPTIONS = [ @@ -74,6 +83,24 @@ class RoborockButtonDescription(ButtonEntityDescription): entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ), + RoborockButtonDescription( + key="reset_dock_strainer_consumable", + translation_key="reset_dock_strainer_consumable", + attribute=ConsumableAttribute.STRAINER_WORK_TIME, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + is_dock_entity=True, + is_supported=_supports_dock_consumables, + ), + RoborockButtonDescription( + key="reset_dock_cleaning_brush_consumable", + translation_key="reset_dock_cleaning_brush_consumable", + attribute=ConsumableAttribute.CLEANING_BRUSH_WORK_TIME, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + is_dock_entity=True, + is_supported=_supports_dock_consumables, + ), ] @@ -128,8 +155,9 @@ async def async_setup_entry( description, ) for coordinator in config_entry.runtime_data.v1 - for description in CONSUMABLE_BUTTON_DESCRIPTIONS if isinstance(coordinator, RoborockDataUpdateCoordinator) + for description in CONSUMABLE_BUTTON_DESCRIPTIONS + if description.is_supported(coordinator) ), ( RoborockRoutineButtonEntity( @@ -176,9 +204,14 @@ def __init__( entity_description: RoborockButtonDescription, ) -> None: """Create a button entity.""" + device_info = ( + coordinator.dock_device_info + if entity_description.is_dock_entity + else coordinator.device_info + ) super().__init__( f"{entity_description.key}_{coordinator.duid_slug}", - coordinator.device_info, + device_info, api=coordinator.properties_api.command, ) self.entity_description = entity_description diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 645cbbea0c39ac..ac8e9a3fe4a870 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -550,6 +550,7 @@ def __init__( RoborockB01Props.WIND, RoborockB01Props.WATER, RoborockB01Props.MODE, + RoborockB01Props.CLEAN_PATH_PREFERENCE, RoborockB01Props.QUANTITY, ] diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index e5c1c6e208184d..71018ee9e14e31 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -24,6 +24,12 @@ "reset_air_filter_consumable": { "default": "mdi:air-filter" }, + "reset_dock_cleaning_brush_consumable": { + "default": "mdi:brush" + }, + "reset_dock_strainer_consumable": { + "default": "mdi:filter" + }, "reset_main_brush_consumable": { "default": "mdi:brush" }, diff --git a/homeassistant/components/roborock/select.py b/homeassistant/components/roborock/select.py index 27305b7db2c794..2ba3d611bb353b 100644 --- a/homeassistant/components/roborock/select.py +++ b/homeassistant/components/roborock/select.py @@ -8,6 +8,7 @@ from roborock import B01Props, CleanTypeMapping from roborock.data import ( + CleanPathPreferenceMapping, RoborockDockDustCollectionModeCode, RoborockEnum, WaterLevelMapping, @@ -118,6 +119,16 @@ class RoborockSelectDescriptionA01(SelectEntityDescription): options_lambda=lambda _: list(CleanTypeMapping.keys()), entity_category=EntityCategory.CONFIG, ), + RoborockB01SelectDescription( + key="cleaning_route", + translation_key="cleaning_route", + api_fn=lambda api, value: api.set_clean_path_preference( + CleanPathPreferenceMapping.from_value(value) + ), + value_fn=lambda data: data.clean_path_preference_name, + options_lambda=lambda _: list(CleanPathPreferenceMapping.keys()), + entity_category=EntityCategory.CONFIG, + ), ] diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 2e688c64064eb0..dcd09fe973fa91 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -93,6 +93,12 @@ "reset_air_filter_consumable": { "name": "Reset air filter consumable" }, + "reset_dock_cleaning_brush_consumable": { + "name": "Reset cleaning brush consumable" + }, + "reset_dock_strainer_consumable": { + "name": "Reset strainer consumable" + }, "reset_main_brush_consumable": { "name": "Reset main brush consumable" }, @@ -123,6 +129,13 @@ "vacuum": "Vacuum only" } }, + "cleaning_route": { + "name": "Cleaning route", + "state": { + "balanced": "[%key:component::roborock::entity::vacuum::roborock::state_attributes::fan_speed::state::balanced%]", + "deep": "[%key:component::roborock::entity::select::mop_mode::state::deep%]" + } + }, "detergent_type": { "name": "Detergent type", "state": { diff --git a/requirements_all.txt b/requirements_all.txt index 669fb788f1a3d6..c06f513cc5f1b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -56,7 +56,7 @@ PyFlume==0.6.5 PyFronius==0.8.2 # homeassistant.components.pyload -PyLoadAPI==2.0.0 +PyLoadAPI==2.1.0 # homeassistant.components.met_eireann PyMetEireann==2024.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32b255e5801198..b5dd264893edda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -56,7 +56,7 @@ PyFlume==0.6.5 PyFronius==0.8.2 # homeassistant.components.pyload -PyLoadAPI==2.0.0 +PyLoadAPI==2.1.0 # homeassistant.components.met_eireann PyMetEireann==2024.11.0 diff --git a/tests/components/airly/snapshots/test_sensor.ambr b/tests/components/airly/snapshots/test_sensor.ambr index b70c89c3d78ebe..224543057ecfa9 100644 --- a/tests/components/airly/snapshots/test_sensor.ambr +++ b/tests/components/airly/snapshots/test_sensor.ambr @@ -29,14 +29,14 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Carbon monoxide', 'platform': 'airly', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'co', + 'translation_key': None, 'unique_id': '123-456-co', 'unit_of_measurement': 'μg/m³', }) @@ -45,6 +45,7 @@ StateSnapshot({ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Airly', + 'device_class': 'carbon_monoxide', 'friendly_name': 'Home Carbon monoxide', 'limit': 4000, 'percent': 4, diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index d574d495f8a2da..e1146639efbcdb 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -159,6 +159,7 @@ async def return_to_dock_side_effect(): b01_trait.find_me = AsyncMock() b01_trait.set_fan_speed = AsyncMock() b01_trait.set_mode = AsyncMock() + b01_trait.set_clean_path_preference = AsyncMock() b01_trait.set_water_level = AsyncMock() b01_trait.send = AsyncMock() return b01_trait diff --git a/tests/components/roborock/mock_data.py b/tests/components/roborock/mock_data.py index b62697fffc1d6f..e848800ab5eed6 100644 --- a/tests/components/roborock/mock_data.py +++ b/tests/components/roborock/mock_data.py @@ -5,6 +5,7 @@ from PIL import Image from roborock.data import ( B01Props, + CleanPathPreferenceMapping, CleanRecord, CleanSummary, Consumable, @@ -1558,6 +1559,7 @@ Q7_B01_PROPS = B01Props( status=WorkStatusMapping.SWEEP_MOPING, + clean_path_preference=CleanPathPreferenceMapping.BALANCED, main_brush=5000, side_brush=3000, hypa=1500, diff --git a/tests/components/roborock/snapshots/test_button.ambr b/tests/components/roborock/snapshots/test_button.ambr index 272e822965a0d7..9d376ab726722e 100644 --- a/tests/components/roborock/snapshots/test_button.ambr +++ b/tests/components/roborock/snapshots/test_button.ambr @@ -49,6 +49,106 @@ 'state': 'unknown', }) # --- +# name: test_buttons[button.roborock_s7_2_dock_reset_cleaning_brush_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.roborock_s7_2_dock_reset_cleaning_brush_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset cleaning brush consumable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset cleaning brush consumable', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_dock_cleaning_brush_consumable', + 'unique_id': 'reset_dock_cleaning_brush_consumable_device_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[button.roborock_s7_2_dock_reset_cleaning_brush_consumable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Dock Reset cleaning brush consumable', + }), + 'context': , + 'entity_id': 'button.roborock_s7_2_dock_reset_cleaning_brush_consumable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.roborock_s7_2_dock_reset_strainer_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.roborock_s7_2_dock_reset_strainer_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset strainer consumable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset strainer consumable', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_dock_strainer_consumable', + 'unique_id': 'reset_dock_strainer_consumable_device_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[button.roborock_s7_2_dock_reset_strainer_consumable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 2 Dock Reset strainer consumable', + }), + 'context': , + 'entity_id': 'button.roborock_s7_2_dock_reset_strainer_consumable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[button.roborock_s7_2_reset_air_filter_consumable-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -349,6 +449,106 @@ 'state': 'unknown', }) # --- +# name: test_buttons[button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset cleaning brush consumable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset cleaning brush consumable', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_dock_cleaning_brush_consumable', + 'unique_id': 'reset_dock_cleaning_brush_consumable_abc123', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 MaxV Dock Reset cleaning brush consumable', + }), + 'context': , + 'entity_id': 'button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_buttons[button.roborock_s7_maxv_dock_reset_strainer_consumable-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.roborock_s7_maxv_dock_reset_strainer_consumable', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset strainer consumable', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset strainer consumable', + 'platform': 'roborock', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_dock_strainer_consumable', + 'unique_id': 'reset_dock_strainer_consumable_abc123', + 'unit_of_measurement': None, + }) +# --- +# name: test_buttons[button.roborock_s7_maxv_dock_reset_strainer_consumable-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Roborock S7 MaxV Dock Reset strainer consumable', + }), + 'context': , + 'entity_id': 'button.roborock_s7_maxv_dock_reset_strainer_consumable', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_buttons[button.roborock_s7_maxv_reset_air_filter_consumable-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/roborock/test_button.py b/tests/components/roborock/test_button.py index fcbbff13fb02fc..d64366389b6a52 100644 --- a/tests/components/roborock/test_button.py +++ b/tests/components/roborock/test_button.py @@ -4,14 +4,17 @@ import pytest from roborock import RoborockException +from roborock.data import RoborockDockTypeCode +from roborock.devices.traits.v1.consumeable import ConsumableAttribute from roborock.exceptions import RoborockTimeout from syrupy.assertion import SnapshotAssertion from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.roborock.const import DOMAIN 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.helpers import device_registry as dr, entity_registry as er from .conftest import FakeDevice @@ -41,6 +44,46 @@ async def test_buttons( await snapshot_platform(hass, entity_registry, snapshot, setup_entry.entry_id) +@pytest.fixture +def non_wash_n_fill_dock(fake_vacuum: FakeDevice) -> None: + """Override dock_type to a non-wash-n-fill value so dock buttons are gated out.""" + status = fake_vacuum.v1_properties.status + original_refresh = status.refresh.side_effect + + async def patched_refresh() -> None: + await original_refresh() + status.dock_type = RoborockDockTypeCode.auto_empty_dock + + status.refresh.side_effect = patched_refresh + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_dock_buttons_absent_for_non_wash_n_fill_dock( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + non_wash_n_fill_dock: None, + setup_entry: MockConfigEntry, +) -> None: + """Dock consumable buttons must not be created when dock type is not wash-n-fill.""" + for entity_id in ( + "button.roborock_s7_maxv_dock_reset_strainer_consumable", + "button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable", + ): + assert hass.states.get(entity_id) is None + # Non-dock consumable buttons must still exist. + for entity_id in ( + "button.roborock_s7_maxv_reset_sensor_consumable", + "button.roborock_s7_maxv_reset_air_filter_consumable", + "button.roborock_s7_maxv_reset_side_brush_consumable", + "button.roborock_s7_maxv_reset_main_brush_consumable", + ): + assert hass.states.get(entity_id) is not None + # No phantom dock device should be registered for the non-wash-n-fill vacuum. + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, "abc123_dock")}) is None + ) + + @pytest.fixture(name="consumeables_trait", autouse=True) def consumeables_trait_fixture(fake_vacuum: FakeDevice) -> Mock: """Get the fake vacuum device command trait for asserting that commands happened.""" @@ -49,12 +92,32 @@ def consumeables_trait_fixture(fake_vacuum: FakeDevice) -> Mock: @pytest.mark.parametrize( - ("entity_id"), + ("entity_id", "expected_attribute"), [ - ("button.roborock_s7_maxv_reset_sensor_consumable"), - ("button.roborock_s7_maxv_reset_air_filter_consumable"), - ("button.roborock_s7_maxv_reset_side_brush_consumable"), - ("button.roborock_s7_maxv_reset_main_brush_consumable"), + ( + "button.roborock_s7_maxv_reset_sensor_consumable", + ConsumableAttribute.SENSOR_DIRTY_TIME, + ), + ( + "button.roborock_s7_maxv_reset_air_filter_consumable", + ConsumableAttribute.FILTER_WORK_TIME, + ), + ( + "button.roborock_s7_maxv_reset_side_brush_consumable", + ConsumableAttribute.SIDE_BRUSH_WORK_TIME, + ), + ( + "button.roborock_s7_maxv_reset_main_brush_consumable", + ConsumableAttribute.MAIN_BRUSH_WORK_TIME, + ), + ( + "button.roborock_s7_maxv_dock_reset_strainer_consumable", + ConsumableAttribute.STRAINER_WORK_TIME, + ), + ( + "button.roborock_s7_maxv_dock_reset_cleaning_brush_consumable", + ConsumableAttribute.CLEANING_BRUSH_WORK_TIME, + ), ], ) @pytest.mark.freeze_time("2023-10-30 08:50:00") @@ -64,6 +127,7 @@ async def test_update_success( bypass_api_client_fixture: None, setup_entry: MockConfigEntry, entity_id: str, + expected_attribute: ConsumableAttribute, consumeables_trait: Mock, ) -> None: """Test pressing the button entities.""" @@ -75,7 +139,7 @@ async def test_update_success( blocking=True, target={"entity_id": entity_id}, ) - assert consumeables_trait.reset_consumable.assert_called_once + consumeables_trait.reset_consumable.assert_called_once_with(expected_attribute) assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00" diff --git a/tests/components/roborock/test_select.py b/tests/components/roborock/test_select.py index 7290614fca02d8..d2d0b806529826 100644 --- a/tests/components/roborock/test_select.py +++ b/tests/components/roborock/test_select.py @@ -6,6 +6,7 @@ import pytest from roborock import CleanTypeMapping, RoborockCommand from roborock.data import ( + CleanPathPreferenceMapping, RoborockDockDustCollectionModeCode, WaterLevelMapping, ZeoProgram, @@ -267,6 +268,64 @@ async def test_update_failure_q7_cleaning_mode( ) +async def test_q7_cleaning_route_state( + hass: HomeAssistant, + setup_entry: MockConfigEntry, +) -> None: + """Test Q7 cleaning route select state and options.""" + entity_id = "select.roborock_q7_cleaning_route" + state = hass.states.get(entity_id) + + assert state is not None + assert state.state == "balanced" + assert state.attributes["options"] == ["balanced", "deep"] + + +async def test_update_failure_q7_cleaning_route( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + q7_device: FakeDevice, +) -> None: + """Test failure when setting Q7 cleaning route.""" + assert q7_device.b01_q7_properties + q7_device.b01_q7_properties.set_clean_path_preference.side_effect = ( + RoborockException + ) + + with pytest.raises(HomeAssistantError, match="Error while calling cleaning_route"): + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + service_data={"option": "deep"}, + blocking=True, + target={"entity_id": "select.roborock_q7_cleaning_route"}, + ) + + +async def test_update_success_q7_cleaning_route( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + q7_device: FakeDevice, +) -> None: + """Test allowed changing values for Q7 cleaning route select entity.""" + entity_id = "select.roborock_q7_cleaning_route" + assert hass.states.get(entity_id) is not None + + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + service_data={"option": "deep"}, + blocking=True, + target={"entity_id": entity_id}, + ) + + assert q7_device.b01_q7_properties + assert q7_device.b01_q7_properties.set_clean_path_preference.call_count == 1 + q7_device.b01_q7_properties.set_clean_path_preference.assert_called_with( + CleanPathPreferenceMapping.DEEP + ) + + async def test_update_success_q7_cleaning_mode( hass: HomeAssistant, setup_entry: MockConfigEntry,