From ace45f31ff87de242adfacee8e8df3e97e95dde5 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Wed, 24 May 2023 08:07:37 -0400 Subject: [PATCH] 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> --- .../components/hydrawise/__init__.py | 50 ++++++++---------- .../components/hydrawise/binary_sensor.py | 41 +++++++++------ homeassistant/components/hydrawise/const.py | 1 - .../components/hydrawise/coordinator.py | 29 +++++++++++ homeassistant/components/hydrawise/entity.py | 36 ++++++------- homeassistant/components/hydrawise/sensor.py | 18 ++++--- homeassistant/components/hydrawise/switch.py | 52 ++++++++++++------- 7 files changed, 136 insertions(+), 91 deletions(-) create mode 100644 homeassistant/components/hydrawise/coordinator.py diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index ba561a14f82..d5b69617ba9 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -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,37 +27,41 @@ 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)) - persistent_notification.create( - hass, - f"Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) + _show_failure_notification(hass, str(ex)) 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) + if not hydrawise.current_controller: + LOGGER.error("Failed to fetch Hydrawise data") + _show_failure_notification(hass, "Failed to fetch Hydrawise data.") + return False - # Call the Hydrawise API to refresh updates - track_time_interval(hass, hub_refresh, scan_interval) + 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: {error}
You will need to restart hass after fixing.", + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID, + ) + + class HydrawiseHub: """Representation of a base Hydrawise device.""" diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index 93594d71436..2986bbb170e 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -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,24 +54,30 @@ 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() diff --git a/homeassistant/components/hydrawise/const.py b/homeassistant/components/hydrawise/const.py index 5a046530f01..515fdaec2b1 100644 --- a/homeassistant/components/hydrawise/const.py +++ b/homeassistant/components/hydrawise/const.py @@ -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 diff --git a/homeassistant/components/hydrawise/coordinator.py b/homeassistant/components/hydrawise/coordinator.py new file mode 100644 index 00000000000..ea2e2dd2c4c --- /dev/null +++ b/homeassistant/components/hydrawise/coordinator.py @@ -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") diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index 5c54c1ee580..405e5bc3fa3 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -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.""" diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 2cec1309ec9..f47f404cfa0 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -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() diff --git a/homeassistant/components/hydrawise/switch.py b/homeassistant/components/hydrawise/switch.py index ac9b0d27025..71c4f8df79e 100644 --- a/homeassistant/components/hydrawise/switch.py +++ b/homeassistant/components/hydrawise/switch.py @@ -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()