mirror of
https://github.com/home-assistant/core.git
synced 2025-07-07 21:37:07 +00:00
Roborock Add vacuum_goto service (#133994)
* Roborock Add vacuum_goto service to control vacuum movement to specified coordinates * roborock Add type specification for x_coord and y_coord in vacuum_goto service * roborock Add get_current_position service to retrieve vacuum's current coordinates * Rename vacuum services for clarity and consistency * Apply suggestions from code review Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Add integration field to vacuum service targets for Roborock --------- Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
parent
9840785363
commit
b2a160d926
@ -49,3 +49,5 @@ IMAGE_CACHE_INTERVAL = 90
|
||||
MAP_SLEEP = 3
|
||||
|
||||
GET_MAPS_SERVICE_NAME = "get_maps"
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME = "set_vacuum_goto_position"
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME = "get_vacuum_current_position"
|
||||
|
@ -121,6 +121,12 @@
|
||||
"services": {
|
||||
"get_maps": {
|
||||
"service": "mdi:floor-plan"
|
||||
},
|
||||
"set_vacuum_goto_position": {
|
||||
"service": "mdi:map-marker"
|
||||
},
|
||||
"get_vacuum_current_position": {
|
||||
"service": "mdi:map-marker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,28 @@
|
||||
get_maps:
|
||||
target:
|
||||
entity:
|
||||
integration: roborock
|
||||
domain: vacuum
|
||||
set_vacuum_goto_position:
|
||||
target:
|
||||
entity:
|
||||
integration: roborock
|
||||
domain: vacuum
|
||||
fields:
|
||||
x_coord:
|
||||
example: 27500
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
type: number
|
||||
y_coord:
|
||||
example: 32000
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
type: number
|
||||
get_vacuum_current_position:
|
||||
target:
|
||||
entity:
|
||||
integration: roborock
|
||||
domain: vacuum
|
||||
|
@ -428,6 +428,24 @@
|
||||
"get_maps": {
|
||||
"name": "Get maps",
|
||||
"description": "Get the map and room information of your device."
|
||||
},
|
||||
"set_vacuum_goto_position": {
|
||||
"name": "Go to position",
|
||||
"description": "Send the vacuum to a specific position.",
|
||||
"fields": {
|
||||
"x_coord": {
|
||||
"name": "X-coordinate",
|
||||
"description": ""
|
||||
},
|
||||
"y_coord": {
|
||||
"name": "Y-coordinate",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_vacuum_current_position": {
|
||||
"name": "Get current position",
|
||||
"description": "Get the current position of the vacuum."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ from typing import Any
|
||||
from roborock.code_mappings import RoborockStateCode
|
||||
from roborock.roborock_message import RoborockDataProtocol
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
StateVacuumEntity,
|
||||
@ -13,13 +14,20 @@ from homeassistant.components.vacuum import (
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||
from homeassistant.helpers import entity_platform
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import RoborockConfigEntry
|
||||
from .const import DOMAIN, GET_MAPS_SERVICE_NAME
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
GET_MAPS_SERVICE_NAME,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
)
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockCoordinatedEntityV1
|
||||
from .image import ColorsPalette, ImageConfig, RoborockMapDataParser, Sizes
|
||||
|
||||
STATE_CODE_TO_STATE = {
|
||||
RoborockStateCode.starting: VacuumActivity.IDLE, # "Starting"
|
||||
@ -69,6 +77,25 @@ async def async_setup_entry(
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
None,
|
||||
RoborockVacuum.get_vacuum_current_position.__name__,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
platform.async_register_entity_service(
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Required("x_coord"): vol.Coerce(int),
|
||||
vol.Required("y_coord"): vol.Coerce(int),
|
||||
},
|
||||
),
|
||||
RoborockVacuum.async_set_vacuum_goto_position.__name__,
|
||||
supports_response=SupportsResponse.NONE,
|
||||
)
|
||||
|
||||
|
||||
class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
"""General Representation of a Roborock vacuum."""
|
||||
@ -158,6 +185,10 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
[self._device_status.get_fan_speed_code(fan_speed)],
|
||||
)
|
||||
|
||||
async def async_set_vacuum_goto_position(self, x_coord: int, y_coord: int) -> None:
|
||||
"""Send vacuum to a specific target point."""
|
||||
await self.send(RoborockCommand.APP_GOTO_TARGET, [x_coord, y_coord])
|
||||
|
||||
async def async_send_command(
|
||||
self,
|
||||
command: str,
|
||||
@ -174,3 +205,21 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
asdict(vacuum_map) for vacuum_map in self.coordinator.maps.values()
|
||||
]
|
||||
}
|
||||
|
||||
async def get_vacuum_current_position(self) -> ServiceResponse:
|
||||
"""Get the current position of the vacuum from the map."""
|
||||
|
||||
map_data = await self.coordinator.cloud_api.get_map_v1()
|
||||
if not isinstance(map_data, bytes):
|
||||
raise HomeAssistantError("Failed to retrieve map data.")
|
||||
parser = RoborockMapDataParser(ColorsPalette(), Sizes(), [], ImageConfig(), [])
|
||||
parsed_map = parser.parse(map_data)
|
||||
robot_position = parsed_map.vacuum_position
|
||||
|
||||
if robot_position is None:
|
||||
raise HomeAssistantError("Robot position not found")
|
||||
|
||||
return {
|
||||
"x": robot_position.x,
|
||||
"y": robot_position.y,
|
||||
}
|
||||
|
@ -8,9 +8,14 @@ import pytest
|
||||
from roborock import RoborockException
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from vacuum_map_parser_base.map_data import Point
|
||||
|
||||
from homeassistant.components.roborock import DOMAIN
|
||||
from homeassistant.components.roborock.const import GET_MAPS_SERVICE_NAME
|
||||
from homeassistant.components.roborock.const import (
|
||||
GET_MAPS_SERVICE_NAME,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
)
|
||||
from homeassistant.components.vacuum import (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
@ -27,7 +32,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .mock_data import PROP
|
||||
from .mock_data import MAP_DATA, PROP
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -181,3 +186,112 @@ async def test_get_maps(
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
async def test_goto(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test sending the vacuum to specific coordinates."""
|
||||
vacuum = hass.states.get(ENTITY_ID)
|
||||
assert vacuum
|
||||
|
||||
data = {ATTR_ENTITY_ID: ENTITY_ID, "x_coord": 25500, "y_coord": 25500}
|
||||
with patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockLocalClientV1.send_command"
|
||||
) as mock_send_command:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SET_VACUUM_GOTO_POSITION_SERVICE_NAME,
|
||||
data,
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_send_command.call_count == 1
|
||||
assert mock_send_command.call_args[0][0] == RoborockCommand.APP_GOTO_TARGET
|
||||
assert mock_send_command.call_args[0][1] == [25500, 25500]
|
||||
|
||||
|
||||
async def test_get_current_position(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test that the service for getting the current position outputs the correct coordinates."""
|
||||
map_data = copy.deepcopy(MAP_DATA)
|
||||
map_data.vacuum_position = Point(x=123, y=456)
|
||||
map_data.image = None
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1",
|
||||
return_value=b"",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.image.RoborockMapDataParser.parse",
|
||||
return_value=map_data,
|
||||
),
|
||||
):
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == {
|
||||
"vacuum.roborock_s7_maxv": {
|
||||
"x": 123,
|
||||
"y": 456,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_get_current_position_no_map_data(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service for getting the current position handles no map data error."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1",
|
||||
return_value=None,
|
||||
),
|
||||
pytest.raises(HomeAssistantError, match="Failed to retrieve map data."),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_get_current_position_no_robot_position(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service for getting the current position handles no robot position error."""
|
||||
map_data = copy.deepcopy(MAP_DATA)
|
||||
map_data.vacuum_position = None
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1",
|
||||
return_value=b"",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.image.RoborockMapDataParser.parse",
|
||||
return_value=map_data,
|
||||
),
|
||||
pytest.raises(HomeAssistantError, match="Robot position not found"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
GET_VACUUM_CURRENT_POSITION_SERVICE_NAME,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user