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 class
pull/122660/head
David Knowles 2024-07-26 11:25:56 -04:00 committed by GitHub
parent 49e2bfae31
commit 7820bcf218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 210 additions and 5 deletions

View File

@ -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)

View File

@ -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"

View File

@ -29,5 +29,10 @@
}
}
}
},
"services": {
"start_watering": "mdi:sprinkler-variant",
"suspend": "mdi:pause-circle-outline",
"resume": "mdi:play"
}
}

View File

@ -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

View File

@ -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."
}
}
}

View File

@ -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)
)