From 0d1fb1fc9ff3eda2ddc740a358fdd12ac8410098 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 5 Jun 2024 08:18:41 -0400 Subject: [PATCH] Fix Hydrawise sensor availability (#118669) Co-authored-by: Robert Resch --- .../components/hydrawise/binary_sensor.py | 13 +++- homeassistant/components/hydrawise/entity.py | 5 ++ .../hydrawise/test_binary_sensor.py | 24 ++++++- .../hydrawise/test_entity_availability.py | 65 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/components/hydrawise/test_entity_availability.py diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index d3382dbce39..e8426e5423a 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -24,13 +24,17 @@ class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Hydrawise binary sensor.""" value_fn: Callable[[HydrawiseBinarySensor], bool | None] + always_available: bool = False CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = ( HydrawiseBinarySensorEntityDescription( key="status", device_class=BinarySensorDeviceClass.CONNECTIVITY, - value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success, + value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success + and status_sensor.controller.online, + # Connectivtiy sensor is always available + always_available=True, ), ) @@ -98,3 +102,10 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): def _update_attrs(self) -> None: """Update state attributes.""" self._attr_is_on = self.entity_description.value_fn(self) + + @property + def available(self) -> bool: + """Set the entity availability.""" + if self.entity_description.always_available: + return True + return super().available diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index 7b3ce6551a5..67dd6375b0e 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -70,3 +70,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]): self.controller = self.coordinator.data.controllers[self.controller.id] self._update_attrs() super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Set the entity availability.""" + return super().available and self.controller.online diff --git a/tests/components/hydrawise/test_binary_sensor.py b/tests/components/hydrawise/test_binary_sensor.py index 6343b345d99..a42f9b1c044 100644 --- a/tests/components/hydrawise/test_binary_sensor.py +++ b/tests/components/hydrawise/test_binary_sensor.py @@ -6,10 +6,11 @@ from unittest.mock import AsyncMock, patch from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory +from pydrawise.schema import Controller from syrupy.assertion import SnapshotAssertion from homeassistant.components.hydrawise.const import SCAN_INTERVAL -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -47,4 +48,23 @@ async def test_update_data_fails( connectivity = hass.states.get("binary_sensor.home_controller_connectivity") assert connectivity is not None - assert connectivity.state == "unavailable" + assert connectivity.state == STATE_OFF + + +async def test_controller_offline( + hass: HomeAssistant, + mock_added_config_entry: MockConfigEntry, + mock_pydrawise: AsyncMock, + freezer: FrozenDateTimeFactory, + controller: Controller, +) -> None: + """Test the binary_sensor for the controller being online.""" + # Make the coordinator refresh data. + controller.online = False + 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") + assert connectivity + assert connectivity.state == STATE_OFF diff --git a/tests/components/hydrawise/test_entity_availability.py b/tests/components/hydrawise/test_entity_availability.py new file mode 100644 index 00000000000..58ded5fe6c3 --- /dev/null +++ b/tests/components/hydrawise/test_entity_availability.py @@ -0,0 +1,65 @@ +"""Test entity availability.""" + +from collections.abc import Awaitable, Callable +from datetime import timedelta +from unittest.mock import AsyncMock + +from aiohttp import ClientError +from freezegun.api import FrozenDateTimeFactory +from pydrawise.schema import Controller + +from homeassistant.components.hydrawise.const import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, async_fire_time_changed + +_SPECIAL_ENTITIES = {"binary_sensor.home_controller_connectivity": STATE_OFF} + + +async def test_controller_offline( + hass: HomeAssistant, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], + entity_registry: er.EntityRegistry, + controller: Controller, +) -> None: + """Test availability for sensors when controller is offline.""" + controller.online = False + config_entry = await mock_add_config_entry() + _test_availability(hass, config_entry, entity_registry) + + +async def test_api_offline( + hass: HomeAssistant, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], + entity_registry: er.EntityRegistry, + mock_pydrawise: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test availability of sensors when API call fails.""" + config_entry = await mock_add_config_entry() + 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() + _test_availability(hass, config_entry, entity_registry) + + +def _test_availability( + hass: HomeAssistant, + config_entry: ConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + entity_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert entity_entries + for entity_entry in entity_entries: + state = hass.states.get(entity_entry.entity_id) + assert state, f"State not found for {entity_entry.entity_id}" + assert state.state == _SPECIAL_ENTITIES.get( + entity_entry.entity_id, STATE_UNAVAILABLE + )