Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """UniFi entity representation."""
2 
3 from __future__ import annotations
4 
5 from abc import abstractmethod
6 from collections.abc import Callable
7 from dataclasses import dataclass
8 from typing import TYPE_CHECKING, Generic, TypeVar
9 
10 import aiounifi
11 from aiounifi.interfaces.api_handlers import (
12  APIHandler,
13  CallbackType,
14  ItemEvent,
15  UnsubscribeType,
16 )
17 from aiounifi.models.api import ApiItemT
18 from aiounifi.models.event import Event, EventKey
19 
20 from homeassistant.core import callback
21 from homeassistant.helpers import entity_registry as er
23  CONNECTION_NETWORK_MAC,
24  DeviceEntryType,
25  DeviceInfo,
26 )
27 from homeassistant.helpers.dispatcher import async_dispatcher_connect
28 from homeassistant.helpers.entity import Entity, EntityDescription
29 
30 from .const import ATTR_MANUFACTURER, DOMAIN
31 
32 if TYPE_CHECKING:
33  from .hub import UnifiHub
34 
35 HandlerT = TypeVar("HandlerT", bound=APIHandler)
36 SubscriptionT = Callable[[CallbackType, ItemEvent], UnsubscribeType]
37 
38 
39 @callback
40 def async_device_available_fn(hub: UnifiHub, obj_id: str) -> bool:
41  """Check if device is available."""
42  if "_" in obj_id: # Sub device (outlet or port)
43  obj_id = obj_id.partition("_")[0]
44 
45  device = hub.api.devices[obj_id]
46  return hub.available and not device.disabled
47 
48 
49 @callback
50 def async_wlan_available_fn(hub: UnifiHub, obj_id: str) -> bool:
51  """Check if WLAN is available."""
52  wlan = hub.api.wlans[obj_id]
53  return hub.available and wlan.enabled
54 
55 
56 @callback
57 def async_device_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo:
58  """Create device registry entry for device."""
59  if "_" in obj_id: # Sub device (outlet or port)
60  obj_id = obj_id.partition("_")[0]
61 
62  device = hub.api.devices[obj_id]
63  return DeviceInfo(
64  connections={(CONNECTION_NETWORK_MAC, device.mac)},
65  manufacturer=ATTR_MANUFACTURER,
66  model=device.model,
67  name=device.name or None,
68  sw_version=device.version,
69  hw_version=str(device.board_revision),
70  )
71 
72 
73 @callback
74 def async_wlan_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo:
75  """Create device registry entry for WLAN."""
76  wlan = hub.api.wlans[obj_id]
77  return DeviceInfo(
78  entry_type=DeviceEntryType.SERVICE,
79  identifiers={(DOMAIN, wlan.id)},
80  manufacturer=ATTR_MANUFACTURER,
81  model="UniFi WLAN",
82  name=wlan.name,
83  )
84 
85 
86 @callback
87 def async_client_device_info_fn(hub: UnifiHub, obj_id: str) -> DeviceInfo:
88  """Create device registry entry for client."""
89  client = hub.api.clients[obj_id]
90  return DeviceInfo(
91  connections={(CONNECTION_NETWORK_MAC, obj_id)},
92  default_manufacturer=client.oui,
93  default_name=client.name or client.hostname,
94  )
95 
96 
97 @dataclass(frozen=True, kw_only=True)
98 class UnifiEntityDescription(EntityDescription, Generic[HandlerT, ApiItemT]):
99  """UniFi Entity Description."""
100 
101  api_handler_fn: Callable[[aiounifi.Controller], HandlerT]
102  """Provide api_handler from api."""
103  device_info_fn: Callable[[UnifiHub, str], DeviceInfo | None]
104  """Provide device info object based on hub and obj_id."""
105  object_fn: Callable[[aiounifi.Controller, str], ApiItemT]
106  """Retrieve object based on api and obj_id."""
107  unique_id_fn: Callable[[UnifiHub, str], str]
108  """Provide a unique ID based on hub and obj_id."""
109 
110  # Optional functions
111  allowed_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: True
112  """Determine if config entry options allow creation of entity."""
113  available_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: hub.available
114  """Determine if entity is available, default is if connection is working."""
115  name_fn: Callable[[ApiItemT], str | None] = lambda obj: None
116  """Entity name function, can be used to extend entity name beyond device name."""
117  supported_fn: Callable[[UnifiHub, str], bool] = lambda hub, obj_id: True
118  """Determine if UniFi object supports providing relevant data for entity."""
119 
120  # Optional constants
121  has_entity_name = True # Part of EntityDescription
122  """Has entity name defaults to true."""
123  event_is_on: set[EventKey] | None = None
124  """Which UniFi events should be used to consider state 'on'."""
125  event_to_subscribe: tuple[EventKey, ...] | None = None
126  """Which UniFi events to listen on."""
127  should_poll: bool = False
128  """If entity needs to do regular checks on state."""
129 
130 
131 class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]):
132  """Representation of a UniFi entity."""
133 
134  entity_description: UnifiEntityDescription[HandlerT, ApiItemT]
135  _attr_unique_id: str
136 
137  def __init__(
138  self,
139  obj_id: str,
140  hub: UnifiHub,
141  description: UnifiEntityDescription[HandlerT, ApiItemT],
142  ) -> None:
143  """Set up UniFi switch entity."""
144  self._obj_id_obj_id = obj_id
145  self.hubhub = hub
146  self.apiapi = hub.api
147  self.entity_descriptionentity_description = description
148 
149  hub.entity_loader.known_objects.add((description.key, obj_id))
150 
151  self._removed_removed = False
152 
153  self._attr_available_attr_available = description.available_fn(hub, obj_id)
154  self._attr_device_info_attr_device_info = description.device_info_fn(hub, obj_id)
155  self._attr_should_poll_attr_should_poll = description.should_poll
156  self._attr_unique_id_attr_unique_id = description.unique_id_fn(hub, obj_id)
157 
158  obj = description.object_fn(self.apiapi, obj_id)
159  self._attr_name_attr_name = description.name_fn(obj)
160  self.async_initiate_stateasync_initiate_state()
161 
162  async def async_added_to_hass(self) -> None:
163  """Register callbacks."""
164  description = self.entity_descriptionentity_description
165  handler = description.api_handler_fn(self.apiapi)
166 
167  @callback
168  def unregister_object() -> None:
169  """Remove object ID from known_objects when unloaded."""
170  self.hubhub.entity_loader.known_objects.discard(
171  (description.key, self._obj_id_obj_id)
172  )
173 
174  self.async_on_removeasync_on_remove(unregister_object)
175 
176  # New data from handler
177  self.async_on_removeasync_on_remove(
178  handler.subscribe(
179  self.async_signalling_callbackasync_signalling_callback,
180  id_filter=self._obj_id_obj_id,
181  )
182  )
183 
184  # State change from hub or websocket
185  self.async_on_removeasync_on_remove(
187  self.hasshass,
188  self.hubhub.signal_reachable,
189  self.async_signal_reachable_callbackasync_signal_reachable_callback,
190  )
191  )
192 
193  # Config entry options updated
194  self.async_on_removeasync_on_remove(
196  self.hasshass,
197  self.hubhub.signal_options_update,
198  self.async_signal_options_updatedasync_signal_options_updated,
199  )
200  )
201 
202  # Subscribe to events if defined
203  if description.event_to_subscribe is not None:
204  self.async_on_removeasync_on_remove(
205  self.apiapi.events.subscribe(
206  self.async_event_callbackasync_event_callback,
207  description.event_to_subscribe,
208  )
209  )
210 
211  @callback
212  def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
213  """Update the entity state."""
214  if event is ItemEvent.DELETED and obj_id == self._obj_id_obj_id:
215  self.hasshass.async_create_task(self.remove_itemremove_item({obj_id}))
216  return
217 
218  description = self.entity_descriptionentity_description
219  if not description.supported_fn(self.hubhub, self._obj_id_obj_id):
220  self.hasshass.async_create_task(self.remove_itemremove_item({self._obj_id_obj_id}))
221  return
222 
223  self._attr_available_attr_available = description.available_fn(self.hubhub, self._obj_id_obj_id)
224  self.async_update_stateasync_update_state(event, obj_id)
225  self.async_write_ha_stateasync_write_ha_state()
226 
227  @callback
229  """Call when hub connection state change."""
230  self.async_signalling_callbackasync_signalling_callback(ItemEvent.ADDED, self._obj_id_obj_id)
231 
232  async def async_signal_options_updated(self) -> None:
233  """Config entry options are updated, remove entity if option is disabled."""
234  if not self.entity_descriptionentity_description.allowed_fn(self.hubhub, self._obj_id_obj_id):
235  await self.remove_itemremove_item({self._obj_id_obj_id})
236 
237  async def remove_item(self, keys: set) -> None:
238  """Remove entity if object ID is part of set."""
239  if self._obj_id_obj_id not in keys or self._removed_removed:
240  return
241  self._removed_removed = True
242  if self.registry_entryregistry_entry:
243  er.async_get(self.hasshass).async_remove(self.entity_identity_id)
244  else:
245  await self.async_removeasync_remove(force_remove=True)
246 
247  async def async_update(self) -> None:
248  """Update state if polling is configured."""
249  self.async_update_stateasync_update_state(ItemEvent.CHANGED, self._obj_id_obj_id)
250 
251  @callback
252  def async_initiate_state(self) -> None:
253  """Initiate entity state.
254 
255  Perform additional actions setting up platform entity child class state.
256  Defaults to using async_update_state to set initial state.
257  """
258  self.async_update_stateasync_update_state(ItemEvent.ADDED, self._obj_id_obj_id)
259 
260  @callback
261  @abstractmethod
262  def async_update_state(self, event: ItemEvent, obj_id: str) -> None:
263  """Update entity state.
264 
265  Perform additional actions updating platform entity child class state.
266  """
267 
268  @callback
269  def async_event_callback(self, event: Event) -> None:
270  """Update entity state based on subscribed event.
271 
272  Perform additional action updating platform entity child class state.
273  """
274  raise NotImplementedError
None async_signalling_callback(self, ItemEvent event, str obj_id)
Definition: entity.py:212
None __init__(self, str obj_id, UnifiHub hub, UnifiEntityDescription[HandlerT, ApiItemT] description)
Definition: entity.py:142
None async_update_state(self, ItemEvent event, str obj_id)
Definition: entity.py:262
None async_event_callback(self, Event event)
Definition: entity.py:269
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_remove(self, *bool force_remove=False)
Definition: entity.py:1387
bool async_device_available_fn(UnifiHub hub, str obj_id)
Definition: entity.py:40
DeviceInfo async_wlan_device_info_fn(UnifiHub hub, str obj_id)
Definition: entity.py:74
DeviceInfo async_client_device_info_fn(UnifiHub hub, str obj_id)
Definition: entity.py:87
bool async_wlan_available_fn(UnifiHub hub, str obj_id)
Definition: entity.py:50
DeviceInfo async_device_device_info_fn(UnifiHub hub, str obj_id)
Definition: entity.py:57
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103