Add entity services to the Hydrawise integration (#120883)
* Add services to the Hydrawise integration * Add validation of duration ranges * Remove clamping test * Fix duration type in test * Changes requested during review * Add back the HydrawiseZoneBinarySensor classpull/122660/head
parent
49e2bfae31
commit
7820bcf218
|
@ -4,6 +4,10 @@ from __future__ import annotations
|
|||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from pydrawise import Zone
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
@ -12,9 +16,11 @@ from homeassistant.components.binary_sensor import (
|
|||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SERVICE_RESUME, SERVICE_START_WATERING, SERVICE_SUSPEND
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
|
||||
|
@ -61,6 +67,13 @@ ZONE_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
|
|||
),
|
||||
)
|
||||
|
||||
SCHEMA_START_WATERING: VolDictType = {
|
||||
vol.Optional("duration"): vol.All(vol.Coerce(int), vol.Range(min=0, max=90)),
|
||||
}
|
||||
SCHEMA_SUSPEND: VolDictType = {
|
||||
vol.Required("until"): cv.datetime,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -89,11 +102,19 @@ async def async_setup_entry(
|
|||
if "rain sensor" in sensor.model.name.lower()
|
||||
)
|
||||
entities.extend(
|
||||
HydrawiseBinarySensor(coordinator, description, controller, zone_id=zone.id)
|
||||
HydrawiseZoneBinarySensor(
|
||||
coordinator, description, controller, zone_id=zone.id
|
||||
)
|
||||
for zone in controller.zones
|
||||
for description in ZONE_BINARY_SENSORS
|
||||
)
|
||||
async_add_entities(entities)
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(SERVICE_RESUME, {}, "resume")
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering"
|
||||
)
|
||||
platform.async_register_entity_service(SERVICE_SUSPEND, SCHEMA_SUSPEND, "suspend")
|
||||
|
||||
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
|
@ -111,3 +132,27 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
|||
if self.entity_description.always_available:
|
||||
return True
|
||||
return super().available
|
||||
|
||||
|
||||
class HydrawiseZoneBinarySensor(HydrawiseBinarySensor):
|
||||
"""A binary sensor for a Hydrawise irrigation zone.
|
||||
|
||||
This is only used for irrigation zones, as they have special methods for
|
||||
service actions that don't apply to other binary sensors.
|
||||
"""
|
||||
|
||||
zone: Zone
|
||||
|
||||
async def start_watering(self, duration: int | None = None) -> None:
|
||||
"""Start watering in the irrigation zone."""
|
||||
await self.coordinator.api.start_zone(
|
||||
self.zone, custom_run_duration=int((duration or 0) * 60)
|
||||
)
|
||||
|
||||
async def suspend(self, until: datetime) -> None:
|
||||
"""Suspend automatic watering in the irrigation zone."""
|
||||
await self.coordinator.api.suspend_zone(self.zone, until=until)
|
||||
|
||||
async def resume(self) -> None:
|
||||
"""Resume automatic watering in the irrigation zone."""
|
||||
await self.coordinator.api.resume_zone(self.zone)
|
||||
|
|
|
@ -5,9 +5,6 @@ import logging
|
|||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60]
|
||||
CONF_WATERING_TIME = "watering_minutes"
|
||||
|
||||
DOMAIN = "hydrawise"
|
||||
DEFAULT_WATERING_TIME = timedelta(minutes=15)
|
||||
|
||||
|
@ -16,3 +13,10 @@ MANUFACTURER = "Hydrawise"
|
|||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
|
||||
|
||||
SERVICE_RESUME = "resume"
|
||||
SERVICE_START_WATERING = "start_watering"
|
||||
SERVICE_SUSPEND = "suspend"
|
||||
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_UNTIL = "until"
|
||||
|
|
|
@ -29,5 +29,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_watering": "mdi:sprinkler-variant",
|
||||
"suspend": "mdi:pause-circle-outline",
|
||||
"resume": "mdi:play"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
start_watering:
|
||||
target:
|
||||
entity:
|
||||
integration: hydrawise
|
||||
domain: binary_sensor
|
||||
device_class: running
|
||||
fields:
|
||||
duration:
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 90
|
||||
unit_of_measurement: min
|
||||
mode: box
|
||||
suspend:
|
||||
target:
|
||||
entity:
|
||||
integration: hydrawise
|
||||
domain: binary_sensor
|
||||
device_class: running
|
||||
fields:
|
||||
until:
|
||||
required: true
|
||||
selector:
|
||||
datetime:
|
||||
resume:
|
||||
target:
|
||||
entity:
|
||||
integration: hydrawise
|
||||
domain: binary_sensor
|
||||
device_class: running
|
|
@ -57,5 +57,31 @@
|
|||
"name": "Manual watering"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"start_watering": {
|
||||
"name": "Start watering",
|
||||
"description": "Starts a watering cycle in the selected irrigation zone.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Duration",
|
||||
"description": "Length of time to run the watering cycle. If not specified (or zero), the default watering duration set in the Hydrawise mobile or web app for the irrigation zone will be used."
|
||||
}
|
||||
}
|
||||
},
|
||||
"suspend": {
|
||||
"name": "Suspend automatic watering",
|
||||
"description": "Suspends an irrigation zone's automatic watering schedule until the given date and time.",
|
||||
"fields": {
|
||||
"until": {
|
||||
"name": "Until",
|
||||
"description": "Date and time to resume the automated watering schedule."
|
||||
}
|
||||
}
|
||||
},
|
||||
"resume": {
|
||||
"name": "Resume automatic watering",
|
||||
"description": "Resumes an irrigation zone's automatic watering schedule."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
"""Test Hydrawise services."""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from pydrawise.schema import Zone
|
||||
|
||||
from homeassistant.components.hydrawise.const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_UNTIL,
|
||||
DOMAIN,
|
||||
SERVICE_RESUME,
|
||||
SERVICE_START_WATERING,
|
||||
SERVICE_SUSPEND,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_start_watering(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test that the start_watering service works as intended."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_WATERING,
|
||||
{
|
||||
ATTR_ENTITY_ID: "binary_sensor.zone_one_watering",
|
||||
ATTR_DURATION: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.start_zone.assert_called_once_with(
|
||||
zones[0], custom_run_duration=20 * 60
|
||||
)
|
||||
|
||||
|
||||
async def test_start_watering_no_duration(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test that the start_watering service works with no duration specified."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_START_WATERING,
|
||||
{ATTR_ENTITY_ID: "binary_sensor.zone_one_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.start_zone.assert_called_once_with(zones[0], custom_run_duration=0)
|
||||
|
||||
|
||||
async def test_resume(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test that the resume service works as intended."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_RESUME,
|
||||
{ATTR_ENTITY_ID: "binary_sensor.zone_one_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.resume_zone.assert_called_once_with(zones[0])
|
||||
|
||||
|
||||
async def test_suspend(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test that the suspend service works as intended."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SUSPEND,
|
||||
{
|
||||
ATTR_ENTITY_ID: "binary_sensor.zone_one_watering",
|
||||
ATTR_UNTIL: datetime(2026, 1, 1, 0, 0, 0),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.suspend_zone.assert_called_once_with(
|
||||
zones[0], until=datetime(2026, 1, 1, 0, 0, 0)
|
||||
)
|
Loading…
Reference in New Issue