mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add service for Husqvarna Automower (#117269)
This commit is contained in:
parent
bd65afa207
commit
1bd95d3596
@ -32,5 +32,8 @@
|
||||
"default": "mdi:tooltip-question"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"override_schedule": "mdi:debug-step-over"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
"""Husqvarna Automower lawn mower entity."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from datetime import timedelta
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioautomower.exceptions import ApiException
|
||||
from aioautomower.model import MowerActivities, MowerStates
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.lawn_mower import (
|
||||
LawnMowerActivity,
|
||||
@ -12,18 +17,14 @@ from homeassistant.components.lawn_mower import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AutomowerConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import AutomowerControlEntity
|
||||
|
||||
SUPPORT_STATE_SERVICES = (
|
||||
LawnMowerEntityFeature.DOCK
|
||||
| LawnMowerEntityFeature.PAUSE
|
||||
| LawnMowerEntityFeature.START_MOWING
|
||||
)
|
||||
|
||||
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
|
||||
MOWING_ACTIVITIES = (
|
||||
MowerActivities.MOWING,
|
||||
@ -35,11 +36,38 @@ PAUSED_STATES = [
|
||||
MowerStates.WAIT_UPDATING,
|
||||
MowerStates.WAIT_POWER_UP,
|
||||
]
|
||||
SUPPORT_STATE_SERVICES = (
|
||||
LawnMowerEntityFeature.DOCK
|
||||
| LawnMowerEntityFeature.PAUSE
|
||||
| LawnMowerEntityFeature.START_MOWING
|
||||
)
|
||||
MOW = "mow"
|
||||
PARK = "park"
|
||||
OVERRIDE_MODES = [MOW, PARK]
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def handle_sending_exception(
|
||||
func: Callable[..., Awaitable[Any]],
|
||||
) -> Callable[..., Coroutine[Any, Any, None]]:
|
||||
"""Handle exceptions while sending a command."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except ApiException as exception:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_send_failed",
|
||||
translation_placeholders={"exception": str(exception)},
|
||||
) from exception
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AutomowerConfigEntry,
|
||||
@ -51,6 +79,20 @@ async def async_setup_entry(
|
||||
AutomowerLawnMowerEntity(mower_id, coordinator) for mower_id in coordinator.data
|
||||
)
|
||||
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
"override_schedule",
|
||||
{
|
||||
vol.Required("override_mode"): vol.In(OVERRIDE_MODES),
|
||||
vol.Required("duration"): vol.All(
|
||||
cv.time_period,
|
||||
cv.positive_timedelta,
|
||||
vol.Range(min=timedelta(minutes=1), max=timedelta(days=42)),
|
||||
),
|
||||
},
|
||||
"async_override_schedule",
|
||||
)
|
||||
|
||||
|
||||
class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity):
|
||||
"""Defining each mower Entity."""
|
||||
@ -81,29 +123,27 @@ class AutomowerLawnMowerEntity(AutomowerControlEntity, LawnMowerEntity):
|
||||
return LawnMowerActivity.DOCKED
|
||||
return LawnMowerActivity.ERROR
|
||||
|
||||
@handle_sending_exception
|
||||
async def async_start_mowing(self) -> None:
|
||||
"""Resume schedule."""
|
||||
try:
|
||||
await self.coordinator.api.commands.resume_schedule(self.mower_id)
|
||||
except ApiException as exception:
|
||||
raise HomeAssistantError(
|
||||
f"Command couldn't be sent to the command queue: {exception}"
|
||||
) from exception
|
||||
await self.coordinator.api.commands.resume_schedule(self.mower_id)
|
||||
|
||||
@handle_sending_exception
|
||||
async def async_pause(self) -> None:
|
||||
"""Pauses the mower."""
|
||||
try:
|
||||
await self.coordinator.api.commands.pause_mowing(self.mower_id)
|
||||
except ApiException as exception:
|
||||
raise HomeAssistantError(
|
||||
f"Command couldn't be sent to the command queue: {exception}"
|
||||
) from exception
|
||||
await self.coordinator.api.commands.pause_mowing(self.mower_id)
|
||||
|
||||
@handle_sending_exception
|
||||
async def async_dock(self) -> None:
|
||||
"""Parks the mower until next schedule."""
|
||||
try:
|
||||
await self.coordinator.api.commands.park_until_next_schedule(self.mower_id)
|
||||
except ApiException as exception:
|
||||
raise HomeAssistantError(
|
||||
f"Command couldn't be sent to the command queue: {exception}"
|
||||
) from exception
|
||||
await self.coordinator.api.commands.park_until_next_schedule(self.mower_id)
|
||||
|
||||
@handle_sending_exception
|
||||
async def async_override_schedule(
|
||||
self, override_mode: str, duration: timedelta
|
||||
) -> None:
|
||||
"""Override the schedule with mowing or parking."""
|
||||
if override_mode == MOW:
|
||||
await self.coordinator.api.commands.start_for(self.mower_id, duration)
|
||||
if override_mode == PARK:
|
||||
await self.coordinator.api.commands.park_for(self.mower_id, duration)
|
||||
|
21
homeassistant/components/husqvarna_automower/services.yaml
Normal file
21
homeassistant/components/husqvarna_automower/services.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
override_schedule:
|
||||
target:
|
||||
entity:
|
||||
integration: "husqvarna_automower"
|
||||
domain: "lawn_mower"
|
||||
fields:
|
||||
duration:
|
||||
required: true
|
||||
example: "{'days': 1, 'hours': 12, 'minutes': 30}"
|
||||
selector:
|
||||
duration:
|
||||
enable_day: true
|
||||
override_mode:
|
||||
required: true
|
||||
example: "mow"
|
||||
selector:
|
||||
select:
|
||||
translation_key: override_modes
|
||||
options:
|
||||
- "mow"
|
||||
- "park"
|
@ -269,5 +269,29 @@
|
||||
"command_send_failed": {
|
||||
"message": "Failed to send command: {exception}"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"override_modes": {
|
||||
"options": {
|
||||
"mow": "Mow",
|
||||
"park": "Park"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"override_schedule": {
|
||||
"name": "Override schedule",
|
||||
"description": "Override the schedule to either mow or park for a duration of time.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "Minimum: 1 minute, maximum: 42 days, seconds will be ignored."
|
||||
},
|
||||
"override_mode": {
|
||||
"name": "Override mode",
|
||||
"description": "With which action the schedule should be overridden."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""Tests for lawn_mower module."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioautomower.exceptions import ApiException
|
||||
from aioautomower.utils import mower_list_to_dictionary_dataclass
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from voluptuous.error import MultipleInvalid
|
||||
|
||||
from homeassistant.components.husqvarna_automower.const import DOMAIN
|
||||
from homeassistant.components.husqvarna_automower.coordinator import SCAN_INTERVAL
|
||||
@ -84,11 +86,103 @@ async def test_lawn_mower_commands(
|
||||
).side_effect = ApiException("Test error")
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Command couldn't be sent to the command queue: Test error",
|
||||
match="Failed to send command: Test error",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
domain="lawn_mower",
|
||||
service=service,
|
||||
service_data={"entity_id": "lawn_mower.test_mower_1"},
|
||||
target={"entity_id": "lawn_mower.test_mower_1"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("aioautomower_command", "extra_data", "service", "service_data"),
|
||||
[
|
||||
(
|
||||
"start_for",
|
||||
timedelta(hours=3),
|
||||
"override_schedule",
|
||||
{
|
||||
"duration": {"days": 0, "hours": 3, "minutes": 0},
|
||||
"override_mode": "mow",
|
||||
},
|
||||
),
|
||||
(
|
||||
"park_for",
|
||||
timedelta(days=1, hours=12, minutes=30),
|
||||
"override_schedule",
|
||||
{
|
||||
"duration": {"days": 1, "hours": 12, "minutes": 30},
|
||||
"override_mode": "park",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lawn_mower_service_commands(
|
||||
hass: HomeAssistant,
|
||||
aioautomower_command: str,
|
||||
extra_data: int | None,
|
||||
service: str,
|
||||
service_data: dict[str, int] | None,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test lawn_mower commands."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mocked_method = AsyncMock()
|
||||
setattr(mock_automower_client.commands, aioautomower_command, mocked_method)
|
||||
await hass.services.async_call(
|
||||
domain=DOMAIN,
|
||||
service=service,
|
||||
target={"entity_id": "lawn_mower.test_mower_1"},
|
||||
service_data=service_data,
|
||||
blocking=True,
|
||||
)
|
||||
mocked_method.assert_called_once_with(TEST_MOWER_ID, extra_data)
|
||||
|
||||
getattr(
|
||||
mock_automower_client.commands, aioautomower_command
|
||||
).side_effect = ApiException("Test error")
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Failed to send command: Test error",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
domain=DOMAIN,
|
||||
service=service,
|
||||
target={"entity_id": "lawn_mower.test_mower_1"},
|
||||
service_data=service_data,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data"),
|
||||
[
|
||||
(
|
||||
"override_schedule",
|
||||
{
|
||||
"duration": {"days": 1, "hours": 12, "minutes": 30},
|
||||
"override_mode": "fly_to_moon",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_lawn_mower_wrong_service_commands(
|
||||
hass: HomeAssistant,
|
||||
service: str,
|
||||
service_data: dict[str, int] | None,
|
||||
mock_automower_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test lawn_mower commands."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
with pytest.raises(MultipleInvalid):
|
||||
await hass.services.async_call(
|
||||
domain=DOMAIN,
|
||||
service=service,
|
||||
target={"entity_id": "lawn_mower.test_mower_1"},
|
||||
service_data=service_data,
|
||||
blocking=True,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user