1 """Track both clients and devices using UniFi Network."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
6 from dataclasses
import dataclass
7 from datetime
import timedelta
12 from aiounifi.interfaces.api_handlers
import ItemEvent
13 from aiounifi.interfaces.clients
import Clients
14 from aiounifi.interfaces.devices
import Devices
15 from aiounifi.models.api
import ApiItemT
16 from aiounifi.models.client
import Client
17 from aiounifi.models.device
import Device
18 from aiounifi.models.event
import Event, EventKey
19 from propcache
import cached_property
22 DOMAIN
as DEVICE_TRACKER_DOMAIN,
24 ScannerEntityDescription,
32 from .
import UnifiConfigEntry
33 from .const
import DOMAIN
as UNIFI_DOMAIN
37 UnifiEntityDescription,
38 async_device_available_fn,
40 from .hub
import UnifiHub
42 LOGGER = logging.getLogger(__name__)
44 CLIENT_TRACKER =
"client"
45 DEVICE_TRACKER =
"device"
47 CLIENT_CONNECTED_ATTRIBUTES = [
62 CLIENT_STATIC_ATTRIBUTES = [
69 CLIENT_CONNECTED_ALL_ATTRIBUTES = CLIENT_CONNECTED_ATTRIBUTES + CLIENT_STATIC_ATTRIBUTES
71 WIRED_CONNECTION = (EventKey.WIRED_CLIENT_CONNECTED,)
72 WIRED_DISCONNECTION = (EventKey.WIRED_CLIENT_DISCONNECTED,)
73 WIRELESS_CONNECTION = (
74 EventKey.WIRELESS_CLIENT_CONNECTED,
75 EventKey.WIRELESS_CLIENT_ROAM,
76 EventKey.WIRELESS_CLIENT_ROAM_RADIO,
77 EventKey.WIRELESS_GUEST_CONNECTED,
78 EventKey.WIRELESS_GUEST_ROAM,
79 EventKey.WIRELESS_GUEST_ROAM_RADIO,
81 WIRELESS_DISCONNECTION = (
82 EventKey.WIRELESS_CLIENT_DISCONNECTED,
83 EventKey.WIRELESS_GUEST_DISCONNECTED,
89 """Check if client is allowed."""
90 if obj_id
in hub.config.option_supported_clients:
93 if not hub.config.option_track_clients:
96 client = hub.api.clients[obj_id]
97 if client.mac
not in hub.entity_loader.wireless_clients:
98 if not hub.config.option_track_wired_clients:
103 and hub.config.option_ssid_filter
104 and client.essid
not in hub.config.option_ssid_filter
113 """Check if device object is disabled."""
114 client = hub.api.clients[obj_id]
116 if hub.entity_loader.wireless_clients.is_wireless(client)
and client.is_wired:
117 if not hub.config.option_ignore_wired_bug:
123 and hub.config.option_ssid_filter
124 and client.essid
not in hub.config.option_ssid_filter
129 dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen
or 0)
130 > hub.config.option_detection_time
139 """Check if device object is disabled."""
140 device = hub.api.devices[obj_id]
141 return timedelta(seconds=device.next_interval + 60)
144 @dataclass(frozen=True, kw_only=True)
146 UnifiEntityDescription[HandlerT, ApiItemT], ScannerEntityDescription
148 """Class describing UniFi device tracker entity."""
150 heartbeat_timedelta_fn: Callable[[UnifiHub, str], timedelta]
151 ip_address_fn: Callable[[aiounifi.Controller, str], str |
None]
152 is_connected_fn: Callable[[UnifiHub, str], bool]
153 hostname_fn: Callable[[aiounifi.Controller, str], str |
None]
156 ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = (
157 UnifiTrackerEntityDescription[Clients, Client](
158 key=
"Client device scanner",
159 allowed_fn=async_client_allowed_fn,
160 api_handler_fn=
lambda api: api.clients,
161 device_info_fn=
lambda api, obj_id:
None,
162 event_is_on=set(WIRED_CONNECTION + WIRELESS_CONNECTION),
165 + WIRED_DISCONNECTION
166 + WIRELESS_CONNECTION
167 + WIRELESS_DISCONNECTION
169 heartbeat_timedelta_fn=
lambda hub, _: hub.config.option_detection_time,
170 is_connected_fn=async_client_is_connected_fn,
171 name_fn=
lambda client: client.name
or client.hostname,
172 object_fn=
lambda api, obj_id: api.clients[obj_id],
173 unique_id_fn=
lambda hub, obj_id: f
"{hub.site}-{obj_id}",
174 ip_address_fn=
lambda api, obj_id: api.clients[obj_id].ip,
175 hostname_fn=
lambda api, obj_id: api.clients[obj_id].hostname,
177 UnifiTrackerEntityDescription[Devices, Device](
178 key=
"Device scanner",
179 allowed_fn=
lambda hub, obj_id: hub.config.option_track_devices,
180 api_handler_fn=
lambda api: api.devices,
181 available_fn=async_device_available_fn,
182 device_info_fn=
lambda api, obj_id:
None,
183 heartbeat_timedelta_fn=async_device_heartbeat_timedelta_fn,
184 is_connected_fn=
lambda hub, obj_id: hub.api.devices[obj_id].state == 1,
185 name_fn=
lambda device: device.name
or device.model,
186 object_fn=
lambda api, obj_id: api.devices[obj_id],
187 unique_id_fn=
lambda hub, obj_id: obj_id,
188 ip_address_fn=
lambda api, obj_id: api.devices[obj_id].ip,
189 hostname_fn=
lambda api, obj_id:
None,
196 """Normalize client unique ID to have a prefix rather than suffix.
198 Introduced with release 2023.12.
200 hub = config_entry.runtime_data
201 ent_reg = er.async_get(hass)
205 """Rework unique ID."""
206 new_unique_id = f
"{hub.site}-{obj_id}"
207 if ent_reg.async_get_entity_id(
208 DEVICE_TRACKER_DOMAIN, UNIFI_DOMAIN, new_unique_id
212 unique_id = f
"{obj_id}-{hub.site}"
213 if entity_id := ent_reg.async_get_entity_id(
214 DEVICE_TRACKER_DOMAIN, UNIFI_DOMAIN, unique_id
216 ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
218 for obj_id
in list(hub.api.clients) +
list(hub.api.clients_all):
224 config_entry: UnifiConfigEntry,
225 async_add_entities: AddEntitiesCallback,
227 """Set up device tracker for UniFi Network integration."""
229 config_entry.runtime_data.entity_loader.register_platform(
230 async_add_entities, UnifiScannerEntity, ENTITY_DESCRIPTIONS
235 """Representation of a UniFi scanner."""
237 entity_description: UnifiTrackerEntityDescription
239 _event_is_on: set[EventKey]
245 """Initiate entity state.
247 Initiate is_connected.
254 self.
hubhub.update_heartbeat(
257 + description.heartbeat_timedelta_fn(self.
hubhub, self.
_obj_id_obj_id),
262 """Return true if the device is connected to the network."""
267 """Return hostname of the device."""
272 """Return the primary ip address of the device."""
277 """Return the mac address of the device."""
282 """Return a unique ID."""
287 """No heart beat by device."""
293 """Update entity state.
295 Remove heartbeat check if hub connection state has changed
296 and entity is unavailable.
298 Schedule new heartbeat check if connected.
303 if event
is ItemEvent.CHANGED:
307 elif event
is ItemEvent.ADDED
and not self.
availableavailable:
315 if is_connected := description.is_connected_fn(hub, obj_id):
317 self.
hubhub.update_heartbeat(
319 dt_util.utcnow() + description.heartbeat_timedelta_fn(hub, obj_id),
324 """Event subscription callback."""
336 hub.update_heartbeat(
343 """Register callbacks."""
348 f
"{self.hub.signal_heartbeat_missed}_{self.unique_id}",
354 """Disconnect object when removed."""
360 """Return the client state attributes."""
367 attributes_to_check = CLIENT_STATIC_ATTRIBUTES
369 attributes_to_check = CLIENT_CONNECTED_ALL_ATTRIBUTES
371 return {k: raw[k]
for k
in attributes_to_check
if k
in raw}
None async_event_callback(self, Event event)
None async_update_state(self, ItemEvent event, str obj_id)
str|None ip_address(self)
None _make_disconnected(self, *core_Event _)
None async_initiate_state(self)
None async_added_to_hass(self)
Mapping[str, Any]|None extra_state_attributes(self)
None async_will_remove_from_hass(self)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
bool async_client_is_connected_fn(UnifiHub hub, str obj_id)
bool async_client_allowed_fn(UnifiHub hub, str obj_id)
None async_setup_entry(HomeAssistant hass, UnifiConfigEntry config_entry, AddEntitiesCallback async_add_entities)
timedelta async_device_heartbeat_timedelta_fn(UnifiHub hub, str obj_id)
None async_update_unique_id(HomeAssistant hass, UnifiConfigEntry config_entry)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)