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.""" """Support for Hydrawise cloud."""
from hydrawiser.core import Hydrawiser from hydrawiser.core import Hydrawiser
from requests.exceptions import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol 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.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv 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 homeassistant.helpers.typing import ConfigType
from .const import ( from .const import DOMAIN, LOGGER, NOTIFICATION_ID, NOTIFICATION_TITLE, SCAN_INTERVAL
DATA_HYDRAWISE, from .coordinator import HydrawiseDataUpdateCoordinator
DOMAIN,
LOGGER,
NOTIFICATION_ID,
NOTIFICATION_TITLE,
SCAN_INTERVAL,
SIGNAL_UPDATE_HYDRAWISE,
)
CONFIG_SCHEMA = vol.Schema( 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.""" """Set up the Hunter Hydrawise component."""
conf = config[DOMAIN] conf = config[DOMAIN]
access_token = conf[CONF_ACCESS_TOKEN] access_token = conf[CONF_ACCESS_TOKEN]
scan_interval = conf.get(CONF_SCAN_INTERVAL) scan_interval = conf.get(CONF_SCAN_INTERVAL)
try: try:
hydrawise = Hydrawiser(user_token=access_token) hydrawise = await hass.async_add_executor_job(Hydrawiser, access_token)
hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise)
except (ConnectTimeout, HTTPError) as ex: except (ConnectTimeout, HTTPError) as ex:
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(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( persistent_notification.create(
hass, 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, title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID, 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: class HydrawiseHub:

View file

@ -1,6 +1,7 @@
"""Support for Hydrawise sprinkler binary sensors.""" """Support for Hydrawise sprinkler binary sensors."""
from __future__ import annotations from __future__ import annotations
from hydrawiser.core import Hydrawiser
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -10,12 +11,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.const import CONF_MONITORED_CONDITIONS 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 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 from .entity import HydrawiseEntity
BINARY_SENSOR_STATUS = BinarySensorEntityDescription( BINARY_SENSOR_STATUS = BinarySensorEntityDescription(
@ -52,23 +54,29 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up a sensor for a Hydrawise device.""" """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] monitored_conditions = config[CONF_MONITORED_CONDITIONS]
entities = [] entities = []
if BINARY_SENSOR_STATUS.key in monitored_conditions: if BINARY_SENSOR_STATUS.key in monitored_conditions:
entities.append( 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 # create a sensor for each zone
entities.extend( for zone in hydrawise.relays:
[ for description in BINARY_SENSOR_TYPES:
HydrawiseBinarySensor(zone, description) if description.key not in monitored_conditions:
for zone in hydrawise.relays continue
for description in BINARY_SENSOR_TYPES entities.append(
if description.key in monitored_conditions HydrawiseBinarySensor(
] data=zone, coordinator=coordinator, description=description
)
) )
add_entities(entities, True) add_entities(entities, True)
@ -77,12 +85,13 @@ def setup_platform(
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""A sensor implementation for Hydrawise device.""" """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.""" """Get the latest data and updates the state."""
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name) LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
mydata = self.hass.data[DATA_HYDRAWISE].data
if self.entity_description.key == "status": 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": 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" 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_ID = "hydrawise_notification"
NOTIFICATION_TITLE = "Hydrawise Setup" NOTIFICATION_TITLE = "Hydrawise Setup"
DATA_HYDRAWISE = "hydrawise"
DOMAIN = "hydrawise" DOMAIN = "hydrawise"
DEFAULT_WATERING_TIME = 15 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.""" """Base classes for Hydrawise entities."""
from homeassistant.core import callback from typing import Any
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
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.""" """Entity class for Hydrawise devices."""
_attr_attribution = "Data provided by hydrawise.com" _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.""" """Initialize the Hydrawise entity."""
self.entity_description = description super().__init__(coordinator=coordinator)
self.data = data self.data = data
self.entity_description = description
self._attr_name = f"{self.data['name']} {description.name}" 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 @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View file

@ -1,6 +1,7 @@
"""Support for Hydrawise sprinkler sensors.""" """Support for Hydrawise sprinkler sensors."""
from __future__ import annotations from __future__ import annotations
from hydrawiser.core import Hydrawiser
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -10,13 +11,14 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt from homeassistant.util import dt
from .const import DATA_HYDRAWISE, LOGGER from .const import DOMAIN, LOGGER
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity from .entity import HydrawiseEntity
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
@ -54,11 +56,12 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up a sensor for a Hydrawise device.""" """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] monitored_conditions = config[CONF_MONITORED_CONDITIONS]
entities = [ entities = [
HydrawiseSensor(zone, description) HydrawiseSensor(data=zone, coordinator=coordinator, description=description)
for zone in hydrawise.relays for zone in hydrawise.relays
for description in SENSOR_TYPES for description in SENSOR_TYPES
if description.key in monitored_conditions if description.key in monitored_conditions
@ -70,11 +73,11 @@ def setup_platform(
class HydrawiseSensor(HydrawiseEntity, SensorEntity): class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""A sensor implementation for Hydrawise device.""" """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.""" """Get the latest data and updates the states."""
mydata = self.hass.data[DATA_HYDRAWISE].data
LOGGER.debug("Updating Hydrawise sensor: %s", self.name) 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 self.entity_description.key == "watering_time":
if relay_data["timestr"] == "Now": if relay_data["timestr"] == "Now":
self._attr_native_value = int(relay_data["run"] / 60) 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( self._attr_native_value = dt.utc_from_timestamp(
dt.as_timestamp(dt.now()) + next_cycle 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 typing import Any
from hydrawiser.core import Hydrawiser
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import ( from homeassistant.components.switch import (
@ -12,18 +13,20 @@ from homeassistant.components.switch import (
SwitchEntityDescription, SwitchEntityDescription,
) )
from homeassistant.const import CONF_MONITORED_CONDITIONS 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ( from .const import (
ALLOWED_WATERING_TIME, ALLOWED_WATERING_TIME,
CONF_WATERING_TIME, CONF_WATERING_TIME,
DATA_HYDRAWISE,
DEFAULT_WATERING_TIME, DEFAULT_WATERING_TIME,
DOMAIN,
LOGGER, LOGGER,
) )
from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity from .entity import HydrawiseEntity
SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
@ -60,12 +63,18 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up a sensor for a Hydrawise device.""" """Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
monitored_conditions = config[CONF_MONITORED_CONDITIONS] hydrawise: Hydrawiser = coordinator.api
default_watering_timer = config[CONF_WATERING_TIME] monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS]
default_watering_timer: int = config[CONF_WATERING_TIME]
entities = [ 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 zone in hydrawise.relays
for description in SWITCH_TYPES for description in SWITCH_TYPES
if description.key in monitored_conditions if description.key in monitored_conditions
@ -78,38 +87,41 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
"""A switch implementation for Hydrawise device.""" """A switch implementation for Hydrawise device."""
def __init__( def __init__(
self, data, description: SwitchEntityDescription, default_watering_timer self,
*,
data: dict[str, Any],
coordinator: DataUpdateCoordinator,
description: SwitchEntityDescription,
default_watering_timer: int,
) -> None: ) -> None:
"""Initialize a switch for Hydrawise device.""" """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 self._default_watering_timer = default_watering_timer
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the device on.""" """Turn the device on."""
relay_data = self.data["relay"] - 1 relay_data = self.data["relay"] - 1
if self.entity_description.key == "manual_watering": if self.entity_description.key == "manual_watering":
self.hass.data[DATA_HYDRAWISE].data.run_zone( self.coordinator.api.run_zone(self._default_watering_timer, relay_data)
self._default_watering_timer, relay_data
)
elif self.entity_description.key == "auto_watering": 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: def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off.""" """Turn the device off."""
relay_data = self.data["relay"] - 1 relay_data = self.data["relay"] - 1
if self.entity_description.key == "manual_watering": 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": 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.""" """Update device state."""
relay_data = self.data["relay"] - 1 relay_data = self.data["relay"] - 1
mydata = self.hass.data[DATA_HYDRAWISE].data
LOGGER.debug("Updating Hydrawise switch: %s", self.name) LOGGER.debug("Updating Hydrawise switch: %s", self.name)
timestr = self.coordinator.api.relays[relay_data]["timestr"]
if self.entity_description.key == "manual_watering": 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": elif self.entity_description.key == "auto_watering":
self._attr_is_on = (mydata.relays[relay_data]["timestr"] != "") and ( self._attr_is_on = timestr not in {"", "Now"}
mydata.relays[relay_data]["timestr"] != "Now" super()._handle_coordinator_update()
)