Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0852ecb
feat(roborock): Add Q10 S5+ cleaning mode select entity
lboue Mar 21, 2026
5a4f435
test(roborock): Add Q10 cleaning mode select entity tests
lboue Mar 21, 2026
213b328
i18n(roborock): Add Q10 cleaning mode translations
lboue Mar 21, 2026
15f58ca
refactor(roborock): Map Q10 cleaning modes to existing Q7 state keys
lboue Mar 21, 2026
d5af70e
fix(roborock): Use Q10 status trait for cleaning mode select state
lboue Mar 21, 2026
dd439b6
fix(roborock): Update Q10 cleaning mode select test to include B01_Q1…
lboue Mar 21, 2026
d059c53
fix(roborock): Import YXCleanType for Q10 cleaning mode select updates
lboue Mar 21, 2026
8452734
fix(roborock): Remove unused import of B01_Q10_DP from select.py
lboue Mar 21, 2026
9e5435b
fix(roborock): Update Q10 cleaning mode select test to assert specifi…
lboue Mar 21, 2026
b5fc23b
refactor(roborock): Rename q10_device fixture to fake_q10_vacuum and …
lboue Mar 21, 2026
e4b86e7
fix(roborock): Update async_select_option to raise ServiceValidationE…
lboue Mar 21, 2026
87b3d7b
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Mar 21, 2026
3368ac0
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Mar 22, 2026
c23443e
Refactor Q10 clean mode mapping to use a constant dictionary
lboue Mar 22, 2026
9ef32f3
Fix assertion in Q10 cleaning mode test to check for non-unknown state
lboue Mar 22, 2026
2530186
Refactor Q10 clean mode mapping to use dictionaries for improved clar…
lboue Mar 22, 2026
8565395
Update Q10 cleaning mode test to use YXCleanType for improved clarity
lboue Mar 22, 2026
c29fd2c
Add test for handling invalid cleaning mode selection in Q10
lboue Mar 22, 2026
0261b6c
Remove unnecessary async refresh call in Q10 clean mode selection
lboue Mar 22, 2026
9057aa9
Add assertions to validate Q10 cleaning mode options in test
lboue Mar 22, 2026
9c372a2
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Mar 22, 2026
bd1c87d
Update cleaning mode selection for Q10 vacuum to use VAC_AND_MOP
lboue Mar 22, 2026
77b9243
Remove UNKNOWN option from Q10 clean mode selection
lboue Mar 22, 2026
babc3ae
Refactor Q10 cleaning mode handling to remove legacy mappings and sim…
lboue Mar 23, 2026
f794471
Refactor Q10 invalid option test to use hass.services.async_call
Luligu Apr 3, 2026
54f109c
Assert initial state before update in Q10 cleaning mode test
Luligu Apr 3, 2026
c6117e6
Remove redundant STATE_UNKNOWN assertion before final state check
Luligu Apr 3, 2026
04c647b
Remove redundant duplicate check in Q10 options assertion
Luligu Apr 3, 2026
4e0c11c
Simplify Q10 current_option by using clean_mode.value directly
Luligu Apr 3, 2026
7e5c79a
Fix Q10 current_option using from_code_optional to map YXDeviceWorkMo…
Luligu Apr 3, 2026
fa47cdb
Use user-facing entity key in Q10 clean mode error placeholder
Luligu Apr 3, 2026
dcaa3de
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Apr 3, 2026
be6b888
Remove comment from options property in RoborockQ10CleanModeSelectEntity
Luligu Apr 3, 2026
36f0d70
Simplify Q10 async_select_option using generator expression
Luligu Apr 3, 2026
a5e0f5b
Use from_value in Q10 async_select_option for clearer option lookup
Luligu Apr 3, 2026
4eaf6c0
Simplify Q10 current_option now that clean_mode is typed as YXCleanType
Luligu Apr 6, 2026
6395628
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Apr 6, 2026
8f83505
Merge branch 'dev' into feat/roborock-q10-s5-plus-vacuum-select
lboue Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions homeassistant/components/roborock/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ZeoSpin,
ZeoTemperature,
)
from roborock.data.b01_q10.b01_q10_code_mappings import YXCleanType
from roborock.devices.traits.b01 import Q7PropertiesApi
from roborock.devices.traits.v1 import PropertiesApi
from roborock.devices.traits.v1.home import HomeTrait
Expand All @@ -37,13 +38,15 @@
from .const import DOMAIN, MAP_SLEEP
from .coordinator import (
RoborockB01Q7UpdateCoordinator,
RoborockB01Q10UpdateCoordinator,
RoborockConfigEntry,
RoborockDataUpdateCoordinator,
RoborockDataUpdateCoordinatorA01,
)
from .entity import (
RoborockCoordinatedEntityA01,
RoborockCoordinatedEntityB01Q7,
RoborockCoordinatedEntityB01Q10,
RoborockCoordinatedEntityV1,
)

Expand Down Expand Up @@ -266,6 +269,10 @@ async def async_setup_entry(
for description in A01_SELECT_DESCRIPTIONS
if description.data_protocol in coordinator.request_protocols
)
async_add_entities(
RoborockQ10CleanModeSelectEntity(coordinator)
for coordinator in config_entry.runtime_data.b01_q10
)


class RoborockB01SelectEntity(RoborockCoordinatedEntityB01Q7, SelectEntity):
Expand Down Expand Up @@ -466,3 +473,59 @@ def current_option(self) -> str | None:
self.entity_description.key,
)
return str(current_value)


class RoborockQ10CleanModeSelectEntity(RoborockCoordinatedEntityB01Q10, SelectEntity):
"""Select entity for Q10 cleaning mode."""

_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "cleaning_mode"
coordinator: RoborockB01Q10UpdateCoordinator

def __init__(
self,
coordinator: RoborockB01Q10UpdateCoordinator,
) -> None:
"""Create a select entity for Q10 cleaning mode."""
super().__init__(
f"cleaning_mode_{coordinator.duid_slug}",
coordinator,
)

async def async_added_to_hass(self) -> None:
"""Register trait listener for push-based status updates."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.api.status.add_update_listener(self.async_write_ha_state)
)

@property
def options(self) -> list[str]:
"""Return available cleaning modes."""
return [mode.value for mode in YXCleanType if mode != YXCleanType.UNKNOWN]

Comment thread
lboue marked this conversation as resolved.
Comment thread
lboue marked this conversation as resolved.
Comment thread
lboue marked this conversation as resolved.
@property
def current_option(self) -> str | None:
"""Get the current cleaning mode."""
clean_mode = self.coordinator.api.status.clean_mode
if clean_mode is None or clean_mode == YXCleanType.UNKNOWN:
return None
return clean_mode.value

async def async_select_option(self, option: str) -> None:
"""Set the cleaning mode."""
try:
mode = YXCleanType.from_value(option)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="select_option_failed",
) from err
try:
await self.coordinator.api.vacuum.set_clean_mode(mode)
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"command": "cleaning_mode"},
) from err
96 changes: 96 additions & 0 deletions tests/components/roborock/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
WaterLevelMapping,
ZeoProgram,
)
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXCleanType
from roborock.exceptions import RoborockException
from roborock.roborock_message import RoborockZeoProtocol

Expand Down Expand Up @@ -383,3 +384,98 @@ async def test_update_failure_zeo_invalid_option() -> None:
await entity.async_select_option("invalid_option")

coordinator.api.set_value.assert_not_called()


async def test_q10_cleaning_mode_select_current_option(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test Q10 cleaning mode select entity current option."""
entity_id = "select.roborock_q10_s5_cleaning_mode"
state = hass.states.get(entity_id)
assert state is not None
Comment thread
allenporter marked this conversation as resolved.
assert state.state == STATE_UNKNOWN
options = state.attributes.get("options")
assert options is not None
Comment thread
lboue marked this conversation as resolved.
assert set(options) == {"vac_and_mop", "vacuum", "mop"}

assert fake_q10_vacuum.b01_q10_properties
fake_q10_vacuum.b01_q10_properties.status.update_from_dps(
{B01_Q10_DP.CLEAN_MODE: YXCleanType.VAC_AND_MOP.code}
)
await hass.async_block_till_done()

updated_state = hass.states.get(entity_id)
assert updated_state is not None
assert updated_state.state == "vac_and_mop"


async def test_q10_cleaning_mode_select_update_success(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test setting Q10 cleaning mode select option."""
entity_id = "select.roborock_q10_s5_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": "vac_and_mop"},
blocking=True,
target={"entity_id": entity_id},
)

assert fake_q10_vacuum.b01_q10_properties
assert fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.call_count == 1
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.assert_called_once_with(
YXCleanType.VAC_AND_MOP
)


async def test_q10_cleaning_mode_select_update_failure(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test failure when setting Q10 cleaning mode."""
assert fake_q10_vacuum.b01_q10_properties
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.side_effect = (
RoborockException
)
entity_id = "select.roborock_q10_s5_cleaning_mode"
assert hass.states.get(entity_id) is not None

with pytest.raises(HomeAssistantError, match="cleaning_mode"):
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "vac_and_mop"},
blocking=True,
target={"entity_id": entity_id},
)


async def test_q10_cleaning_mode_select_invalid_option(
Comment thread
lboue marked this conversation as resolved.
hass: HomeAssistant,
setup_entry: MockConfigEntry,
fake_q10_vacuum: FakeDevice,
) -> None:
"""Test that an invalid option raises ServiceValidationError and does not call set_clean_mode."""
entity_id = "select.roborock_q10_s5_cleaning_mode"
assert hass.states.get(entity_id) is not None

with pytest.raises(ServiceValidationError):
await hass.services.async_call(
"select",
SERVICE_SELECT_OPTION,
service_data={"option": "invalid_option"},
blocking=True,
target={"entity_id": entity_id},
)

assert fake_q10_vacuum.b01_q10_properties
fake_q10_vacuum.b01_q10_properties.vacuum.set_clean_mode.assert_not_called()
Loading