Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """An abstract class common to all Bond entities."""
2 
3 from __future__ import annotations
4 
5 from abc import abstractmethod
6 from asyncio import Lock
7 from datetime import datetime
8 import logging
9 
10 from aiohttp import ClientError
11 
12 from homeassistant.const import (
13  ATTR_HW_VERSION,
14  ATTR_MODEL,
15  ATTR_NAME,
16  ATTR_SUGGESTED_AREA,
17  ATTR_SW_VERSION,
18  ATTR_VIA_DEVICE,
19 )
20 from homeassistant.core import CALLBACK_TYPE, HassJob, callback
21 from homeassistant.helpers.device_registry import DeviceInfo
22 from homeassistant.helpers.entity import Entity
23 from homeassistant.helpers.event import async_call_later
24 
25 from .const import DOMAIN
26 from .models import BondData
27 from .utils import BondDevice
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 _FALLBACK_SCAN_INTERVAL = 10
32 _BPUP_ALIVE_SCAN_INTERVAL = 60
33 
34 
36  """Generic Bond entity encapsulating common features of any Bond controlled device."""
37 
38  _attr_should_poll = False
39 
40  def __init__(
41  self,
42  data: BondData,
43  device: BondDevice,
44  sub_device: str | None = None,
45  sub_device_id: str | None = None,
46  ) -> None:
47  """Initialize entity with API and device info."""
48  hub = data.hub
49  self._hub_hub = hub
50  self._bond_bond = hub.bond
51  self._device_device = device
52  self._device_id_device_id = device.device_id
53  self._sub_device_sub_device = sub_device
54  self._attr_available_attr_available = True
55  self._bpup_subs_bpup_subs = data.bpup_subs
56  self._update_lock_update_lock = Lock()
57  self._initialized_initialized = False
58  if sub_device_id:
59  sub_device_id = f"_{sub_device_id}"
60  elif sub_device:
61  sub_device_id = f"_{sub_device}"
62  else:
63  sub_device_id = ""
64  self._attr_unique_id_attr_unique_id = f"{hub.bond_id}_{device.device_id}{sub_device_id}"
65  if sub_device:
66  sub_device_name = sub_device.replace("_", " ").title()
67  self._attr_name_attr_name = f"{device.name} {sub_device_name}"
68  else:
69  self._attr_name_attr_name = device.name
70  self._attr_assumed_state_attr_assumed_state = self._hub_hub.is_bridge and not self._device_device.trust_state
71  self._apply_state_apply_state()
72  self._bpup_polling_fallback_bpup_polling_fallback: CALLBACK_TYPE | None = None
73  self._async_update_if_bpup_not_alive_job_async_update_if_bpup_not_alive_job = HassJob(
74  self._async_update_if_bpup_not_alive_async_update_if_bpup_not_alive
75  )
76 
77  @property
78  def device_info(self) -> DeviceInfo:
79  """Get a an HA device representing this Bond controlled device."""
80  device_info = DeviceInfo(
81  manufacturer=self._hub_hub.make,
82  # type ignore: tuple items should not be Optional
83  identifiers={(DOMAIN, self._hub_hub.bond_id, self._device_id_device_id)}, # type: ignore[arg-type]
84  configuration_url=f"http://{self._hub.host}",
85  )
86  if self.namename is not None:
87  device_info[ATTR_NAME] = self._device_device.name
88  if self._hub_hub.bond_id is not None:
89  device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub_hub.bond_id)
90  if self._device_device.location is not None:
91  device_info[ATTR_SUGGESTED_AREA] = self._device_device.location
92  if not self._hub_hub.is_bridge:
93  if self._hub_hub.model is not None:
94  device_info[ATTR_MODEL] = self._hub_hub.model
95  if self._hub_hub.fw_ver is not None:
96  device_info[ATTR_SW_VERSION] = self._hub_hub.fw_ver
97  if self._hub_hub.mcu_ver is not None:
98  device_info[ATTR_HW_VERSION] = self._hub_hub.mcu_ver
99  else:
100  model_data = []
101  if self._device_device.branding_profile:
102  model_data.append(self._device_device.branding_profile)
103  if self._device_device.template:
104  model_data.append(self._device_device.template)
105  if model_data:
106  device_info[ATTR_MODEL] = " ".join(model_data)
107 
108  return device_info
109 
110  async def async_update(self) -> None:
111  """Perform a manual update from API."""
112  await self._async_update_from_api_async_update_from_api()
113 
114  @callback
115  def _async_update_if_bpup_not_alive(self, now: datetime) -> None:
116  """Fetch via the API if BPUP is not alive."""
117  self._async_schedule_bpup_alive_or_poll_async_schedule_bpup_alive_or_poll()
118  if (
119  self.hasshass.is_stopping
120  or self._bpup_subs_bpup_subs.alive
121  and self._initialized_initialized
122  and self.availableavailable
123  ):
124  return
125  if self._update_lock_update_lock.locked():
126  _LOGGER.warning(
127  "Updating %s took longer than the scheduled update interval %s",
128  self.entity_identity_id,
129  _FALLBACK_SCAN_INTERVAL,
130  )
131  return
132  self.hasshass.async_create_background_task(
133  self._async_update_async_update(), f"{DOMAIN} {self.name} update", eager_start=True
134  )
135 
136  async def _async_update(self) -> None:
137  """Fetch via the API."""
138  async with self._update_lock_update_lock:
139  await self._async_update_from_api_async_update_from_api()
140  self.async_write_ha_stateasync_write_ha_state()
141 
142  async def _async_update_from_api(self) -> None:
143  """Fetch via the API."""
144  try:
145  state: dict = await self._bond_bond.device_state(self._device_id_device_id)
146  except (ClientError, TimeoutError, OSError) as error:
147  if self.availableavailable:
148  _LOGGER.warning(
149  "Entity %s has become unavailable", self.entity_identity_id, exc_info=error
150  )
151  self._attr_available_attr_available = False
152  else:
153  self._async_state_callback_async_state_callback(state)
154 
155  @abstractmethod
156  def _apply_state(self) -> None:
157  raise NotImplementedError
158 
159  @callback
160  def _async_state_callback(self, state: dict) -> None:
161  """Process a state change."""
162  self._initialized_initialized = True
163  if not self.availableavailable:
164  _LOGGER.info("Entity %s has come back", self.entity_identity_id)
165  self._attr_available_attr_available = True
166  _LOGGER.debug(
167  "Device state for %s (%s) is:\n%s", self.namename, self.entity_identity_id, state
168  )
169  self._device_device.state = state
170  self._apply_state_apply_state()
171 
172  @callback
173  def _async_bpup_callback(self, json_msg: dict) -> None:
174  """Process a state change from BPUP."""
175  topic = json_msg["t"]
176  if topic != f"devices/{self._device_id}/state":
177  return
178 
179  self._async_state_callback_async_state_callback(json_msg["b"])
180  self.async_write_ha_stateasync_write_ha_state()
181 
182  async def async_added_to_hass(self) -> None:
183  """Subscribe to BPUP and start polling."""
184  await super().async_added_to_hass()
185  self._bpup_subs_bpup_subs.subscribe(self._device_id_device_id, self._async_bpup_callback_async_bpup_callback)
186  self._async_schedule_bpup_alive_or_poll_async_schedule_bpup_alive_or_poll()
187 
188  @callback
190  """Schedule the BPUP alive or poll."""
191  alive = self._bpup_subs_bpup_subs.alive
192  self._bpup_polling_fallback_bpup_polling_fallback = async_call_later(
193  self.hasshass,
194  _BPUP_ALIVE_SCAN_INTERVAL if alive else _FALLBACK_SCAN_INTERVAL,
195  self._async_update_if_bpup_not_alive_job_async_update_if_bpup_not_alive_job,
196  )
197 
198  async def async_will_remove_from_hass(self) -> None:
199  """Unsubscribe from BPUP data on remove."""
200  await super().async_will_remove_from_hass()
201  self._bpup_subs_bpup_subs.unsubscribe(self._device_id_device_id, self._async_bpup_callback_async_bpup_callback)
202  if self._bpup_polling_fallback_bpup_polling_fallback:
203  self._bpup_polling_fallback_bpup_polling_fallback()
204  self._bpup_polling_fallback_bpup_polling_fallback = None
None _async_update_if_bpup_not_alive(self, datetime now)
Definition: entity.py:115
None _async_bpup_callback(self, dict json_msg)
Definition: entity.py:173
None __init__(self, BondData data, BondDevice device, str|None sub_device=None, str|None sub_device_id=None)
Definition: entity.py:46
None _async_state_callback(self, dict state)
Definition: entity.py:160
str|UndefinedType|None name(self)
Definition: entity.py:738
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
Definition: client.py:247
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597