Make Hydrawise initialize data immediately (#101936)

This commit is contained in:
David Knowles 2023-10-30 10:18:59 -04:00 committed by GitHub
parent 92ec525de1
commit f160fa4bc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 45 additions and 86 deletions

View File

@ -2,7 +2,6 @@
from pydrawise import legacy from pydrawise import legacy
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
@ -13,11 +12,10 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN, SCAN_INTERVAL
from .coordinator import HydrawiseDataUpdateCoordinator from .coordinator import HydrawiseDataUpdateCoordinator
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
@ -53,24 +51,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Hydrawise from a config entry.""" """Set up Hydrawise from a config entry."""
access_token = config_entry.data[CONF_API_KEY] access_token = config_entry.data[CONF_API_KEY]
try: hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False)
hydrawise = await hass.async_add_executor_job( coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
legacy.LegacyHydrawise, access_token await coordinator.async_config_entry_first_refresh()
) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
except (ConnectTimeout, HTTPError) as ex:
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
raise ConfigEntryNotReady(
f"Unable to connect to Hydrawise cloud service: {ex}"
) from ex
hass.data.setdefault(DOMAIN, {})[
config_entry.entry_id
] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
if not hydrawise.controller_info or not hydrawise.controller_status:
raise ConfigEntryNotReady("Hydrawise data not loaded")
# NOTE: We don't need to call async_config_entry_first_refresh() because
# data is fetched when the Hydrawiser object is instantiated.
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True return True

View File

@ -12,12 +12,12 @@ from homeassistant.components.binary_sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
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 DOMAIN, LOGGER from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity from .entity import HydrawiseEntity
@ -95,13 +95,10 @@ async def async_setup_entry(
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
"""A sensor implementation for Hydrawise device.""" """A sensor implementation for Hydrawise device."""
@callback def _update_attrs(self) -> None:
def _handle_coordinator_update(self) -> None: """Update state attributes."""
"""Get the latest data and updates the state."""
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
if self.entity_description.key == "status": if self.entity_description.key == "status":
self._attr_is_on = self.coordinator.last_update_success self._attr_is_on = self.coordinator.last_update_success
elif self.entity_description.key == "is_watering": elif self.entity_description.key == "is_watering":
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
self._attr_is_on = relay_data["timestr"] == "Now" self._attr_is_on = relay_data["timestr"] == "Now"
super()._handle_coordinator_update()

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -36,3 +37,14 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
name=data["name"], name=data["name"],
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
) )
self._update_attrs()
def _update_attrs(self) -> None:
"""Update state attributes."""
return # pragma: no cover
@callback
def _handle_coordinator_update(self) -> None:
"""Get the latest data and updates the state."""
self._update_attrs()
super()._handle_coordinator_update()

View File

@ -11,13 +11,13 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
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 as dt_util from homeassistant.util import dt as dt_util
from .const import DOMAIN, LOGGER from .const import DOMAIN
from .coordinator import HydrawiseDataUpdateCoordinator from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity from .entity import HydrawiseEntity
@ -82,10 +82,8 @@ async def async_setup_entry(
class HydrawiseSensor(HydrawiseEntity, SensorEntity): class HydrawiseSensor(HydrawiseEntity, SensorEntity):
"""A sensor implementation for Hydrawise device.""" """A sensor implementation for Hydrawise device."""
@callback def _update_attrs(self) -> None:
def _handle_coordinator_update(self) -> None: """Update state attributes."""
"""Get the latest data and updates the states."""
LOGGER.debug("Updating Hydrawise sensor: %s", self.name)
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]] relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
if self.entity_description.key == "watering_time": if self.entity_description.key == "watering_time":
if relay_data["timestr"] == "Now": if relay_data["timestr"] == "Now":
@ -94,8 +92,6 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
self._attr_native_value = 0 self._attr_native_value = 0
else: # _sensor_type == 'next_cycle' else: # _sensor_type == 'next_cycle'
next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS) next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS)
LOGGER.debug("New cycle time: %s", next_cycle)
self._attr_native_value = dt_util.utc_from_timestamp( self._attr_native_value = dt_util.utc_from_timestamp(
dt_util.as_timestamp(dt_util.now()) + next_cycle dt_util.as_timestamp(dt_util.now()) + next_cycle
) )
super()._handle_coordinator_update()

View File

@ -13,7 +13,7 @@ from homeassistant.components.switch import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
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
@ -23,7 +23,6 @@ from .const import (
CONF_WATERING_TIME, CONF_WATERING_TIME,
DEFAULT_WATERING_TIME, DEFAULT_WATERING_TIME,
DOMAIN, DOMAIN,
LOGGER,
) )
from .coordinator import HydrawiseDataUpdateCoordinator from .coordinator import HydrawiseDataUpdateCoordinator
from .entity import HydrawiseEntity from .entity import HydrawiseEntity
@ -124,14 +123,11 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
elif self.entity_description.key == "auto_watering": elif self.entity_description.key == "auto_watering":
self.coordinator.api.suspend_zone(365, zone_number) self.coordinator.api.suspend_zone(365, zone_number)
@callback def _update_attrs(self) -> None:
def _handle_coordinator_update(self) -> None: """Update state attributes."""
"""Update device state."""
zone_number = self.data["relay"] zone_number = self.data["relay"]
LOGGER.debug("Updating Hydrawise switch: %s", self.name)
timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"] timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"]
if self.entity_description.key == "manual_watering": if self.entity_description.key == "manual_watering":
self._attr_is_on = 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 = timestr not in {"", "Now"} self._attr_is_on = timestr not in {"", "Now"}
super()._handle_coordinator_update()

View File

@ -17,11 +17,6 @@ async def test_states(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test binary_sensor states.""" """Test binary_sensor states."""
# Make the coordinator refresh data.
freezer.tick(SCAN_INTERVAL + timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
connectivity = hass.states.get("binary_sensor.home_controller_connectivity") connectivity = hass.states.get("binary_sensor.home_controller_connectivity")
assert connectivity is not None assert connectivity is not None
assert connectivity.state == "on" assert connectivity.state == "on"

View File

@ -1,6 +1,6 @@
"""Tests for the Hydrawise integration.""" """Tests for the Hydrawise integration."""
from unittest.mock import Mock, patch from unittest.mock import Mock
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
@ -15,6 +15,7 @@ from tests.common import MockConfigEntry
async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) -> None: async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) -> None:
"""Test that setup with a YAML config triggers an import and warning.""" """Test that setup with a YAML config triggers an import and warning."""
mock_pydrawise.update_controller_info.return_value = True
mock_pydrawise.customer_id = 12345 mock_pydrawise.customer_id = 12345
mock_pydrawise.status = "unknown" mock_pydrawise.status = "unknown"
config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}} config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}}
@ -29,29 +30,22 @@ async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) -
async def test_connect_retry( async def test_connect_retry(
hass: HomeAssistant, mock_config_entry: MockConfigEntry hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock
) -> None: ) -> None:
"""Test that a connection error triggers a retry.""" """Test that a connection error triggers a retry."""
with patch("pydrawise.legacy.LegacyHydrawise") as mock_api: mock_pydrawise.update_controller_info.side_effect = HTTPError
mock_api.side_effect = HTTPError mock_config_entry.add_to_hass(hass)
mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done()
await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
mock_api.assert_called_once()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_no_data( async def test_setup_no_data(
hass: HomeAssistant, mock_config_entry: MockConfigEntry hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock
) -> None: ) -> None:
"""Test that no data from the API triggers a retry.""" """Test that no data from the API triggers a retry."""
with patch("pydrawise.legacy.LegacyHydrawise") as mock_api: mock_pydrawise.update_controller_info.return_value = False
mock_api.return_value.controller_info = {} mock_config_entry.add_to_hass(hass)
mock_api.return_value.controller_status = None await hass.config_entries.async_setup(mock_config_entry.entry_id)
mock_config_entry.add_to_hass(hass) await hass.async_block_till_done()
await hass.config_entries.async_setup(mock_config_entry.entry_id) assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
await hass.async_block_till_done()
mock_api.assert_called_once()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@ -1,14 +1,11 @@
"""Test Hydrawise sensor.""" """Test Hydrawise sensor."""
from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant.components.hydrawise.const import SCAN_INTERVAL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry
@pytest.mark.freeze_time("2023-10-01 00:00:00+00:00") @pytest.mark.freeze_time("2023-10-01 00:00:00+00:00")
@ -18,11 +15,6 @@ async def test_states(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test sensor states.""" """Test sensor states."""
# Make the coordinator refresh data.
freezer.tick(SCAN_INTERVAL + timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
watering_time1 = hass.states.get("sensor.zone_one_watering_time") watering_time1 = hass.states.get("sensor.zone_one_watering_time")
assert watering_time1 is not None assert watering_time1 is not None
assert watering_time1.state == "0" assert watering_time1.state == "0"
@ -33,4 +25,4 @@ async def test_states(
next_cycle = hass.states.get("sensor.zone_one_next_cycle") next_cycle = hass.states.get("sensor.zone_one_next_cycle")
assert next_cycle is not None assert next_cycle is not None
assert next_cycle.state == "2023-10-04T19:52:27+00:00" assert next_cycle.state == "2023-10-04T19:49:57+00:00"

View File

@ -1,16 +1,14 @@
"""Test Hydrawise switch.""" """Test Hydrawise switch."""
from datetime import timedelta
from unittest.mock import Mock from unittest.mock import Mock
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.hydrawise.const import SCAN_INTERVAL
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry
async def test_states( async def test_states(
@ -19,11 +17,6 @@ async def test_states(
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test switch states.""" """Test switch states."""
# Make the coordinator refresh data.
freezer.tick(SCAN_INTERVAL + timedelta(seconds=30))
async_fire_time_changed(hass)
await hass.async_block_till_done()
watering1 = hass.states.get("switch.zone_one_manual_watering") watering1 = hass.states.get("switch.zone_one_manual_watering")
assert watering1 is not None assert watering1 is not None
assert watering1.state == "off" assert watering1.state == "off"