mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 22:07:10 +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
|
MAP_SLEEP = 3
|
||||||
|
|
||||||
GET_MAPS_SERVICE_NAME = "get_maps"
|
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": {
|
"services": {
|
||||||
"get_maps": {
|
"get_maps": {
|
||||||
"service": "mdi:floor-plan"
|
"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:
|
get_maps:
|
||||||
target:
|
target:
|
||||||
entity:
|
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
|
domain: vacuum
|
||||||
|
@ -428,6 +428,24 @@
|
|||||||
"get_maps": {
|
"get_maps": {
|
||||||
"name": "Get maps",
|
"name": "Get maps",
|
||||||
"description": "Get the map and room information of your device."
|
"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.code_mappings import RoborockStateCode
|
||||||
from roborock.roborock_message import RoborockDataProtocol
|
from roborock.roborock_message import RoborockDataProtocol
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
@ -13,13 +14,20 @@ from homeassistant.components.vacuum import (
|
|||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
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 homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import RoborockConfigEntry
|
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 .coordinator import RoborockDataUpdateCoordinator
|
||||||
from .entity import RoborockCoordinatedEntityV1
|
from .entity import RoborockCoordinatedEntityV1
|
||||||
|
from .image import ColorsPalette, ImageConfig, RoborockMapDataParser, Sizes
|
||||||
|
|
||||||
STATE_CODE_TO_STATE = {
|
STATE_CODE_TO_STATE = {
|
||||||
RoborockStateCode.starting: VacuumActivity.IDLE, # "Starting"
|
RoborockStateCode.starting: VacuumActivity.IDLE, # "Starting"
|
||||||
@ -69,6 +77,25 @@ async def async_setup_entry(
|
|||||||
supports_response=SupportsResponse.ONLY,
|
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):
|
class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||||
"""General Representation of a Roborock vacuum."""
|
"""General Representation of a Roborock vacuum."""
|
||||||
@ -158,6 +185,10 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
|||||||
[self._device_status.get_fan_speed_code(fan_speed)],
|
[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(
|
async def async_send_command(
|
||||||
self,
|
self,
|
||||||
command: str,
|
command: str,
|
||||||
@ -174,3 +205,21 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
|||||||
asdict(vacuum_map) for vacuum_map in self.coordinator.maps.values()
|
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 import RoborockException
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
from vacuum_map_parser_base.map_data import Point
|
||||||
|
|
||||||
from homeassistant.components.roborock import DOMAIN
|
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 (
|
from homeassistant.components.vacuum import (
|
||||||
SERVICE_CLEAN_SPOT,
|
SERVICE_CLEAN_SPOT,
|
||||||
SERVICE_LOCATE,
|
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.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .mock_data import PROP
|
from .mock_data import MAP_DATA, PROP
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -181,3 +186,112 @@ async def test_get_maps(
|
|||||||
return_response=True,
|
return_response=True,
|
||||||
)
|
)
|
||||||
assert response == snapshot
|
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