diff --git a/homeassistant/components/ecovacs/icons.json b/homeassistant/components/ecovacs/icons.json index b627ada718c..d129273e891 100644 --- a/homeassistant/components/ecovacs/icons.json +++ b/homeassistant/components/ecovacs/icons.json @@ -140,5 +140,8 @@ "default": "mdi:laser-pointer" } } + }, + "services": { + "raw_get_positions": "mdi:map-marker-radius-outline" } } diff --git a/homeassistant/components/ecovacs/services.yaml b/homeassistant/components/ecovacs/services.yaml new file mode 100644 index 00000000000..0d884a24feb --- /dev/null +++ b/homeassistant/components/ecovacs/services.yaml @@ -0,0 +1,4 @@ +raw_get_positions: + target: + entity: + domain: vacuum diff --git a/homeassistant/components/ecovacs/strings.json b/homeassistant/components/ecovacs/strings.json index d1ea3eb4faf..25fd9b1b978 100644 --- a/homeassistant/components/ecovacs/strings.json +++ b/homeassistant/components/ecovacs/strings.json @@ -226,6 +226,9 @@ }, "vacuum_send_command_params_required": { "message": "Params are required for the command: {command}" + }, + "vacuum_raw_get_positions_not_supported": { + "message": "Getting the positions of the charges and the device itself is not supported" } }, "issues": { @@ -261,5 +264,11 @@ "self_hosted": "Self-hosted" } } + }, + "services": { + "raw_get_positions": { + "name": "Get raw positions", + "description": "Get the raw response for the positions of the chargers and the device itself." + } } } diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 5c898694cbb..e637eb14fd6 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -23,8 +23,9 @@ from homeassistant.components.vacuum import ( StateVacuumEntityDescription, VacuumEntityFeature, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, SupportsResponse from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -39,6 +40,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_ERROR = "error" ATTR_COMPONENT_PREFIX = "component_" +SERVICE_RAW_GET_POSITIONS = "raw_get_positions" + async def async_setup_entry( hass: HomeAssistant, @@ -46,6 +49,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Ecovacs vacuums.""" + controller = config_entry.runtime_data vacuums: list[EcovacsVacuum | EcovacsLegacyVacuum] = [ EcovacsVacuum(device) for device in controller.devices(VacuumCapabilities) @@ -56,6 +60,14 @@ async def async_setup_entry( _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) async_add_entities(vacuums) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_RAW_GET_POSITIONS, + {}, + "async_raw_get_positions", + supports_response=SupportsResponse.ONLY, + ) + class EcovacsLegacyVacuum(StateVacuumEntity): """Legacy Ecovacs vacuums.""" @@ -197,6 +209,15 @@ class EcovacsLegacyVacuum(StateVacuumEntity): """Send a command to a vacuum cleaner.""" self.device.run(sucks.VacBotCommand(command, params)) + async def async_raw_get_positions( + self, + ) -> None: + """Get bot and chargers positions.""" + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="vacuum_raw_get_positions_not_supported", + ) + _STATE_TO_VACUUM_STATE = { State.IDLE: STATE_IDLE, @@ -377,3 +398,19 @@ class EcovacsVacuum( await self._device.execute_command( self._capability.custom.set(command, params) ) + + async def async_raw_get_positions( + self, + ) -> dict[str, Any]: + """Get bot and chargers positions.""" + _LOGGER.debug("async_raw_get_positions") + + if not (map_cap := self._capability.map) or not ( + position_commands := map_cap.position.get + ): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="vacuum_raw_get_positions_not_supported", + ) + + return await self._device.execute_command(position_commands[0]) diff --git a/tests/components/ecovacs/test_services.py b/tests/components/ecovacs/test_services.py new file mode 100644 index 00000000000..973c63782ec --- /dev/null +++ b/tests/components/ecovacs/test_services.py @@ -0,0 +1,89 @@ +"""Tests for Ecovacs services.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import patch + +from deebot_client.device import Device +import pytest + +from homeassistant.components.ecovacs.const import DOMAIN +from homeassistant.components.ecovacs.vacuum import SERVICE_RAW_GET_POSITIONS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +pytestmark = [pytest.mark.usefixtures("init_integration")] + + +@pytest.fixture +def mock_device_execute_response( + data: dict[str, Any], +) -> Generator[dict[str, Any], None, None]: + """Mock the device execute function response.""" + + response = { + "ret": "ok", + "resp": { + "header": { + "pri": 1, + "tzm": 480, + "ts": "1717113600000", + "ver": "0.0.1", + "fwVer": "1.2.0", + "hwVer": "0.1.0", + }, + "body": { + "code": 0, + "msg": "ok", + "data": data, + }, + }, + "id": "xRV3", + "payloadType": "j", + } + + with patch.object( + Device, + "execute_command", + return_value=response, + ): + yield response + + +@pytest.mark.usefixtures("mock_device_execute_response") +@pytest.mark.parametrize( + "data", + [ + { + "deebotPos": {"x": 1, "y": 5, "a": 85}, + "chargePos": {"x": 5, "y": 9, "a": 85}, + }, + { + "deebotPos": {"x": 375, "y": 313, "a": 90}, + "chargePos": [{"x": 112, "y": 768, "a": 32}, {"x": 489, "y": 322, "a": 0}], + }, + ], +) +@pytest.mark.parametrize( + ("device_fixture", "entity_id"), + [ + ("yna5x1", "vacuum.ozmo_950"), + ], + ids=["yna5x1"], +) +async def test_get_positions_service( + hass: HomeAssistant, + mock_device_execute_response: dict[str], + entity_id: str, +) -> None: + """Test that get_positions service response snapshots match.""" + vacuum = hass.states.get(entity_id) + assert vacuum + + assert await hass.services.async_call( + DOMAIN, + SERVICE_RAW_GET_POSITIONS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + return_response=True, + ) == {entity_id: mock_device_execute_response}