Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions homeassistant/components/roborock/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,12 @@
"invalid_fan_speed": {
"message": "Invalid fan speed: {fan_speed}"
},
"invalid_q10_coordinate": {
"message": "Coordinates ({x}, {y}) are outside the range supported by this vacuum"
},
"invalid_q10_zone": {
"message": "The zone coordinates are outside the range supported by this vacuum"
},
"invalid_user_agreement": {
"message": "User agreement must be accepted again. Open your Roborock app and accept the agreement."
},
Expand Down
47 changes: 43 additions & 4 deletions homeassistant/components/roborock/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,21 +769,60 @@
"command": command,
},
) from err
self.coordinator.api.vacuum.cancel_goto()

Check failure on line 772 in homeassistant/components/roborock/vacuum.py

View workflow job for this annotation

GitHub Actions / Check mypy

"VacuumTrait" has no attribute "cancel_goto" [attr-defined]

async def get_maps(self) -> ServiceResponse:
"""Get map information such as map id and room ids."""
raise ServiceNotSupported(DOMAIN, "get_maps", self.entity_id)

async def get_vacuum_current_position(self) -> ServiceResponse:
"""Get the current position of the vacuum from the map."""
raise ServiceNotSupported(DOMAIN, "get_vacuum_current_position", self.entity_id)
if (position := self.coordinator.api.map.roborock_position) is None:

Check failure on line 780 in homeassistant/components/roborock/vacuum.py

View workflow job for this annotation

GitHub Actions / Check mypy

"MapContentTrait" has no attribute "roborock_position" [attr-defined]
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="position_not_found",
)
return {"x": position.x, "y": position.y}

async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
Comment thread
hmmbob marked this conversation as resolved.
"""Set the vacuum to go to a specific position."""
raise ServiceNotSupported(DOMAIN, "set_vacuum_goto_position", self.entity_id)
"""Move the Q10 to a position using the library goto operation."""
try:
await self.coordinator.api.vacuum.goto_position(x, y)

Check failure on line 790 in homeassistant/components/roborock/vacuum.py

View workflow job for this annotation

GitHub Actions / Check mypy

"VacuumTrait" has no attribute "goto_position" [attr-defined]
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_q10_coordinate",
translation_placeholders={"x": str(x), "y": str(y)},
) from err
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"command": "set_vacuum_goto_position"},
) from err

async def async_set_vacuum_zoned_cleaning(
self, x1: int, y1: int, x2: int, y2: int, repeats: int
) -> None:
"""Clean the specified zone."""
raise ServiceNotSupported(DOMAIN, "set_vacuum_zoned_cleaning", self.entity_id)
try:
# Home Assistant defines repeats as additional passes, while Q10
# carries the total clean count.
await self.coordinator.api.vacuum.clean_zone(

Check failure on line 811 in homeassistant/components/roborock/vacuum.py

View workflow job for this annotation

GitHub Actions / Check mypy

"VacuumTrait" has no attribute "clean_zone" [attr-defined]
x1,
y1,
x2,
y2,
clean_count=repeats + 1,
)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_q10_zone",
) from err
except RoborockException as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_failed",
translation_placeholders={"command": "set_vacuum_zoned_cleaning"},
) from err
151 changes: 148 additions & 3 deletions tests/components/roborock/test_vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from roborock import RoborockException
from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP, YXFanLevel
from roborock.map.b01_q10_map_parser import Q10Point
from roborock.roborock_typing import RoborockCommand
from syrupy.assertion import SnapshotAssertion
from vacuum_map_parser_base.map_data import Point
Expand Down Expand Up @@ -298,7 +299,6 @@ async def test_goto(
"entity_id",
[
Q7_ENTITY_ID,
Q10_ENTITY_ID,
],
)
async def test_goto_not_supported(
Expand Down Expand Up @@ -348,7 +348,6 @@ async def test_zoned_cleaning(
"entity_id",
[
Q7_ENTITY_ID,
Q10_ENTITY_ID,
],
)
async def test_zoned_cleaning_not_supported(
Expand Down Expand Up @@ -446,7 +445,6 @@ async def test_get_current_position_no_robot_position(
"entity_id",
[
Q7_ENTITY_ID,
Q10_ENTITY_ID,
],
)
async def test_get_current_position_not_supported(
Expand Down Expand Up @@ -905,10 +903,155 @@ def fake_q10_vacuum_api_fixture(
api.vacuum.return_to_dock.side_effect = send_message_exception
api.vacuum.set_fan_level.side_effect = send_message_exception
api.vacuum.clean_segments.side_effect = send_message_exception
api.vacuum.clean_zone.side_effect = send_message_exception
api.vacuum.goto_position.side_effect = send_message_exception
api.command.send.side_effect = send_message_exception
return api


async def test_q10_get_current_position(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q10_vacuum_api: Mock,
) -> None:
"""Test returning the Q10 position in common Roborock coordinates."""
q10_vacuum_api.map.roborock_position = Q10Point(x=30020, y=28705)

response = await hass.services.async_call(
DOMAIN,
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
{ATTR_ENTITY_ID: Q10_ENTITY_ID},
blocking=True,
return_response=True,
)

assert response == {Q10_ENTITY_ID: {"x": 30020, "y": 28705}}


async def test_q10_get_current_position_not_available(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q10_vacuum_api: Mock,
) -> None:
"""Test the Q10 position service before a trace has been received."""
q10_vacuum_api.map.roborock_position = None

with pytest.raises(HomeAssistantError, match="Robot position not found"):
await hass.services.async_call(
DOMAIN,
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
{ATTR_ENTITY_ID: Q10_ENTITY_ID},
blocking=True,
return_response=True,
)


async def test_q10_zoned_cleaning(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q10_vacuum_api: Mock,
) -> None:
"""Test that Q10 zoned cleaning uses the native zone task."""
await hass.services.async_call(
DOMAIN,
SET_VACUUM_ZONED_CLEANING_SERVICE_NAME,
{
ATTR_ENTITY_ID: Q10_ENTITY_ID,
"x1": 28582,
"y1": 21363,
"x2": 27425,
"y2": 22816,
"repeats": 1,
},
blocking=True,
)

assert q10_vacuum_api.vacuum.clean_zone.call_args == call(
28582,
21363,
27425,
22816,
clean_count=2,
)


async def test_q10_goto(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q10_vacuum_api: Mock,
) -> None:
"""Test that Q10 goto delegates the complete operation to the library."""
q10_vacuum_api.map.roborock_position = Q10Point(x=25500, y=25500)

await hass.services.async_call(
DOMAIN,
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
{ATTR_ENTITY_ID: Q10_ENTITY_ID, "x": 29900, "y": 28650},
blocking=True,
)

assert q10_vacuum_api.vacuum.goto_position.call_args == call(29900, 28650)


@pytest.mark.parametrize(
("service", "service_data", "api_method", "error", "expected_exception"),
[
pytest.param(
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
{"x": 29900, "y": 28650},
"goto_position",
ValueError("invalid coordinate"),
ServiceValidationError,
id="goto-validation",
),
pytest.param(
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
{"x": 29900, "y": 28650},
"goto_position",
RoborockException(),
HomeAssistantError,
id="goto-command",
),
pytest.param(
SET_VACUUM_ZONED_CLEANING_SERVICE_NAME,
{"x1": 28582, "y1": 21363, "x2": 27425, "y2": 22816, "repeats": 1},
"clean_zone",
ValueError("invalid zone"),
ServiceValidationError,
id="zone-validation",
),
pytest.param(
SET_VACUUM_ZONED_CLEANING_SERVICE_NAME,
{"x1": 28582, "y1": 21363, "x2": 27425, "y2": 22816, "repeats": 1},
"clean_zone",
RoborockException(),
HomeAssistantError,
id="zone-command",
),
],
)
async def test_q10_targeted_action_errors(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
q10_vacuum_api: Mock,
service: str,
service_data: dict[str, Any],
api_method: str,
error: Exception,
expected_exception: type[Exception],
) -> None:
"""Test validation and command failures for Q10 targeted actions."""
getattr(q10_vacuum_api.vacuum, api_method).side_effect = error

with pytest.raises(expected_exception):
await hass.services.async_call(
DOMAIN,
service,
{ATTR_ENTITY_ID: Q10_ENTITY_ID, **service_data},
blocking=True,
)


async def test_q10_registry_entries(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
Expand Down Expand Up @@ -1021,6 +1164,7 @@ async def test_q10_send_command(
blocking=True,
)
assert q10_vacuum_api.command.send.call_count == 1
q10_vacuum_api.vacuum.cancel_goto.assert_called_once_with()


async def test_q10_send_command_invalid(
Expand All @@ -1039,6 +1183,7 @@ async def test_q10_send_command_invalid(
{ATTR_ENTITY_ID: Q10_ENTITY_ID, "command": "INVALID_COMMAND"},
blocking=True,
)
q10_vacuum_api.vacuum.cancel_goto.assert_not_called()


@pytest.mark.parametrize(
Expand Down
Loading