mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Migrate Hydrawise to an async client library (#103636)
* Migrate Hydrawise to an async client library * Changes requested during review * Additional changes requested during review
This commit is contained in:
parent
45f1d50f03
commit
0899be6d4b
@ -51,7 +51,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Hydrawise from a config entry."""
|
||||
access_token = config_entry.data[CONF_API_KEY]
|
||||
hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False)
|
||||
hydrawise = legacy.LegacyHydrawiseAsync(access_token)
|
||||
coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Support for Hydrawise sprinkler binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pydrawise.legacy import LegacyHydrawise
|
||||
from pydrawise.schema import Zone
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@ -69,26 +69,16 @@ async def async_setup_entry(
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
hydrawise: LegacyHydrawise = coordinator.api
|
||||
|
||||
entities = [
|
||||
HydrawiseBinarySensor(
|
||||
data=hydrawise.current_controller,
|
||||
coordinator=coordinator,
|
||||
description=BINARY_SENSOR_STATUS,
|
||||
device_id_key="controller_id",
|
||||
entities = []
|
||||
for controller in coordinator.data.controllers:
|
||||
entities.append(
|
||||
HydrawiseBinarySensor(coordinator, BINARY_SENSOR_STATUS, controller)
|
||||
)
|
||||
]
|
||||
|
||||
# create a sensor for each zone
|
||||
for zone in hydrawise.relays:
|
||||
for description in BINARY_SENSOR_TYPES:
|
||||
entities.append(
|
||||
HydrawiseBinarySensor(
|
||||
data=zone, coordinator=coordinator, description=description
|
||||
for zone in controller.zones:
|
||||
for description in BINARY_SENSOR_TYPES:
|
||||
entities.append(
|
||||
HydrawiseBinarySensor(coordinator, description, controller, zone)
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@ -100,5 +90,5 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
if self.entity_description.key == "status":
|
||||
self._attr_is_on = self.coordinator.last_update_success
|
||||
elif self.entity_description.key == "is_watering":
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
self._attr_is_on = relay_data["timestr"] == "Now"
|
||||
zone: Zone = self.zone
|
||||
self._attr_is_on = zone.scheduled_runs.current_run is not None
|
||||
|
@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from pydrawise import legacy
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -27,20 +27,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, api_key: str, *, on_failure: Callable[[str], FlowResult]
|
||||
) -> FlowResult:
|
||||
"""Create the config entry."""
|
||||
api = legacy.LegacyHydrawiseAsync(api_key)
|
||||
try:
|
||||
api = await self.hass.async_add_executor_job(
|
||||
legacy.LegacyHydrawise, api_key
|
||||
)
|
||||
except ConnectTimeout:
|
||||
# Skip fetching zones to save on metered API calls.
|
||||
user = await api.get_user(fetch_zones=False)
|
||||
except TimeoutError:
|
||||
return on_failure("timeout_connect")
|
||||
except HTTPError as ex:
|
||||
except ClientError as ex:
|
||||
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", ex)
|
||||
return on_failure("cannot_connect")
|
||||
|
||||
if not api.status:
|
||||
return on_failure("unknown")
|
||||
|
||||
await self.async_set_unique_id(f"hydrawise-{api.customer_id}")
|
||||
await self.async_set_unique_id(f"hydrawise-{user.customer_id}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key})
|
||||
|
@ -4,26 +4,25 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from pydrawise.legacy import LegacyHydrawise
|
||||
from pydrawise import HydrawiseBase
|
||||
from pydrawise.schema import User
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[User]):
|
||||
"""The Hydrawise Data Update Coordinator."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: LegacyHydrawise, scan_interval: timedelta
|
||||
self, hass: HomeAssistant, api: HydrawiseBase, 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:
|
||||
async def _async_update_data(self) -> User:
|
||||
"""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")
|
||||
return await self.api.get_user()
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Base classes for Hydrawise entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from pydrawise.schema import Controller, Zone
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@ -20,23 +20,25 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
data: dict[str, Any],
|
||||
coordinator: HydrawiseDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
device_id_key: str = "relay_id",
|
||||
controller: Controller,
|
||||
zone: Zone | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Hydrawise entity."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self.data = data
|
||||
self.entity_description = description
|
||||
self._device_id = str(data.get(device_id_key))
|
||||
self.controller = controller
|
||||
self.zone = zone
|
||||
self._device_id = str(controller.id if zone is None else zone.id)
|
||||
self._attr_unique_id = f"{self._device_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_id)},
|
||||
name=data["name"],
|
||||
name=controller.name if zone is None else zone.name,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
if zone is not None:
|
||||
self._attr_device_info["via_device"] = (DOMAIN, str(controller.id))
|
||||
self._update_attrs()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Support for Hydrawise sprinkler sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydrawise.schema import Zone
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@ -71,27 +74,30 @@ async def async_setup_entry(
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
entities = [
|
||||
HydrawiseSensor(data=zone, coordinator=coordinator, description=description)
|
||||
for zone in coordinator.api.relays
|
||||
async_add_entities(
|
||||
HydrawiseSensor(coordinator, description, controller, zone)
|
||||
for controller in coordinator.data.controllers
|
||||
for zone in controller.zones
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
)
|
||||
|
||||
|
||||
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
zone: Zone
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
if self.entity_description.key == "watering_time":
|
||||
if relay_data["timestr"] == "Now":
|
||||
self._attr_native_value = int(relay_data["run"] / 60)
|
||||
if (current_run := self.zone.scheduled_runs.current_run) is not None:
|
||||
self._attr_native_value = int(
|
||||
current_run.remaining_time.total_seconds() / 60
|
||||
)
|
||||
else:
|
||||
self._attr_native_value = 0
|
||||
else: # _sensor_type == 'next_cycle'
|
||||
next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS)
|
||||
self._attr_native_value = dt_util.utc_from_timestamp(
|
||||
dt_util.as_timestamp(dt_util.now()) + next_cycle
|
||||
)
|
||||
elif self.entity_description.key == "next_cycle":
|
||||
if (next_run := self.zone.scheduled_runs.next_run) is not None:
|
||||
self._attr_native_value = dt_util.as_utc(next_run.start_time)
|
||||
else:
|
||||
self._attr_native_value = datetime.max.replace(tzinfo=dt_util.UTC)
|
||||
|
@ -1,8 +1,10 @@
|
||||
"""Support for Hydrawise cloud switches."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pydrawise.schema import Zone
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
@ -17,6 +19,7 @@ from homeassistant.core import HomeAssistant
|
||||
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 as dt_util
|
||||
|
||||
from .const import (
|
||||
ALLOWED_WATERING_TIME,
|
||||
@ -76,62 +79,44 @@ async def async_setup_entry(
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
default_watering_timer = DEFAULT_WATERING_TIME
|
||||
|
||||
entities = [
|
||||
HydrawiseSwitch(
|
||||
data=zone,
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
default_watering_timer=default_watering_timer,
|
||||
)
|
||||
for zone in coordinator.api.relays
|
||||
async_add_entities(
|
||||
HydrawiseSwitch(coordinator, description, controller, zone)
|
||||
for controller in coordinator.data.controllers
|
||||
for zone in controller.zones
|
||||
for description in SWITCH_TYPES
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
)
|
||||
|
||||
|
||||
class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
|
||||
"""A switch implementation for Hydrawise device."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
data: dict[str, Any],
|
||||
coordinator: HydrawiseDataUpdateCoordinator,
|
||||
description: SwitchEntityDescription,
|
||||
default_watering_timer: int,
|
||||
) -> None:
|
||||
"""Initialize a switch for Hydrawise device."""
|
||||
super().__init__(data=data, coordinator=coordinator, description=description)
|
||||
self._default_watering_timer = default_watering_timer
|
||||
zone: Zone
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
zone_number = self.data["relay"]
|
||||
if self.entity_description.key == "manual_watering":
|
||||
self.coordinator.api.run_zone(self._default_watering_timer, zone_number)
|
||||
await self.coordinator.api.start_zone(
|
||||
self.zone, custom_run_duration=DEFAULT_WATERING_TIME
|
||||
)
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self.coordinator.api.suspend_zone(0, zone_number)
|
||||
await self.coordinator.api.resume_zone(self.zone)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
zone_number = self.data["relay"]
|
||||
if self.entity_description.key == "manual_watering":
|
||||
self.coordinator.api.run_zone(0, zone_number)
|
||||
await self.coordinator.api.stop_zone(self.zone)
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self.coordinator.api.suspend_zone(365, zone_number)
|
||||
await self.coordinator.api.suspend_zone(
|
||||
self.zone, dt_util.now() + timedelta(days=365)
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
zone_number = self.data["relay"]
|
||||
timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"]
|
||||
if self.entity_description.key == "manual_watering":
|
||||
self._attr_is_on = timestr == "Now"
|
||||
self._attr_is_on = self.zone.scheduled_runs.current_run is not None
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self._attr_is_on = timestr not in {"", "Now"}
|
||||
self._attr_is_on = self.zone.status.suspended_until is None
|
||||
|
@ -1,14 +1,23 @@
|
||||
"""Common fixtures for the Hydrawise tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pydrawise.schema import (
|
||||
Controller,
|
||||
ControllerHardware,
|
||||
ScheduledZoneRun,
|
||||
ScheduledZoneRuns,
|
||||
User,
|
||||
Zone,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hydrawise.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -24,59 +33,71 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pydrawise(
|
||||
mock_controller: dict[str, Any],
|
||||
mock_zones: list[dict[str, Any]],
|
||||
) -> Generator[Mock, None, None]:
|
||||
"""Mock LegacyHydrawise."""
|
||||
with patch("pydrawise.legacy.LegacyHydrawise", autospec=True) as mock_pydrawise:
|
||||
mock_pydrawise.return_value.controller_info = {"controllers": [mock_controller]}
|
||||
mock_pydrawise.return_value.current_controller = mock_controller
|
||||
mock_pydrawise.return_value.controller_status = {"relays": mock_zones}
|
||||
mock_pydrawise.return_value.relays = mock_zones
|
||||
mock_pydrawise.return_value.relays_by_zone_number = {
|
||||
r["relay"]: r for r in mock_zones
|
||||
}
|
||||
user: User,
|
||||
controller: Controller,
|
||||
zones: list[Zone],
|
||||
) -> Generator[AsyncMock, None, None]:
|
||||
"""Mock LegacyHydrawiseAsync."""
|
||||
with patch(
|
||||
"pydrawise.legacy.LegacyHydrawiseAsync", autospec=True
|
||||
) as mock_pydrawise:
|
||||
user.controllers = [controller]
|
||||
controller.zones = zones
|
||||
mock_pydrawise.return_value.get_user.return_value = user
|
||||
yield mock_pydrawise.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_controller() -> dict[str, Any]:
|
||||
"""Mock Hydrawise controller."""
|
||||
return {
|
||||
"name": "Home Controller",
|
||||
"last_contact": 1693292420,
|
||||
"serial_number": "0310b36090",
|
||||
"controller_id": 52496,
|
||||
"status": "Unknown",
|
||||
}
|
||||
def user() -> User:
|
||||
"""Hydrawise User fixture."""
|
||||
return User(customer_id=12345)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_zones() -> list[dict[str, Any]]:
|
||||
"""Mock Hydrawise zones."""
|
||||
def controller() -> Controller:
|
||||
"""Hydrawise Controller fixture."""
|
||||
return Controller(
|
||||
id=52496,
|
||||
name="Home Controller",
|
||||
hardware=ControllerHardware(
|
||||
serial_number="0310b36090",
|
||||
),
|
||||
last_contact_time=datetime.fromtimestamp(1693292420),
|
||||
online=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def zones() -> list[Zone]:
|
||||
"""Hydrawise zone fixtures."""
|
||||
return [
|
||||
{
|
||||
"name": "Zone One",
|
||||
"period": 259200,
|
||||
"relay": 1,
|
||||
"relay_id": 5965394,
|
||||
"run": 1800,
|
||||
"stop": 1,
|
||||
"time": 330597,
|
||||
"timestr": "Sat",
|
||||
"type": 1,
|
||||
},
|
||||
{
|
||||
"name": "Zone Two",
|
||||
"period": 259200,
|
||||
"relay": 2,
|
||||
"relay_id": 5965395,
|
||||
"run": 1788,
|
||||
"stop": 1,
|
||||
"time": 1,
|
||||
"timestr": "Now",
|
||||
"type": 106,
|
||||
},
|
||||
Zone(
|
||||
name="Zone One",
|
||||
number=1,
|
||||
id=5965394,
|
||||
scheduled_runs=ScheduledZoneRuns(
|
||||
summary="",
|
||||
current_run=None,
|
||||
next_run=ScheduledZoneRun(
|
||||
start_time=dt_util.now() + timedelta(seconds=330597),
|
||||
end_time=dt_util.now()
|
||||
+ timedelta(seconds=330597)
|
||||
+ timedelta(seconds=1800),
|
||||
normal_duration=timedelta(seconds=1800),
|
||||
duration=timedelta(seconds=1800),
|
||||
),
|
||||
),
|
||||
),
|
||||
Zone(
|
||||
name="Zone Two",
|
||||
number=2,
|
||||
id=5965395,
|
||||
scheduled_runs=ScheduledZoneRuns(
|
||||
current_run=ScheduledZoneRun(
|
||||
remaining_time=timedelta(seconds=1788),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@ -95,13 +116,25 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_added_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: Mock,
|
||||
mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]]
|
||||
) -> MockConfigEntry:
|
||||
"""Mock ConfigEntry that's been added to HA."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert DOMAIN in hass.config_entries.async_domains()
|
||||
return mock_config_entry
|
||||
return await mock_add_config_entry()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_add_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
) -> Callable[[], Awaitable[MockConfigEntry]]:
|
||||
"""Callable that creates a mock ConfigEntry that's been added to HA."""
|
||||
|
||||
async def callback() -> MockConfigEntry:
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert DOMAIN in hass.config_entries.async_domains()
|
||||
return mock_config_entry
|
||||
|
||||
return callback
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Test Hydrawise binary_sensor."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aiohttp import ClientError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.components.hydrawise.const import SCAN_INTERVAL
|
||||
@ -33,12 +34,13 @@ async def test_states(
|
||||
async def test_update_data_fails(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: Mock,
|
||||
mock_pydrawise: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test that no data from the API sets the correct connectivity."""
|
||||
# Make the coordinator refresh data.
|
||||
mock_pydrawise.update_controller_info.return_value = None
|
||||
mock_pydrawise.get_user.reset_mock(return_value=True)
|
||||
mock_pydrawise.get_user.side_effect = ClientError
|
||||
freezer.tick(SCAN_INTERVAL + timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""Test the Hydrawise config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aiohttp import ClientError
|
||||
from pydrawise.schema import User
|
||||
import pytest
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hydrawise.const import DOMAIN
|
||||
@ -17,9 +18,11 @@ from tests.common import MockConfigEntry
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form(
|
||||
mock_api: MagicMock, hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_pydrawise: AsyncMock,
|
||||
user: User,
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -32,19 +35,22 @@ async def test_form(
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"api_key": "abc123"}
|
||||
)
|
||||
mock_api.return_value.customer_id = 12345
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Hydrawise"
|
||||
assert result2["data"] == {"api_key": "abc123"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
mock_pydrawise.get_user.assert_called_once_with(fetch_zones=False)
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
async def test_form_api_error(
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
|
||||
) -> None:
|
||||
"""Test we handle API errors."""
|
||||
mock_api.side_effect = HTTPError
|
||||
mock_pydrawise.get_user.side_effect = ClientError("XXX")
|
||||
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -55,15 +61,17 @@ async def test_form_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_api.side_effect = None
|
||||
mock_pydrawise.get_user.reset_mock(side_effect=True)
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form_connect_timeout(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
async def test_form_connect_timeout(
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
|
||||
) -> None:
|
||||
"""Test we handle API errors."""
|
||||
mock_api.side_effect = ConnectTimeout
|
||||
mock_pydrawise.get_user.side_effect = TimeoutError
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -75,15 +83,17 @@ async def test_form_connect_timeout(mock_api: MagicMock, hass: HomeAssistant) ->
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "timeout_connect"}
|
||||
|
||||
mock_api.side_effect = None
|
||||
mock_pydrawise.get_user.reset_mock(side_effect=True)
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_success(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
async def test_flow_import_success(
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
|
||||
) -> None:
|
||||
"""Test that we can import a YAML config."""
|
||||
mock_api.return_value.status = "All good!"
|
||||
mock_pydrawise.get_user.return_value = User
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
@ -107,9 +117,11 @@ async def test_flow_import_success(mock_api: MagicMock, hass: HomeAssistant) ->
|
||||
assert issue.translation_key == "deprecated_yaml"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise", side_effect=HTTPError)
|
||||
async def test_flow_import_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
async def test_flow_import_api_error(
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock
|
||||
) -> None:
|
||||
"""Test that we handle API errors on YAML import."""
|
||||
mock_pydrawise.get_user.side_effect = ClientError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
@ -129,11 +141,11 @@ async def test_flow_import_api_error(mock_api: MagicMock, hass: HomeAssistant) -
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise", side_effect=ConnectTimeout)
|
||||
async def test_flow_import_connect_timeout(
|
||||
mock_api: MagicMock, hass: HomeAssistant
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock
|
||||
) -> None:
|
||||
"""Test that we handle connection timeouts on YAML import."""
|
||||
mock_pydrawise.get_user.side_effect = TimeoutError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
@ -153,32 +165,8 @@ async def test_flow_import_connect_timeout(
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_no_status(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test we handle a lack of API status on YAML import."""
|
||||
mock_api.return_value.status = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_unknown"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_already_imported(
|
||||
mock_api: MagicMock, hass: HomeAssistant
|
||||
hass: HomeAssistant, mock_pydrawise: AsyncMock, user: User
|
||||
) -> None:
|
||||
"""Test that we can handle a YAML config already imported."""
|
||||
mock_config_entry = MockConfigEntry(
|
||||
@ -187,12 +175,12 @@ async def test_flow_import_already_imported(
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
},
|
||||
unique_id="hydrawise-CUSTOMER_ID",
|
||||
unique_id="hydrawise-12345",
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_api.return_value.customer_id = "CUSTOMER_ID"
|
||||
mock_api.return_value.status = "All good!"
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Tests for the Hydrawise integration."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
@ -13,11 +13,10 @@ from homeassistant.setup import async_setup_component
|
||||
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: AsyncMock
|
||||
) -> None:
|
||||
"""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.status = "unknown"
|
||||
config = {"hydrawise": {CONF_ACCESS_TOKEN: "_access-token_"}}
|
||||
assert await async_setup_component(hass, "hydrawise", config)
|
||||
await hass.async_block_till_done()
|
||||
@ -30,21 +29,10 @@ async def test_setup_import_success(hass: HomeAssistant, mock_pydrawise: Mock) -
|
||||
|
||||
|
||||
async def test_connect_retry(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: AsyncMock
|
||||
) -> None:
|
||||
"""Test that a connection error triggers a retry."""
|
||||
mock_pydrawise.update_controller_info.side_effect = HTTPError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_no_data(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_pydrawise: Mock
|
||||
) -> None:
|
||||
"""Test that no data from the API triggers a retry."""
|
||||
mock_pydrawise.update_controller_info.return_value = False
|
||||
mock_pydrawise.get_user.side_effect = ClientError
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Test Hydrawise sensor."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pydrawise.schema import Zone
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -26,3 +29,18 @@ async def test_states(
|
||||
next_cycle = hass.states.get("sensor.zone_one_next_cycle")
|
||||
assert next_cycle is not None
|
||||
assert next_cycle.state == "2023-10-04T19:49:57+00:00"
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-10-01 00:00:00+00:00")
|
||||
async def test_suspended_state(
|
||||
hass: HomeAssistant,
|
||||
zones: list[Zone],
|
||||
mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]],
|
||||
) -> None:
|
||||
"""Test sensor states."""
|
||||
zones[0].scheduled_runs.next_run = None
|
||||
await mock_add_config_entry()
|
||||
|
||||
next_cycle = hass.states.get("sensor.zone_one_next_cycle")
|
||||
assert next_cycle is not None
|
||||
assert next_cycle.state == "9999-12-31T23:59:59+00:00"
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""Test Hydrawise switch."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pydrawise.schema import Zone
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hydrawise.const import DEFAULT_WATERING_TIME
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@ -14,7 +18,6 @@ from tests.common import MockConfigEntry
|
||||
async def test_states(
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test switch states."""
|
||||
watering1 = hass.states.get("switch.zone_one_manual_watering")
|
||||
@ -31,11 +34,14 @@ async def test_states(
|
||||
|
||||
auto_watering2 = hass.states.get("switch.zone_two_automatic_watering")
|
||||
assert auto_watering2 is not None
|
||||
assert auto_watering2.state == "off"
|
||||
assert auto_watering2.state == "on"
|
||||
|
||||
|
||||
async def test_manual_watering_services(
|
||||
hass: HomeAssistant, mock_added_config_entry: MockConfigEntry, mock_pydrawise: Mock
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test Manual Watering services."""
|
||||
await hass.services.async_call(
|
||||
@ -44,7 +50,9 @@ async def test_manual_watering_services(
|
||||
service_data={ATTR_ENTITY_ID: "switch.zone_one_manual_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.run_zone.assert_called_once_with(15, 1)
|
||||
mock_pydrawise.start_zone.assert_called_once_with(
|
||||
zones[0], custom_run_duration=DEFAULT_WATERING_TIME
|
||||
)
|
||||
state = hass.states.get("switch.zone_one_manual_watering")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
@ -56,14 +64,18 @@ async def test_manual_watering_services(
|
||||
service_data={ATTR_ENTITY_ID: "switch.zone_one_manual_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.run_zone.assert_called_once_with(0, 1)
|
||||
mock_pydrawise.stop_zone.assert_called_once_with(zones[0])
|
||||
state = hass.states.get("switch.zone_one_manual_watering")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-10-01 00:00:00+00:00")
|
||||
async def test_auto_watering_services(
|
||||
hass: HomeAssistant, mock_added_config_entry: MockConfigEntry, mock_pydrawise: Mock
|
||||
hass: HomeAssistant,
|
||||
mock_added_config_entry: MockConfigEntry,
|
||||
mock_pydrawise: AsyncMock,
|
||||
zones: list[Zone],
|
||||
) -> None:
|
||||
"""Test Automatic Watering services."""
|
||||
await hass.services.async_call(
|
||||
@ -72,7 +84,9 @@ async def test_auto_watering_services(
|
||||
service_data={ATTR_ENTITY_ID: "switch.zone_one_automatic_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.suspend_zone.assert_called_once_with(365, 1)
|
||||
mock_pydrawise.suspend_zone.assert_called_once_with(
|
||||
zones[0], dt_util.now() + timedelta(days=365)
|
||||
)
|
||||
state = hass.states.get("switch.zone_one_automatic_watering")
|
||||
assert state is not None
|
||||
assert state.state == "off"
|
||||
@ -84,7 +98,7 @@ async def test_auto_watering_services(
|
||||
service_data={ATTR_ENTITY_ID: "switch.zone_one_automatic_watering"},
|
||||
blocking=True,
|
||||
)
|
||||
mock_pydrawise.suspend_zone.assert_called_once_with(0, 1)
|
||||
mock_pydrawise.resume_zone.assert_called_once_with(zones[0])
|
||||
state = hass.states.get("switch.zone_one_automatic_watering")
|
||||
assert state is not None
|
||||
assert state.state == "on"
|
||||
|
Loading…
x
Reference in New Issue
Block a user