Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Generic entity for the HomematicIP Cloud component."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from homematicip.aio.device import AsyncDevice
9 from homematicip.aio.group import AsyncGroup
10 from homematicip.base.functionalChannels import FunctionalChannel
11 
12 from homeassistant.const import ATTR_ID
13 from homeassistant.core import callback
14 from homeassistant.helpers import device_registry as dr, entity_registry as er
15 from homeassistant.helpers.device_registry import DeviceInfo
16 from homeassistant.helpers.entity import Entity
17 
18 from .const import DOMAIN
19 from .hap import AsyncHome, HomematicipHAP
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 ATTR_MODEL_TYPE = "model_type"
24 ATTR_LOW_BATTERY = "low_battery"
25 ATTR_CONFIG_PENDING = "config_pending"
26 ATTR_CONNECTION_TYPE = "connection_type"
27 ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached"
28 ATTR_IS_GROUP = "is_group"
29 # RSSI HAP -> Device
30 ATTR_RSSI_DEVICE = "rssi_device"
31 # RSSI Device -> HAP
32 ATTR_RSSI_PEER = "rssi_peer"
33 ATTR_SABOTAGE = "sabotage"
34 ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable"
35 ATTR_DEVICE_OVERHEATED = "device_overheated"
36 ATTR_DEVICE_OVERLOADED = "device_overloaded"
37 ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage"
38 ATTR_EVENT_DELAY = "event_delay"
39 
40 DEVICE_ATTRIBUTE_ICONS = {
41  "lowBat": "mdi:battery-outline",
42  "sabotage": "mdi:shield-alert",
43  "dutyCycle": "mdi:alert",
44  "deviceOverheated": "mdi:alert",
45  "deviceOverloaded": "mdi:alert",
46  "deviceUndervoltage": "mdi:alert",
47  "configPending": "mdi:alert-circle",
48 }
49 
50 DEVICE_ATTRIBUTES = {
51  "modelType": ATTR_MODEL_TYPE,
52  "connectionType": ATTR_CONNECTION_TYPE,
53  "sabotage": ATTR_SABOTAGE,
54  "dutyCycle": ATTR_DUTY_CYCLE_REACHED,
55  "rssiDeviceValue": ATTR_RSSI_DEVICE,
56  "rssiPeerValue": ATTR_RSSI_PEER,
57  "deviceOverheated": ATTR_DEVICE_OVERHEATED,
58  "deviceOverloaded": ATTR_DEVICE_OVERLOADED,
59  "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE,
60  "configPending": ATTR_CONFIG_PENDING,
61  "eventDelay": ATTR_EVENT_DELAY,
62  "id": ATTR_ID,
63 }
64 
65 GROUP_ATTRIBUTES = {
66  "modelType": ATTR_MODEL_TYPE,
67  "lowBat": ATTR_LOW_BATTERY,
68  "sabotage": ATTR_SABOTAGE,
69  "dutyCycle": ATTR_DUTY_CYCLE_REACHED,
70  "configPending": ATTR_CONFIG_PENDING,
71  "unreach": ATTR_GROUP_MEMBER_UNREACHABLE,
72 }
73 
74 
76  """Representation of the HomematicIP generic entity."""
77 
78  _attr_should_poll = False
79 
80  def __init__(
81  self,
82  hap: HomematicipHAP,
83  device,
84  post: str | None = None,
85  channel: int | None = None,
86  is_multi_channel: bool | None = False,
87  ) -> None:
88  """Initialize the generic entity."""
89  self._hap_hap = hap
90  self._home: AsyncHome = hap.home
91  self._device_device = device
92  self._post_post = post
93  self._channel_channel = channel
94  self._is_multi_channel_is_multi_channel = is_multi_channel
95  self.functional_channelfunctional_channel = self.get_current_channelget_current_channel()
96  # Marker showing that the HmIP device hase been removed.
97  self.hmip_device_removedhmip_device_removed = False
98 
99  @property
100  def device_info(self) -> DeviceInfo | None:
101  """Return device specific attributes."""
102  # Only physical devices should be HA devices.
103  if isinstance(self._device_device, AsyncDevice):
104  return DeviceInfo(
105  identifiers={
106  # Serial numbers of Homematic IP device
107  (DOMAIN, self._device_device.id)
108  },
109  manufacturer=self._device_device.oem,
110  model=self._device_device.modelType,
111  name=self._device_device.label,
112  sw_version=self._device_device.firmwareVersion,
113  # Link to the homematic ip access point.
114  via_device=(DOMAIN, self._device_device.homeId),
115  )
116  return None
117 
118  async def async_added_to_hass(self) -> None:
119  """Register callbacks."""
120  self._hap_hap.hmip_device_by_entity_id[self.entity_identity_id] = self._device_device
121  self._device_device.on_update(self._async_device_changed_async_device_changed)
122  self._device_device.on_remove(self._async_device_removed_async_device_removed)
123 
124  @callback
125  def _async_device_changed(self, *args, **kwargs) -> None:
126  """Handle device state changes."""
127  # Don't update disabled entities
128  if self.enabledenabled:
129  _LOGGER.debug("Event %s (%s)", self.namenamename, self._device_device.modelType)
130  self.async_write_ha_stateasync_write_ha_state()
131  else:
132  _LOGGER.debug(
133  "Device Changed Event for %s (%s) not fired. Entity is disabled",
134  self.namenamename,
135  self._device_device.modelType,
136  )
137 
138  async def async_will_remove_from_hass(self) -> None:
139  """Run when hmip device will be removed from hass."""
140 
141  # Only go further if the device/entity should be removed from registries
142  # due to a removal of the HmIP device.
143 
144  if self.hmip_device_removedhmip_device_removed:
145  try:
146  del self._hap_hap.hmip_device_by_entity_id[self.entity_identity_id]
147  self.async_remove_from_registriesasync_remove_from_registries()
148  except KeyError as err:
149  _LOGGER.debug("Error removing HMIP device from registry: %s", err)
150 
151  @callback
152  def async_remove_from_registries(self) -> None:
153  """Remove entity/device from registry."""
154  # Remove callback from device.
155  self._device_device.remove_callback(self._async_device_changed_async_device_changed)
156  self._device_device.remove_callback(self._async_device_removed_async_device_removed)
157 
158  if not self.registry_entryregistry_entry:
159  return
160 
161  if device_id := self.registry_entryregistry_entry.device_id:
162  # Remove from device registry.
163  device_registry = dr.async_get(self.hasshass)
164  if device_id in device_registry.devices:
165  # This will also remove associated entities from entity registry.
166  device_registry.async_remove_device(device_id)
167  else: # noqa: PLR5501
168  # Remove from entity registry.
169  # Only relevant for entities that do not belong to a device.
170  if entity_id := self.registry_entryregistry_entry.entity_id:
171  entity_registry = er.async_get(self.hasshass)
172  if entity_id in entity_registry.entities:
173  entity_registry.async_remove(entity_id)
174 
175  @callback
176  def _async_device_removed(self, *args, **kwargs) -> None:
177  """Handle hmip device removal."""
178  # Set marker showing that the HmIP device hase been removed.
179  self.hmip_device_removedhmip_device_removed = True
180  self.hasshass.async_create_task(
181  self.async_removeasync_remove(force_remove=True), eager_start=False
182  )
183 
184  @property
185  def name(self) -> str:
186  """Return the name of the generic entity."""
187 
188  name = None
189  # Try to get a label from a channel.
190  if hasattr(self._device_device, "functionalChannels"):
191  if self._is_multi_channel_is_multi_channel:
192  name = self._device_device.functionalChannels[self._channel_channel].label
193  elif len(self._device_device.functionalChannels) > 1:
194  name = self._device_device.functionalChannels[1].label
195 
196  # Use device label, if name is not defined by channel label.
197  if not name:
198  name = self._device_device.label
199  if self._post_post:
200  name = f"{name} {self._post}"
201  elif self._is_multi_channel_is_multi_channel:
202  name = f"{name} Channel{self._channel}"
203 
204  # Add a prefix to the name if the homematic ip home has a name.
205  if name and self._home.name:
206  name = f"{self._home.name} {name}"
207 
208  return name
209 
210  @property
211  def available(self) -> bool:
212  """Return if entity is available."""
213  return not self._device_device.unreach
214 
215  @property
216  def unique_id(self) -> str:
217  """Return a unique ID."""
218  unique_id = f"{self.__class__.__name__}_{self._device.id}"
219  if self._is_multi_channel_is_multi_channel:
220  unique_id = (
221  f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
222  )
223 
224  return unique_id
225 
226  @property
227  def icon(self) -> str | None:
228  """Return the icon."""
229  for attr, icon in DEVICE_ATTRIBUTE_ICONS.items():
230  if getattr(self._device_device, attr, None):
231  return icon
232 
233  return None
234 
235  @property
236  def extra_state_attributes(self) -> dict[str, Any]:
237  """Return the state attributes of the generic entity."""
238  state_attr = {}
239 
240  if isinstance(self._device_device, AsyncDevice):
241  for attr, attr_key in DEVICE_ATTRIBUTES.items():
242  if attr_value := getattr(self._device_device, attr, None):
243  state_attr[attr_key] = attr_value
244 
245  state_attr[ATTR_IS_GROUP] = False
246 
247  if isinstance(self._device_device, AsyncGroup):
248  for attr, attr_key in GROUP_ATTRIBUTES.items():
249  if attr_value := getattr(self._device_device, attr, None):
250  state_attr[attr_key] = attr_value
251 
252  state_attr[ATTR_IS_GROUP] = True
253 
254  return state_attr
255 
256  def get_current_channel(self) -> FunctionalChannel:
257  """Return the FunctionalChannel for device."""
258  if hasattr(self._device_device, "functionalChannels"):
259  if self._is_multi_channel_is_multi_channel:
260  return self._device_device.functionalChannels[self._channel_channel]
261 
262  if len(self._device_device.functionalChannels) > 1:
263  return self._device_device.functionalChannels[1]
264 
265  return None
None __init__(self, HomematicipHAP hap, device, str|None post=None, int|None channel=None, bool|None is_multi_channel=False)
Definition: entity.py:87
None async_remove(self, *bool force_remove=False)
Definition: entity.py:1387
str|UndefinedType|None name(self)
Definition: entity.py:738