mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +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."""
|
"""The Bond integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp import ClientTimeout
|
||||||
from bond_api import Bond
|
from bond_api import Bond
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .utils import BondHub
|
from .utils import BondHub
|
||||||
|
|
||||||
PLATFORMS = ["cover", "fan", "light", "switch"]
|
PLATFORMS = ["cover", "fan", "light", "switch"]
|
||||||
|
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict):
|
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]
|
host = entry.data[CONF_HOST]
|
||||||
token = entry.data[CONF_ACCESS_TOKEN]
|
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)
|
hub = BondHub(bond)
|
||||||
await hub.setup()
|
await hub.setup()
|
||||||
hass.data[DOMAIN][entry.entry_id] = hub
|
hass.data[DOMAIN][entry.entry_id] = hub
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
"""An abstract class common to all Bond entities."""
|
"""An abstract class common to all Bond entities."""
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||||
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from homeassistant.const import ATTR_NAME
|
from homeassistant.const import ATTR_NAME
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .utils import BondDevice, BondHub
|
from .utils import BondDevice, BondHub
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BondEntity(Entity):
|
class BondEntity(Entity):
|
||||||
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
"""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."""
|
"""Initialize entity with API and device info."""
|
||||||
self._hub = hub
|
self._hub = hub
|
||||||
self._device = device
|
self._device = device
|
||||||
|
self._available = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> Optional[str]:
|
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."""
|
"""Let HA know this entity relies on an assumed state tracked by Bond."""
|
||||||
return True
|
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):
|
async def async_update(self):
|
||||||
"""Fetch assumed state of the cover from the hub using API."""
|
"""Fetch assumed state of the cover from the hub using API."""
|
||||||
state: dict = await self._hub.bond.device_state(self._device.device_id)
|
try:
|
||||||
self._apply_state(state)
|
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
|
@abstractmethod
|
||||||
def _apply_state(self, state: dict):
|
def _apply_state(self, state: dict):
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
"""Common methods used across tests for Bond."""
|
"""Common methods used across tests for Bond."""
|
||||||
|
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN
|
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.setup import async_setup_component
|
||||||
|
from homeassistant.util import utcnow
|
||||||
|
|
||||||
from tests.async_mock import patch
|
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"}
|
MOCK_HUB_VERSION: dict = {"bondid": "test-bond-id"}
|
||||||
|
|
||||||
@ -74,11 +77,32 @@ def patch_bond_action():
|
|||||||
return patch("homeassistant.components.bond.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."""
|
"""Patch Bond API device state endpoint."""
|
||||||
if return_value is None:
|
if return_value is None:
|
||||||
return_value = {}
|
return_value = {}
|
||||||
|
|
||||||
return patch(
|
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.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.util import utcnow
|
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
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("cover.name_1").state == "closed"
|
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.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.util import utcnow
|
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
|
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(
|
mock_set_direction.assert_called_once_with(
|
||||||
"test-device-id", Action.set_direction(Direction.FORWARD)
|
"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.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.util import utcnow
|
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
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("light.name_1").attributes[ATTR_BRIGHTNESS] == 128
|
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.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.util import utcnow
|
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
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get("switch.name_1").state == "off"
|
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