mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Gracefully handle bond API errors and timeouts through available state (#38137)
* Gracefully handle API errors and timeouts through available state * Gracefully handle API errors and timeouts through available state
This commit is contained in:
parent
21db4a4160
commit
69203b5373
@ -1,17 +1,20 @@
|
||||
"""The Bond integration."""
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from bond_api import Bond
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import BondHub
|
||||
|
||||
PLATFORMS = ["cover", "fan", "light", "switch"]
|
||||
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
@ -25,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
host = entry.data[CONF_HOST]
|
||||
token = entry.data[CONF_ACCESS_TOKEN]
|
||||
|
||||
bond = Bond(host=host, token=token)
|
||||
bond = Bond(host=host, token=token, timeout=ClientTimeout(total=_API_TIMEOUT))
|
||||
hub = BondHub(bond)
|
||||
await hub.setup()
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
|
@ -1,13 +1,19 @@
|
||||
"""An abstract class common to all Bond entities."""
|
||||
from abc import abstractmethod
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .utils import BondDevice, BondHub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BondEntity(Entity):
|
||||
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
||||
@ -16,6 +22,7 @@ class BondEntity(Entity):
|
||||
"""Initialize entity with API and device info."""
|
||||
self._hub = hub
|
||||
self._device = device
|
||||
self._available = True
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
@ -41,10 +48,26 @@ class BondEntity(Entity):
|
||||
"""Let HA know this entity relies on an assumed state tracked by Bond."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Report availability of this entity based on last API call results."""
|
||||
return self._available
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch assumed state of the cover from the hub using API."""
|
||||
state: dict = await self._hub.bond.device_state(self._device.device_id)
|
||||
self._apply_state(state)
|
||||
try:
|
||||
state: dict = await self._hub.bond.device_state(self._device.device_id)
|
||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||
if self._available:
|
||||
_LOGGER.warning(
|
||||
"Entity %s has become unavailable", self.entity_id, exc_info=error
|
||||
)
|
||||
self._available = False
|
||||
else:
|
||||
if not self._available:
|
||||
_LOGGER.info("Entity %s has come back", self.entity_id)
|
||||
self._available = True
|
||||
self._apply_state(state)
|
||||
|
||||
@abstractmethod
|
||||
def _apply_state(self, state: dict):
|
||||
|
@ -1,13 +1,16 @@
|
||||
"""Common methods used across tests for Bond."""
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, STATE_UNAVAILABLE
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
MOCK_HUB_VERSION: dict = {"bondid": "test-bond-id"}
|
||||
|
||||
@ -74,11 +77,32 @@ def patch_bond_action():
|
||||
return patch("homeassistant.components.bond.Bond.action")
|
||||
|
||||
|
||||
def patch_bond_device_state(return_value=None):
|
||||
def patch_bond_device_state(return_value=None, side_effect=None):
|
||||
"""Patch Bond API device state endpoint."""
|
||||
if return_value is None:
|
||||
return_value = {}
|
||||
|
||||
return patch(
|
||||
"homeassistant.components.bond.Bond.device_state", return_value=return_value
|
||||
"homeassistant.components.bond.Bond.device_state",
|
||||
return_value=return_value,
|
||||
side_effect=side_effect,
|
||||
)
|
||||
|
||||
|
||||
async def help_test_entity_available(
|
||||
hass: core.HomeAssistant, domain: str, device: Dict[str, Any], entity_id: str
|
||||
):
|
||||
"""Run common test to verify available property."""
|
||||
await setup_platform(hass, domain, device)
|
||||
|
||||
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
|
||||
|
||||
with patch_bond_device_state(side_effect=AsyncIOTimeoutError()):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
with patch_bond_device_state(return_value={}):
|
||||
async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
|
||||
|
@ -15,7 +15,12 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
from .common import patch_bond_action, patch_bond_device_state, setup_platform
|
||||
from .common import (
|
||||
help_test_entity_available,
|
||||
patch_bond_action,
|
||||
patch_bond_device_state,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@ -109,3 +114,10 @@ async def test_update_reports_closed_cover(hass: core.HomeAssistant):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("cover.name_1").state == "closed"
|
||||
|
||||
|
||||
async def test_cover_available(hass: core.HomeAssistant):
|
||||
"""Tests that available state is updated based on API errors."""
|
||||
await help_test_entity_available(
|
||||
hass, COVER_DOMAIN, shades("name-1"), "cover.name_1"
|
||||
)
|
||||
|
@ -18,7 +18,12 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
from .common import patch_bond_action, patch_bond_device_state, setup_platform
|
||||
from .common import (
|
||||
help_test_entity_available,
|
||||
patch_bond_action,
|
||||
patch_bond_device_state,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@ -192,3 +197,10 @@ async def test_set_fan_direction(hass: core.HomeAssistant):
|
||||
mock_set_direction.assert_called_once_with(
|
||||
"test-device-id", Action.set_direction(Direction.FORWARD)
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_available(hass: core.HomeAssistant):
|
||||
"""Tests that available state is updated based on API errors."""
|
||||
await help_test_entity_available(
|
||||
hass, FAN_DOMAIN, ceiling_fan("name-1"), "fan.name_1"
|
||||
)
|
||||
|
@ -10,7 +10,12 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
from .common import patch_bond_action, patch_bond_device_state, setup_platform
|
||||
from .common import (
|
||||
help_test_entity_available,
|
||||
patch_bond_action,
|
||||
patch_bond_device_state,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@ -162,3 +167,10 @@ async def test_flame_converted_to_brightness(hass: core.HomeAssistant):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.name_1").attributes[ATTR_BRIGHTNESS] == 128
|
||||
|
||||
|
||||
async def test_light_available(hass: core.HomeAssistant):
|
||||
"""Tests that available state is updated based on API errors."""
|
||||
await help_test_entity_available(
|
||||
hass, LIGHT_DOMAIN, ceiling_fan("name-1"), "light.name_1"
|
||||
)
|
||||
|
@ -10,7 +10,12 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
from .common import patch_bond_action, patch_bond_device_state, setup_platform
|
||||
from .common import (
|
||||
help_test_entity_available,
|
||||
patch_bond_action,
|
||||
patch_bond_device_state,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
@ -86,3 +91,10 @@ async def test_update_reports_switch_is_off(hass: core.HomeAssistant):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("switch.name_1").state == "off"
|
||||
|
||||
|
||||
async def test_switch_available(hass: core.HomeAssistant):
|
||||
"""Tests that available state is updated based on API errors."""
|
||||
await help_test_entity_available(
|
||||
hass, SWITCH_DOMAIN, generic_device("name-1"), "switch.name_1"
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user