Add Rachio smart hose timer support (#107901)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Brian Rogers 2024-03-23 13:36:03 -04:00 committed by GitHub
parent 3c13a28357
commit dbb4cf0ee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 263 additions and 17 deletions

View file

@ -11,19 +11,28 @@ import voluptuous as vol
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, ATTR_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_platform,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp
from .const import (
CONF_MANUAL_RUN_MINS,
DEFAULT_MANUAL_RUN_MINS,
DEFAULT_NAME,
DOMAIN as DOMAIN_RACHIO,
KEY_CONNECTED,
KEY_CURRENT_STATUS,
KEY_CUSTOM_CROP,
KEY_CUSTOM_SHADE,
KEY_CUSTOM_SLOPE,
@ -36,7 +45,9 @@ from .const import (
KEY_ON,
KEY_RAIN_DELAY,
KEY_RAIN_DELAY_END,
KEY_REPORTED_STATE,
KEY_SCHEDULE_ID,
KEY_STATE,
KEY_SUBTYPE,
KEY_SUMMARY,
KEY_TYPE,
@ -46,6 +57,7 @@ from .const import (
SCHEDULE_TYPE_FLEX,
SERVICE_SET_ZONE_MOISTURE,
SERVICE_START_MULTIPLE_ZONES,
SERVICE_START_WATERING,
SIGNAL_RACHIO_CONTROLLER_UPDATE,
SIGNAL_RACHIO_RAIN_DELAY_UPDATE,
SIGNAL_RACHIO_SCHEDULE_UPDATE,
@ -55,6 +67,7 @@ from .const import (
SLOPE_SLIGHT,
SLOPE_STEEP,
)
from .coordinator import RachioUpdateCoordinator
from .device import RachioPerson
from .entity import RachioDevice
from .webhooks import (
@ -80,6 +93,7 @@ ATTR_SCHEDULE_ENABLED = "Enabled"
ATTR_SCHEDULE_DURATION = "Duration"
ATTR_SCHEDULE_TYPE = "Type"
ATTR_SORT_ORDER = "sortOrder"
ATTR_WATERING_DURATION = "Watering Duration seconds"
ATTR_ZONE_NUMBER = "Zone number"
ATTR_ZONE_SHADE = "Shade"
ATTR_ZONE_SLOPE = "Slope"
@ -141,6 +155,19 @@ async def async_setup_entry(
else:
raise HomeAssistantError("No matching zones found in given entity_ids")
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_START_WATERING,
{
vol.Optional(ATTR_DURATION): cv.positive_int,
},
"turn_on",
)
# If only hose timers on account, none of these services apply
if not zone_entities:
return
hass.services.async_register(
DOMAIN_RACHIO,
SERVICE_START_MULTIPLE_ZONES,
@ -176,6 +203,11 @@ def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Ent
RachioSchedule(person, controller, schedule, current_schedule)
for schedule in schedules + flex_schedules
)
entities.extend(
RachioValve(person, base_station, valve, base_station.coordinator)
for base_station in person.base_stations
for valve in base_station.coordinator.data.values()
)
return entities
@ -246,9 +278,9 @@ class RachioRainDelay(RachioSwitch):
_attr_has_entity_name = True
_attr_translation_key = "rain_delay"
def __init__(self, controller):
def __init__(self, controller) -> None:
"""Set up a Rachio rain delay switch."""
self._cancel_update = None
self._cancel_update: CALLBACK_TYPE | None = None
super().__init__(controller)
@property
@ -324,7 +356,7 @@ class RachioZone(RachioSwitch):
_attr_icon = "mdi:water"
def __init__(self, person, controller, data, current_schedule):
def __init__(self, person, controller, data, current_schedule) -> None:
"""Initialize a new Rachio Zone."""
self.id = data[KEY_ID]
self._attr_name = data[KEY_NAME]
@ -379,11 +411,14 @@ class RachioZone(RachioSwitch):
self.turn_off()
# Start this zone
manual_run_time = timedelta(
minutes=self._person.config_entry.options.get(
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
if ATTR_DURATION in kwargs:
manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION])
else:
manual_run_time = timedelta(
minutes=self._person.config_entry.options.get(
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
)
)
)
# The API limit is 3 hours, and requires an int be passed
self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds)
_LOGGER.debug(
@ -435,7 +470,7 @@ class RachioZone(RachioSwitch):
class RachioSchedule(RachioSwitch):
"""Representation of one fixed schedule on the Rachio Iro."""
def __init__(self, person, controller, data, current_schedule):
def __init__(self, person, controller, data, current_schedule) -> None:
"""Initialize a new Rachio Schedule."""
self._schedule_id = data[KEY_ID]
self._duration = data[KEY_DURATION]
@ -509,3 +544,70 @@ class RachioSchedule(RachioSwitch):
self.hass, SIGNAL_RACHIO_SCHEDULE_UPDATE, self._async_handle_update
)
)
class RachioValve(CoordinatorEntity[RachioUpdateCoordinator], SwitchEntity):
"""Representation of one smart hose timer valve."""
def __init__(
self, person, base, data, coordinator: RachioUpdateCoordinator
) -> None:
"""Initialize a new smart hose valve."""
super().__init__(coordinator)
self._person = person
self._base = base
self.id = data[KEY_ID]
self._attr_name = data[KEY_NAME]
self._attr_unique_id = f"{self.id}-valve"
self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs
self._attr_device_info = DeviceInfo(
identifiers={
(
DOMAIN_RACHIO,
self.id,
)
},
connections={(dr.CONNECTION_NETWORK_MAC, self._base.mac_address)},
manufacturer=DEFAULT_NAME,
model="Smart Hose Timer",
name=self._attr_name,
configuration_url="https://app.rach.io",
)
@property
def available(self) -> bool:
"""Return if the valve is available."""
return super().available and self._static_attrs[KEY_CONNECTED]
def turn_on(self, **kwargs: Any) -> None:
"""Turn on this valve."""
if ATTR_DURATION in kwargs:
manual_run_time = timedelta(minutes=kwargs[ATTR_DURATION])
else:
manual_run_time = timedelta(
minutes=self._person.config_entry.options.get(
CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS
)
)
self._base.start_watering(self.id, manual_run_time.seconds)
self._attr_is_on = True
self.schedule_update_ha_state(force_refresh=True)
_LOGGER.debug("Starting valve %s for %s", self.name, str(manual_run_time))
def turn_off(self, **kwargs: Any) -> None:
"""Turn off this valve."""
self._base.stop_watering(self.id)
self._attr_is_on = False
self.schedule_update_ha_state(force_refresh=True)
_LOGGER.debug("Stopping watering on valve %s", self.name)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated coordinator data."""
data = self.coordinator.data[self.id]
self._static_attrs = data[KEY_STATE][KEY_REPORTED_STATE]
self._attr_is_on = KEY_CURRENT_STATUS in self._static_attrs
super()._handle_coordinator_update()