1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add the ability to set Cleaning mode and mop mode for Q7 Vacs (#161725)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Luke Lashley
2026-01-28 09:34:41 -05:00
committed by GitHub
parent 0e98e8c893
commit d45ddd3762
5 changed files with 211 additions and 6 deletions
@@ -553,6 +553,8 @@ class RoborockB01Q7UpdateCoordinator(RoborockDataUpdateCoordinatorB01):
RoborockB01Props.REAL_CLEAN_TIME,
RoborockB01Props.HYPA,
RoborockB01Props.WIND,
RoborockB01Props.WATER,
RoborockB01Props.MODE,
]
async def _async_update_data(
+93 -4
View File
@@ -1,10 +1,13 @@
"""Support for Roborock select."""
import asyncio
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from roborock.data import RoborockDockDustCollectionModeCode
from roborock import B01Props, CleanTypeMapping
from roborock.data import RoborockDockDustCollectionModeCode, WaterLevelMapping
from roborock.devices.traits.b01 import Q7PropertiesApi
from roborock.devices.traits.v1 import PropertiesApi
from roborock.devices.traits.v1.home import HomeTrait
from roborock.devices.traits.v1.maps import MapsTrait
@@ -18,8 +21,12 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MAP_SLEEP
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
from .entity import RoborockCoordinatedEntityV1
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
)
from .entity import RoborockCoordinatedEntityB01, RoborockCoordinatedEntityV1
PARALLEL_UPDATES = 0
@@ -44,6 +51,42 @@ class RoborockSelectDescription(SelectEntityDescription):
"""Whether this entity is for the dock."""
@dataclass(frozen=True, kw_only=True)
class RoborockB01SelectDescription(SelectEntityDescription):
"""Class to describe a Roborock B01 select entity."""
api_fn: Callable[[Q7PropertiesApi, str], Awaitable[Any]]
"""Function to call the API."""
value_fn: Callable[[B01Props], str | None]
"""Function to get the current value of the select entity."""
options_lambda: Callable[[Q7PropertiesApi], list[str] | None]
"""Function to get all options of the select entity or returns None if not supported."""
B01_SELECT_DESCRIPTIONS: list[RoborockB01SelectDescription] = [
RoborockB01SelectDescription(
key="water_flow",
translation_key="water_flow",
api_fn=lambda api, value: api.set_water_level(
WaterLevelMapping.from_value(value)
),
value_fn=lambda data: data.water.value if data.water else None,
options_lambda=lambda _: [option.value for option in WaterLevelMapping],
entity_category=EntityCategory.CONFIG,
),
RoborockB01SelectDescription(
key="cleaning_mode",
translation_key="cleaning_mode",
api_fn=lambda api, value: api.set_mode(CleanTypeMapping.from_value(value)),
value_fn=lambda data: data.mode.value if data.mode else None,
options_lambda=lambda _: list(CleanTypeMapping.keys()),
entity_category=EntityCategory.CONFIG,
),
]
SELECT_DESCRIPTIONS: list[RoborockSelectDescription] = [
RoborockSelectDescription(
key="water_box_mode",
@@ -114,6 +157,52 @@ async def async_setup_entry(
if (home_trait := coordinator.properties_api.home) is not None
if (map_trait := coordinator.properties_api.maps) is not None
)
async_add_entities(
RoborockB01SelectEntity(coordinator, description, options)
for coordinator in config_entry.runtime_data.b01
for description in B01_SELECT_DESCRIPTIONS
if isinstance(coordinator, RoborockB01Q7UpdateCoordinator)
if (options := description.options_lambda(coordinator.api)) is not None
)
class RoborockB01SelectEntity(RoborockCoordinatedEntityB01, SelectEntity):
"""Select entity for Roborock B01 devices."""
entity_description: RoborockB01SelectDescription
coordinator: RoborockB01Q7UpdateCoordinator
def __init__(
self,
coordinator: RoborockB01Q7UpdateCoordinator,
entity_description: RoborockB01SelectDescription,
options: list[str],
) -> None:
"""Initialize the entity."""
self.entity_description = entity_description
super().__init__(
f"{entity_description.key}_{coordinator.duid_slug}", coordinator
)
self._attr_options = options
async def async_select_option(self, option: str) -> None:
"""Set the option."""
try:
await self.entity_description.api_fn(self.coordinator.api, option)
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={
"command": self.entity_description.key,
},
) from err
await self.coordinator.async_refresh()
@property
def current_option(self) -> str | None:
"""Return the current option."""
return self.entity_description.value_fn(self.coordinator.data)
class RoborockSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
@@ -86,6 +86,14 @@
}
},
"select": {
"cleaning_mode": {
"name": "Cleaning mode",
"state": {
"mop": "Mop only",
"vac_and_mop": "Vacuum and mop",
"vacuum": "Vacuum only"
}
},
"dust_collection_mode": {
"name": "Empty mode",
"state": {
@@ -126,6 +134,14 @@
},
"selected_map": {
"name": "Selected map"
},
"water_flow": {
"name": "Water flow",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]"
}
}
},
"sensor": {
+2
View File
@@ -137,6 +137,8 @@ def create_b01_q7_trait() -> Mock:
b01_trait.return_to_dock = AsyncMock(side_effect=return_to_dock_side_effect)
b01_trait.find_me = AsyncMock()
b01_trait.set_fan_speed = AsyncMock()
b01_trait.set_mode = AsyncMock()
b01_trait.set_water_level = AsyncMock()
b01_trait.send = AsyncMock()
return b01_trait
+98 -2
View File
@@ -4,8 +4,8 @@ from typing import Any
from unittest.mock import AsyncMock, call
import pytest
from roborock import RoborockCommand
from roborock.data.v1 import RoborockDockDustCollectionModeCode
from roborock import CleanTypeMapping, RoborockCommand
from roborock.data import RoborockDockDustCollectionModeCode, WaterLevelMapping
from roborock.exceptions import RoborockException
from homeassistant.components.roborock import DOMAIN
@@ -182,3 +182,99 @@ async def test_dust_collection_mode_none(
select_entity = hass.states.get("select.roborock_s7_maxv_dock_empty_mode")
assert select_entity
assert select_entity.state == expected_state
@pytest.fixture
def q7_device(fake_devices: list[FakeDevice]) -> FakeDevice:
"""Get the fake Q7 vacuum device."""
# The Q7 is the fourth device in the list (index 3) based on HOME_DATA
return fake_devices[3]
async def test_update_success_q7_water_level(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q7_device: FakeDevice,
) -> None:
"""Test allowed changing values for Q7 water flow select entity."""
entity_id = "select.roborock_q7_water_flow"
assert hass.states.get(entity_id) is not None
# Test setting value
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "high"},
blocking=True,
target={"entity_id": entity_id},
)
assert q7_device.b01_q7_properties
assert q7_device.b01_q7_properties.set_water_level.call_count == 1
q7_device.b01_q7_properties.set_water_level.assert_called_with(
WaterLevelMapping.HIGH
)
async def test_update_failure_q7_water_level(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q7_device: FakeDevice,
) -> None:
"""Test failure when setting Q7 water flow."""
assert q7_device.b01_q7_properties
q7_device.b01_q7_properties.set_water_level.side_effect = RoborockException
entity_id = "select.roborock_q7_water_flow"
assert hass.states.get(entity_id) is not None
with pytest.raises(HomeAssistantError, match="Error while calling water_flow"):
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "high"},
blocking=True,
target={"entity_id": entity_id},
)
async def test_update_failure_q7_cleaning_mode(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q7_device: FakeDevice,
) -> None:
"""Test failure when setting Q7 cleaning mode."""
assert q7_device.b01_q7_properties
q7_device.b01_q7_properties.set_mode.side_effect = RoborockException
with pytest.raises(HomeAssistantError, match="Error while calling cleaning_mode"):
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "vacuum"},
blocking=True,
target={"entity_id": "select.roborock_q7_cleaning_mode"},
)
async def test_update_success_q7_cleaning_mode(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q7_device: FakeDevice,
) -> None:
"""Test allowed changing values for Q7 cleaning mode select entity."""
entity_id = "select.roborock_q7_cleaning_mode"
assert hass.states.get(entity_id) is not None
# Test setting value
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "vacuum"},
blocking=True,
target={"entity_id": entity_id},
)
assert q7_device.b01_q7_properties
assert q7_device.b01_q7_properties.set_mode.call_count == 1
q7_device.b01_q7_properties.set_mode.assert_called_with(CleanTypeMapping.VACUUM)