2020-07-11 01:20:50 +00:00
|
|
|
"""An abstract class common to all Bond entities."""
|
2021-03-17 22:34:25 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-07-23 02:15:27 +00:00
|
|
|
from abc import abstractmethod
|
2021-02-09 08:43:38 +00:00
|
|
|
from asyncio import Lock, TimeoutError as AsyncIOTimeoutError
|
2022-12-28 17:14:38 +00:00
|
|
|
from datetime import datetime, timedelta
|
2020-07-24 20:14:47 +00:00
|
|
|
import logging
|
2020-07-11 01:20:50 +00:00
|
|
|
|
2020-07-24 20:14:47 +00:00
|
|
|
from aiohttp import ClientError
|
2022-05-26 04:12:43 +00:00
|
|
|
from bond_async import BPUPSubscriptions
|
2020-07-24 20:14:47 +00:00
|
|
|
|
2021-10-23 19:01:34 +00:00
|
|
|
from homeassistant.const import (
|
2021-12-19 06:30:44 +00:00
|
|
|
ATTR_HW_VERSION,
|
2021-10-23 19:01:34 +00:00
|
|
|
ATTR_MODEL,
|
|
|
|
ATTR_NAME,
|
|
|
|
ATTR_SUGGESTED_AREA,
|
|
|
|
ATTR_SW_VERSION,
|
|
|
|
ATTR_VIA_DEVICE,
|
|
|
|
)
|
2023-04-09 03:12:42 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE, callback
|
2021-04-30 21:21:39 +00:00
|
|
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
2023-04-09 03:12:42 +00:00
|
|
|
from homeassistant.helpers.event import async_call_later
|
2020-07-11 01:20:50 +00:00
|
|
|
|
|
|
|
from .const import DOMAIN
|
2020-07-12 16:31:53 +00:00
|
|
|
from .utils import BondDevice, BondHub
|
2020-07-11 01:20:50 +00:00
|
|
|
|
2020-07-24 20:14:47 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2021-02-09 08:43:38 +00:00
|
|
|
_FALLBACK_SCAN_INTERVAL = timedelta(seconds=10)
|
2023-04-09 03:12:42 +00:00
|
|
|
_BPUP_ALIVE_SCAN_INTERVAL = timedelta(seconds=60)
|
2021-02-09 08:43:38 +00:00
|
|
|
|
2020-07-11 01:20:50 +00:00
|
|
|
|
2020-07-23 02:15:27 +00:00
|
|
|
class BondEntity(Entity):
|
2020-07-11 01:20:50 +00:00
|
|
|
"""Generic Bond entity encapsulating common features of any Bond controlled device."""
|
|
|
|
|
2021-07-16 21:06:18 +00:00
|
|
|
_attr_should_poll = False
|
|
|
|
|
2020-10-18 19:11:24 +00:00
|
|
|
def __init__(
|
2021-02-09 08:43:38 +00:00
|
|
|
self,
|
|
|
|
hub: BondHub,
|
|
|
|
device: BondDevice,
|
|
|
|
bpup_subs: BPUPSubscriptions,
|
2021-03-17 22:34:25 +00:00
|
|
|
sub_device: str | None = None,
|
2022-01-23 04:52:00 +00:00
|
|
|
sub_device_id: str | None = None,
|
2021-05-20 15:51:39 +00:00
|
|
|
) -> None:
|
2020-07-11 01:20:50 +00:00
|
|
|
"""Initialize entity with API and device info."""
|
2020-07-12 16:31:53 +00:00
|
|
|
self._hub = hub
|
2020-07-11 01:20:50 +00:00
|
|
|
self._device = device
|
2021-02-09 08:43:38 +00:00
|
|
|
self._device_id = device.device_id
|
2020-10-18 19:11:24 +00:00
|
|
|
self._sub_device = sub_device
|
2021-07-16 21:06:18 +00:00
|
|
|
self._attr_available = True
|
2021-02-09 08:43:38 +00:00
|
|
|
self._bpup_subs = bpup_subs
|
2022-12-28 17:14:38 +00:00
|
|
|
self._update_lock = Lock()
|
2021-02-09 08:43:38 +00:00
|
|
|
self._initialized = False
|
2022-01-23 04:52:00 +00:00
|
|
|
if sub_device_id:
|
|
|
|
sub_device_id = f"_{sub_device_id}"
|
|
|
|
elif sub_device:
|
|
|
|
sub_device_id = f"_{sub_device}"
|
|
|
|
else:
|
|
|
|
sub_device_id = ""
|
2021-07-16 21:06:18 +00:00
|
|
|
self._attr_unique_id = f"{hub.bond_id}_{device.device_id}{sub_device_id}"
|
|
|
|
if sub_device:
|
|
|
|
sub_device_name = sub_device.replace("_", " ").title()
|
|
|
|
self._attr_name = f"{device.name} {sub_device_name}"
|
|
|
|
else:
|
|
|
|
self._attr_name = device.name
|
2022-06-15 06:30:59 +00:00
|
|
|
self._attr_assumed_state = self._hub.is_bridge and not self._device.trust_state
|
|
|
|
self._apply_state()
|
2023-04-09 03:12:42 +00:00
|
|
|
self._bpup_polling_fallback: CALLBACK_TYPE | None = None
|
2021-02-09 08:43:38 +00:00
|
|
|
|
2020-07-11 01:20:50 +00:00
|
|
|
@property
|
2021-04-30 21:21:39 +00:00
|
|
|
def device_info(self) -> DeviceInfo:
|
2020-07-11 01:20:50 +00:00
|
|
|
"""Get a an HA device representing this Bond controlled device."""
|
2021-10-22 15:04:25 +00:00
|
|
|
device_info = DeviceInfo(
|
|
|
|
manufacturer=self._hub.make,
|
2021-04-30 21:21:39 +00:00
|
|
|
# type ignore: tuple items should not be Optional
|
2021-10-22 15:04:25 +00:00
|
|
|
identifiers={(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type]
|
2021-11-29 07:44:11 +00:00
|
|
|
configuration_url=f"http://{self._hub.host}",
|
2021-10-22 15:04:25 +00:00
|
|
|
)
|
2021-04-30 21:21:39 +00:00
|
|
|
if self.name is not None:
|
2022-01-23 04:52:00 +00:00
|
|
|
device_info[ATTR_NAME] = self._device.name
|
2021-04-30 21:21:39 +00:00
|
|
|
if self._hub.bond_id is not None:
|
2021-10-22 15:04:25 +00:00
|
|
|
device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub.bond_id)
|
2021-04-30 21:21:39 +00:00
|
|
|
if self._device.location is not None:
|
2021-10-23 19:01:34 +00:00
|
|
|
device_info[ATTR_SUGGESTED_AREA] = self._device.location
|
2021-02-08 23:37:32 +00:00
|
|
|
if not self._hub.is_bridge:
|
2021-04-30 21:21:39 +00:00
|
|
|
if self._hub.model is not None:
|
2021-10-22 15:04:25 +00:00
|
|
|
device_info[ATTR_MODEL] = self._hub.model
|
2021-04-30 21:21:39 +00:00
|
|
|
if self._hub.fw_ver is not None:
|
2021-10-22 15:04:25 +00:00
|
|
|
device_info[ATTR_SW_VERSION] = self._hub.fw_ver
|
2021-12-19 06:30:44 +00:00
|
|
|
if self._hub.mcu_ver is not None:
|
|
|
|
device_info[ATTR_HW_VERSION] = self._hub.mcu_ver
|
2021-02-08 23:37:32 +00:00
|
|
|
else:
|
|
|
|
model_data = []
|
|
|
|
if self._device.branding_profile:
|
|
|
|
model_data.append(self._device.branding_profile)
|
|
|
|
if self._device.template:
|
|
|
|
model_data.append(self._device.template)
|
|
|
|
if model_data:
|
2021-10-22 15:04:25 +00:00
|
|
|
device_info[ATTR_MODEL] = " ".join(model_data)
|
2021-02-08 23:37:32 +00:00
|
|
|
|
|
|
|
return device_info
|
2020-07-11 01:20:50 +00:00
|
|
|
|
2021-03-01 02:16:30 +00:00
|
|
|
async def async_update(self) -> None:
|
2023-04-09 03:12:42 +00:00
|
|
|
"""Perform a manual update from API."""
|
2021-02-09 08:43:38 +00:00
|
|
|
await self._async_update_from_api()
|
|
|
|
|
2022-12-28 17:14:38 +00:00
|
|
|
@callback
|
|
|
|
def _async_update_if_bpup_not_alive(self, now: datetime) -> None:
|
2021-02-09 08:43:38 +00:00
|
|
|
"""Fetch via the API if BPUP is not alive."""
|
2023-04-09 03:12:42 +00:00
|
|
|
self._async_schedule_bpup_alive_or_poll()
|
2021-04-18 20:46:46 +00:00
|
|
|
if (
|
|
|
|
self.hass.is_stopping
|
|
|
|
or self._bpup_subs.alive
|
|
|
|
and self._initialized
|
2021-07-16 21:06:18 +00:00
|
|
|
and self.available
|
2021-04-18 20:46:46 +00:00
|
|
|
):
|
2021-02-09 08:43:38 +00:00
|
|
|
return
|
|
|
|
if self._update_lock.locked():
|
|
|
|
_LOGGER.warning(
|
|
|
|
"Updating %s took longer than the scheduled update interval %s",
|
|
|
|
self.entity_id,
|
|
|
|
_FALLBACK_SCAN_INTERVAL,
|
|
|
|
)
|
|
|
|
return
|
2022-12-28 17:14:38 +00:00
|
|
|
self.hass.async_create_task(self._async_update())
|
2021-02-09 08:43:38 +00:00
|
|
|
|
2022-12-28 17:14:38 +00:00
|
|
|
async def _async_update(self) -> None:
|
|
|
|
"""Fetch via the API."""
|
2021-02-09 08:43:38 +00:00
|
|
|
async with self._update_lock:
|
|
|
|
await self._async_update_from_api()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2021-03-01 02:16:30 +00:00
|
|
|
async def _async_update_from_api(self) -> None:
|
2021-02-09 08:43:38 +00:00
|
|
|
"""Fetch via the API."""
|
2020-07-24 20:14:47 +00:00
|
|
|
try:
|
2021-02-09 08:43:38 +00:00
|
|
|
state: dict = await self._hub.bond.device_state(self._device_id)
|
2020-07-24 20:14:47 +00:00
|
|
|
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
2021-07-16 21:06:18 +00:00
|
|
|
if self.available:
|
2020-07-24 20:14:47 +00:00
|
|
|
_LOGGER.warning(
|
|
|
|
"Entity %s has become unavailable", self.entity_id, exc_info=error
|
|
|
|
)
|
2021-07-16 21:06:18 +00:00
|
|
|
self._attr_available = False
|
2020-07-24 20:14:47 +00:00
|
|
|
else:
|
2021-02-09 08:43:38 +00:00
|
|
|
self._async_state_callback(state)
|
2020-07-23 02:15:27 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
2022-06-15 06:30:59 +00:00
|
|
|
def _apply_state(self) -> None:
|
2020-07-23 02:15:27 +00:00
|
|
|
raise NotImplementedError
|
2021-02-09 08:43:38 +00:00
|
|
|
|
|
|
|
@callback
|
2021-03-01 02:16:30 +00:00
|
|
|
def _async_state_callback(self, state: dict) -> None:
|
2021-02-09 08:43:38 +00:00
|
|
|
"""Process a state change."""
|
|
|
|
self._initialized = True
|
2021-07-16 21:06:18 +00:00
|
|
|
if not self.available:
|
2021-02-09 08:43:38 +00:00
|
|
|
_LOGGER.info("Entity %s has come back", self.entity_id)
|
2021-07-16 21:06:18 +00:00
|
|
|
self._attr_available = True
|
2021-02-09 08:43:38 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Device state for %s (%s) is:\n%s", self.name, self.entity_id, state
|
|
|
|
)
|
2022-06-15 06:30:59 +00:00
|
|
|
self._device.state = state
|
|
|
|
self._apply_state()
|
2021-02-09 08:43:38 +00:00
|
|
|
|
|
|
|
@callback
|
2022-05-26 04:12:43 +00:00
|
|
|
def _async_bpup_callback(self, json_msg: dict) -> None:
|
2021-02-09 08:43:38 +00:00
|
|
|
"""Process a state change from BPUP."""
|
2022-05-26 04:12:43 +00:00
|
|
|
topic = json_msg["t"]
|
|
|
|
if topic != f"devices/{self._device_id}/state":
|
|
|
|
return
|
|
|
|
|
|
|
|
self._async_state_callback(json_msg["b"])
|
2021-02-09 08:43:38 +00:00
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2021-03-01 02:16:30 +00:00
|
|
|
async def async_added_to_hass(self) -> None:
|
2021-02-09 08:43:38 +00:00
|
|
|
"""Subscribe to BPUP and start polling."""
|
|
|
|
await super().async_added_to_hass()
|
|
|
|
self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback)
|
2023-04-09 03:12:42 +00:00
|
|
|
self._async_schedule_bpup_alive_or_poll()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_schedule_bpup_alive_or_poll(self) -> None:
|
|
|
|
"""Schedule the BPUP alive or poll."""
|
|
|
|
alive = self._bpup_subs.alive
|
|
|
|
self._bpup_polling_fallback = async_call_later(
|
|
|
|
self.hass,
|
|
|
|
_BPUP_ALIVE_SCAN_INTERVAL if alive else _FALLBACK_SCAN_INTERVAL,
|
|
|
|
self._async_update_if_bpup_not_alive,
|
2021-02-09 08:43:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
|
|
"""Unsubscribe from BPUP data on remove."""
|
|
|
|
await super().async_will_remove_from_hass()
|
|
|
|
self._bpup_subs.unsubscribe(self._device_id, self._async_bpup_callback)
|
2023-04-09 03:12:42 +00:00
|
|
|
if self._bpup_polling_fallback:
|
|
|
|
self._bpup_polling_fallback()
|
|
|
|
self._bpup_polling_fallback = None
|