Add ecovacs lawn mover (#111673)

This commit is contained in:
Andy 2024-02-28 16:35:29 +01:00 committed by GitHub
parent 515ca2b6f1
commit 69bb827a20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 312 additions and 3 deletions

View File

@ -331,8 +331,8 @@ build.json @home-assistant/supervisor
/tests/components/ecoforest/ @pjanuario
/homeassistant/components/econet/ @w1ll1am23
/tests/components/econet/ @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus
/homeassistant/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @augar
/tests/components/ecovacs/ @OverloadUT @mib1185 @edenhaus @augar
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/efergy/ @tkdrob

View File

@ -28,6 +28,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.IMAGE,
Platform.LAWN_MOWER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,

View File

@ -0,0 +1,99 @@
"""Ecovacs mower entity."""
from __future__ import annotations
import logging
from deebot_client.capabilities import MowerCapabilities
from deebot_client.device import Device
from deebot_client.events import StateEvent
from deebot_client.models import CleanAction, State
from homeassistant.components.lawn_mower import (
LawnMowerActivity,
LawnMowerEntity,
LawnMowerEntityEntityDescription,
LawnMowerEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .controller import EcovacsController
from .entity import EcovacsEntity
_LOGGER = logging.getLogger(__name__)
_STATE_TO_MOWER_STATE = {
State.IDLE: LawnMowerActivity.PAUSED,
State.CLEANING: LawnMowerActivity.MOWING,
State.RETURNING: LawnMowerActivity.MOWING,
State.DOCKED: LawnMowerActivity.DOCKED,
State.ERROR: LawnMowerActivity.ERROR,
State.PAUSED: LawnMowerActivity.PAUSED,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Ecovacs mowers."""
mowers: list[EcovacsMower] = []
controller: EcovacsController = hass.data[DOMAIN][config_entry.entry_id]
for device in controller.devices(MowerCapabilities):
mowers.append(EcovacsMower(device))
_LOGGER.debug("Adding Ecovacs Mowers to Home Assistant: %s", mowers)
async_add_entities(mowers)
class EcovacsMower(
EcovacsEntity[MowerCapabilities, MowerCapabilities],
LawnMowerEntity,
):
"""Ecovacs Mower."""
_attr_supported_features = (
LawnMowerEntityFeature.DOCK
| LawnMowerEntityFeature.PAUSE
| LawnMowerEntityFeature.START_MOWING
)
entity_description = LawnMowerEntityEntityDescription(
key="mower", translation_key="mower", name=None
)
def __init__(self, device: Device[MowerCapabilities]) -> None:
"""Initialize the mower."""
capabilities = device.capabilities
super().__init__(device, capabilities)
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
async def on_status(event: StateEvent) -> None:
self._attr_activity = _STATE_TO_MOWER_STATE[event.state]
self.async_write_ha_state()
self._subscribe(self._capability.state.event, on_status)
async def _clean_command(self, action: CleanAction) -> None:
await self._device.execute_command(
self._capability.clean.action.command(action)
)
async def async_start_mowing(self) -> None:
"""Resume schedule."""
await self._clean_command(CleanAction.START)
async def async_pause(self) -> None:
"""Pauses the mower."""
await self._clean_command(CleanAction.PAUSE)
async def async_dock(self) -> None:
"""Parks the mower until next schedule."""
await self._device.execute_command(self._capability.charge.execute())

View File

@ -1,7 +1,7 @@
{
"domain": "ecovacs",
"name": "Ecovacs",
"codeowners": ["@OverloadUT", "@mib1185", "@edenhaus"],
"codeowners": ["@OverloadUT", "@mib1185", "@edenhaus", "@augar"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",

View File

@ -0,0 +1,22 @@
{
"did": "8516fbb1-17f1-4194-0000000",
"name": "E1234567890000000002",
"class": "5xu9h3",
"resource": "cbCt",
"company": "eco-ng",
"service": {
"jmq": "jmq-ngiot-eu.dc.ww.ecouser.net",
"mqs": "api-ngiot.dc-as.ww.ecouser.net"
},
"deviceName": "GOAT",
"icon": "https://portal-ww.ecouser.net/api/pim/file/get/63913d74df25d049785c3bd7",
"UILogicId": "goat_ww_h_goat",
"materialNo": "116-2201-0001",
"pid": "0000000",
"product_category": "GOATBOT",
"model": "GOAT",
"nick": "Goat G1",
"homeSort": 9999,
"status": 1,
"ota": true
}

View File

@ -0,0 +1,67 @@
# serializer version: 1
# name: test_lawn_mower[5xu9h3][lawn_mower.goat_g1-entity_entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lawn_mower',
'entity_category': None,
'entity_id': 'lawn_mower.goat_g1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': <LawnMowerEntityFeature: 7>,
'translation_key': 'mower',
'unique_id': '8516fbb1-17f1-4194-0000000_mower',
'unit_of_measurement': None,
})
# ---
# name: test_lawn_mower[5xu9h3][lawn_mower.goat_g1-state]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lawn_mower',
'entity_category': None,
'entity_id': 'lawn_mower.goat_g1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'ecovacs',
'previous_unique_id': None,
'supported_features': <LawnMowerEntityFeature: 7>,
'translation_key': 'mower',
'unique_id': '8516fbb1-17f1-4194-0000000_mower',
'unit_of_measurement': None,
})
# ---

View File

@ -121,6 +121,7 @@ async def test_devices_in_dr(
("device_fixture", "entities"),
[
("yna5x1", 25),
("5xu9h3", 14),
],
)
async def test_all_entities_loaded(

View File

@ -0,0 +1,119 @@
"""Tests for Ecovacs lawn mower entities."""
from dataclasses import dataclass
from deebot_client.capabilities import MowerCapabilities
from deebot_client.command import Command
from deebot_client.commands.json import Charge, CleanV2
from deebot_client.events import StateEvent
from deebot_client.models import CleanAction, State
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.ecovacs.const import DOMAIN
from homeassistant.components.ecovacs.controller import EcovacsController
from homeassistant.components.lawn_mower import (
DOMAIN as PLATFORM_DOMAIN,
LawnMowerActivity,
)
from homeassistant.components.lawn_mower.const import (
SERVICE_DOCK,
SERVICE_PAUSE,
SERVICE_START_MOWING,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .util import notify_and_wait
pytestmark = [pytest.mark.usefixtures("init_integration")]
@pytest.fixture
def platforms() -> Platform | list[Platform]:
"""Platforms, which should be loaded during the test."""
return Platform.LAWN_MOWER
@pytest.mark.parametrize(
("device_fixture"),
[
"5xu9h3",
],
)
async def test_lawn_mower(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
controller: EcovacsController,
) -> None:
"""Test lawn mower states."""
entity_id = "lawn_mower.goat_g1"
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNKNOWN
assert (entity_entry := entity_registry.async_get(state.entity_id))
assert entity_entry == snapshot(name=f"{entity_id}-entity_entry")
assert entity_entry.device_id
device = next(controller.devices(MowerCapabilities))
assert (device_entry := device_registry.async_get(entity_entry.device_id))
assert device_entry.identifiers == {(DOMAIN, device.device_info["did"])}
event_bus = device.events
await notify_and_wait(hass, event_bus, StateEvent(State.CLEANING))
assert (state := hass.states.get(state.entity_id))
assert entity_entry == snapshot(name=f"{entity_id}-state")
assert state.state == LawnMowerActivity.MOWING
await notify_and_wait(hass, event_bus, StateEvent(State.DOCKED))
assert (state := hass.states.get(state.entity_id))
assert state.state == LawnMowerActivity.DOCKED
@dataclass(frozen=True)
class MowerTestCase:
"""Mower test."""
command: Command
service_name: str
@pytest.mark.parametrize(
("device_fixture", "entity_id", "tests"),
[
(
"5xu9h3",
"lawn_mower.goat_g1",
[
MowerTestCase(Charge(), SERVICE_DOCK),
MowerTestCase(CleanV2(CleanAction.PAUSE), SERVICE_PAUSE),
MowerTestCase(CleanV2(CleanAction.START), SERVICE_START_MOWING),
],
),
],
ids=["5xu9h3"],
)
async def test_mover_services(
hass: HomeAssistant,
controller: EcovacsController,
entity_id: list[str],
tests: list[MowerTestCase],
) -> None:
"""Test mover services."""
device = next(controller.devices(MowerCapabilities))
for test in tests:
device._execute_command.reset_mock()
await hass.services.async_call(
PLATFORM_DOMAIN,
test.service_name,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
device._execute_command.assert_called_with(test.command)