Add a DataUpdateCoordinator to Hydrawise (#93223)

* Add a DataUpdateCoordinator to Hydrawise

* Replace DATA_HYDRAWISE with DOMAIN

* Replace persistent notification with a ConfigEntryNotReady exception

* Changes requested during PR review

* Add a type annotation to the `monitored_conditions` field.

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
David Knowles 2023-05-24 08:07:37 -04:00 committed by GitHub
parent f355f0cc6d
commit ace45f31ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 91 deletions

View file

@ -1,5 +1,6 @@
"""Support for Hydrawise cloud."""
from hydrawiser.core import Hydrawiser
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
@ -8,19 +9,10 @@ from homeassistant.components import persistent_notification
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from .const import (
DATA_HYDRAWISE,
DOMAIN,
LOGGER,
NOTIFICATION_ID,
NOTIFICATION_TITLE,
SCAN_INTERVAL,
SIGNAL_UPDATE_HYDRAWISE,
)
from .const import DOMAIN, LOGGER, NOTIFICATION_ID, NOTIFICATION_TITLE, SCAN_INTERVAL
from .coordinator import HydrawiseDataUpdateCoordinator
CONFIG_SCHEMA = vol.Schema(
{
@ -35,35 +27,39 @@ CONFIG_SCHEMA = vol.Schema(
)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Hunter Hydrawise component."""
conf = config[DOMAIN]
access_token = conf[CONF_ACCESS_TOKEN]
scan_interval = conf.get(CONF_SCAN_INTERVAL)
try:
hydrawise = Hydrawiser(user_token=access_token)
hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise)
hydrawise = await hass.async_add_executor_job(Hydrawiser, access_token)
except (ConnectTimeout, HTTPError) as ex:
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
_show_failure_notification(hass, str(ex))
return False
if not hydrawise.current_controller:
LOGGER.error("Failed to fetch Hydrawise data")
_show_failure_notification(hass, "Failed to fetch Hydrawise data.")
return False
hass.data[DOMAIN] = HydrawiseDataUpdateCoordinator(hass, hydrawise, scan_interval)
# NOTE: We don't need to call async_config_entry_first_refresh() because
# data is fetched when the Hydrawiser object is instantiated.
return True
def _show_failure_notification(hass: HomeAssistant, error: str):
persistent_notification.create(
hass,
f"Error: {ex}<br />You will need to restart hass after fixing.",
f"Error: {error}<br />You will need to restart hass after fixing.",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
return False
def hub_refresh(event_time):
"""Call Hydrawise hub to refresh information."""
LOGGER.debug("Updating Hydrawise Hub component")
hass.data[DATA_HYDRAWISE].data.update_controller_info()
dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE)
# Call the Hydrawise API to refresh updates
track_time_interval(hass, hub_refresh, scan_interval)
return True
class HydrawiseHub:

View file

@ -1,6 +1,7 @@
"""Support for Hydrawise sprinkler binary sensors."""
from __future__ import annotations
from hydrawiser.core import Hydrawiser
import voluptuous as vol
from homeassistant.components.binary_sensor import (
@ -10,12 +11,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DATA_HYDRAWISE, LOGGER
from .const import DOMAIN, LOGGER
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
BINARY_SENSOR_STATUS = BinarySensorEntityDescription(
@ -52,23 +54,29 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
hydrawise: Hydrawiser = coordinator.api
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
entities = []
if BINARY_SENSOR_STATUS.key in monitored_conditions:
entities.append(
HydrawiseBinarySensor(hydrawise.current_controller, BINARY_SENSOR_STATUS)
HydrawiseBinarySensor(
data=hydrawise.current_controller,
coordinator=coordinator,
description=BINARY_SENSOR_STATUS,
)
)
# create a sensor for each zone
entities.extend(
[
HydrawiseBinarySensor(zone, description)
for zone in hydrawise.relays
for description in BINARY_SENSOR_TYPES
if description.key in monitored_conditions
]
for zone in hydrawise.relays:
for description in BINARY_SENSOR_TYPES:
if description.key not in monitored_conditions:
continue
entities.append(
HydrawiseBinarySensor(
data=zone, coordinator=coordinator, description=description
)
)
add_entities(entities, True)
@ -77,12 +85,13 @@ def setup_platform(
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""A sensor implementation for Hydrawise device."""
def update(self) -> None:
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
mydata = self.hass.data[DATA_HYDRAWISE].data
if self.entity_description.key == "status":
self._attr_is_on = mydata.status == "All good!"
self._attr_is_on = self.coordinator.api.status == "All good!"
elif self.entity_description.key == "is_watering":
relay_data = mydata.relays[self.data["relay"] - 1]
relay_data = self.coordinator.api.relays[self.data["relay"] - 1]
self._attr_is_on = relay_data["timestr"] == "Now"
super()._handle_coordinator_update()

View file

@ -11,7 +11,6 @@ CONF_WATERING_TIME = "watering_minutes"
NOTIFICATION_ID = "hydrawise_notification"
NOTIFICATION_TITLE = "Hydrawise Setup"
DATA_HYDRAWISE = "hydrawise"
DOMAIN = "hydrawise"
DEFAULT_WATERING_TIME = 15

View file

@ -0,0 +1,29 @@
"""DataUpdateCoordinator for the Hydrawise integration."""
from __future__ import annotations
from datetime import timedelta
from hydrawiser.core import Hydrawiser
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[None]):
"""The Hydrawise Data Update Coordinator."""
def __init__(
self, hass: HomeAssistant, api: Hydrawiser, scan_interval: timedelta
) -> None:
"""Initialize HydrawiseDataUpdateCoordinator."""
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=scan_interval)
self.api = api
async def _async_update_data(self) -> None:
"""Fetch the latest data from Hydrawise."""
result = await self.hass.async_add_executor_job(self.api.update_controller_info)
if not result:
raise UpdateFailed("Failed to refresh Hydrawise data")

View file

@ -1,36 +1,32 @@
"""Base classes for Hydrawise entities."""
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
from typing import Any
from .const import SIGNAL_UPDATE_HYDRAWISE
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
class HydrawiseEntity(Entity):
class HydrawiseEntity(CoordinatorEntity):
"""Entity class for Hydrawise devices."""
_attr_attribution = "Data provided by hydrawise.com"
def __init__(self, data, description: EntityDescription) -> None:
def __init__(
self,
*,
data: dict[str, Any],
coordinator: DataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the Hydrawise entity."""
self.entity_description = description
super().__init__(coordinator=coordinator)
self.data = data
self.entity_description = description
self._attr_name = f"{self.data['name']} {description.name}"
async def async_added_to_hass(self):
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback
)
)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def extra_state_attributes(self):
"""Return the state attributes."""

View file

@ -1,6 +1,7 @@
"""Support for Hydrawise sprinkler sensors."""
from __future__ import annotations
from hydrawiser.core import Hydrawiser
import voluptuous as vol
from homeassistant.components.sensor import (
@ -10,13 +11,14 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt
from .const import DATA_HYDRAWISE, LOGGER
from .const import DOMAIN, LOGGER
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
@ -54,11 +56,12 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
hydrawise: Hydrawiser = coordinator.api
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
entities = [
HydrawiseSensor(zone, description)
HydrawiseSensor(data=zone, coordinator=coordinator, description=description)
for zone in hydrawise.relays
for description in SENSOR_TYPES
if description.key in monitored_conditions
@ -70,11 +73,11 @@ def setup_platform(
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""A sensor implementation for Hydrawise device."""
def update(self) -> None:
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the states."""
mydata = self.hass.data[DATA_HYDRAWISE].data
LOGGER.debug("Updating Hydrawise sensor: %s", self.name)
relay_data = mydata.relays[self.data["relay"] - 1]
relay_data = self.coordinator.api.relays[self.data["relay"] - 1]
if self.entity_description.key == "watering_time":
if relay_data["timestr"] == "Now":
self._attr_native_value = int(relay_data["run"] / 60)
@ -86,3 +89,4 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
self._attr_native_value = dt.utc_from_timestamp(
dt.as_timestamp(dt.now()) + next_cycle
)
super()._handle_coordinator_update()

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any
from hydrawiser.core import Hydrawiser
import voluptuous as vol
from homeassistant.components.switch import (
@ -12,18 +13,20 @@ from homeassistant.components.switch import (
SwitchEntityDescription,
)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import (
ALLOWED_WATERING_TIME,
CONF_WATERING_TIME,
DATA_HYDRAWISE,
DEFAULT_WATERING_TIME,
DOMAIN,
LOGGER,
)
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
@ -60,12 +63,18 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
default_watering_timer = config[CONF_WATERING_TIME]
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
hydrawise: Hydrawiser = coordinator.api
monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS]
default_watering_timer: int = config[CONF_WATERING_TIME]
entities = [
HydrawiseSwitch(zone, description, default_watering_timer)
HydrawiseSwitch(
data=zone,
coordinator=coordinator,
description=description,
default_watering_timer=default_watering_timer,
)
for zone in hydrawise.relays
for description in SWITCH_TYPES
if description.key in monitored_conditions
@ -78,38 +87,41 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
"""A switch implementation for Hydrawise device."""
def __init__(
self, data, description: SwitchEntityDescription, default_watering_timer
self,
*,
data: dict[str, Any],
coordinator: DataUpdateCoordinator,
description: SwitchEntityDescription,
default_watering_timer: int,
) -> None:
"""Initialize a switch for Hydrawise device."""
super().__init__(data, description)
super().__init__(data=data, coordinator=coordinator, description=description)
self._default_watering_timer = default_watering_timer
def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
relay_data = self.data["relay"] - 1
if self.entity_description.key == "manual_watering":
self.hass.data[DATA_HYDRAWISE].data.run_zone(
self._default_watering_timer, relay_data
)
self.coordinator.api.run_zone(self._default_watering_timer, relay_data)
elif self.entity_description.key == "auto_watering":
self.hass.data[DATA_HYDRAWISE].data.suspend_zone(0, relay_data)
self.coordinator.api.suspend_zone(0, relay_data)
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
relay_data = self.data["relay"] - 1
if self.entity_description.key == "manual_watering":
self.hass.data[DATA_HYDRAWISE].data.run_zone(0, relay_data)
self.coordinator.api.run_zone(0, relay_data)
elif self.entity_description.key == "auto_watering":
self.hass.data[DATA_HYDRAWISE].data.suspend_zone(365, relay_data)
self.coordinator.api.suspend_zone(365, relay_data)
def update(self) -> None:
@callback
def _handle_coordinator_update(self) -> None:
"""Update device state."""
relay_data = self.data["relay"] - 1
mydata = self.hass.data[DATA_HYDRAWISE].data
LOGGER.debug("Updating Hydrawise switch: %s", self.name)
timestr = self.coordinator.api.relays[relay_data]["timestr"]
if self.entity_description.key == "manual_watering":
self._attr_is_on = mydata.relays[relay_data]["timestr"] == "Now"
self._attr_is_on = timestr == "Now"
elif self.entity_description.key == "auto_watering":
self._attr_is_on = (mydata.relays[relay_data]["timestr"] != "") and (
mydata.relays[relay_data]["timestr"] != "Now"
)
self._attr_is_on = timestr not in {"", "Now"}
super()._handle_coordinator_update()