Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensor platform for UniFi Network integration.
2 
3 Support for bandwidth sensors of network clients.
4 Support for uptime sensors of network clients.
5 """
6 
7 from __future__ import annotations
8 
9 from collections.abc import Callable
10 from dataclasses import dataclass
11 from datetime import date, datetime, timedelta
12 from decimal import Decimal
13 from functools import partial
14 from typing import TYPE_CHECKING, Literal
15 
16 from aiounifi.interfaces.api_handlers import ItemEvent
17 from aiounifi.interfaces.clients import Clients
18 from aiounifi.interfaces.devices import Devices
19 from aiounifi.interfaces.outlets import Outlets
20 from aiounifi.interfaces.ports import Ports
21 from aiounifi.interfaces.wlans import Wlans
22 from aiounifi.models.api import ApiItemT
23 from aiounifi.models.client import Client
24 from aiounifi.models.device import (
25  Device,
26  TypedDeviceTemperature,
27  TypedDeviceUptimeStatsWanMonitor,
28 )
29 from aiounifi.models.outlet import Outlet
30 from aiounifi.models.port import Port
31 from aiounifi.models.wlan import Wlan
32 
34  SensorDeviceClass,
35  SensorEntity,
36  SensorEntityDescription,
37  SensorStateClass,
38  UnitOfTemperature,
39 )
40 from homeassistant.const import (
41  PERCENTAGE,
42  EntityCategory,
43  UnitOfDataRate,
44  UnitOfPower,
45  UnitOfTime,
46 )
47 from homeassistant.core import Event as core_Event, HomeAssistant, callback
48 from homeassistant.helpers.dispatcher import async_dispatcher_connect
49 from homeassistant.helpers.entity_platform import AddEntitiesCallback
50 from homeassistant.helpers.typing import StateType
51 from homeassistant.util import slugify
52 import homeassistant.util.dt as dt_util
53 
54 from . import UnifiConfigEntry
55 from .const import DEVICE_STATES
56 from .entity import (
57  HandlerT,
58  UnifiEntity,
59  UnifiEntityDescription,
60  async_client_device_info_fn,
61  async_device_available_fn,
62  async_device_device_info_fn,
63  async_wlan_available_fn,
64  async_wlan_device_info_fn,
65 )
66 from .hub import UnifiHub
67 
68 
69 @callback
70 def async_bandwidth_sensor_allowed_fn(hub: UnifiHub, obj_id: str) -> bool:
71  """Check if client is allowed."""
72  if obj_id in hub.config.option_supported_clients:
73  return True
74  return hub.config.option_allow_bandwidth_sensors
75 
76 
77 @callback
78 def async_uptime_sensor_allowed_fn(hub: UnifiHub, obj_id: str) -> bool:
79  """Check if client is allowed."""
80  if obj_id in hub.config.option_supported_clients:
81  return True
82  return hub.config.option_allow_uptime_sensors
83 
84 
85 @callback
86 def async_client_rx_value_fn(hub: UnifiHub, client: Client) -> float:
87  """Calculate receiving data transfer value."""
88  if hub.entity_loader.wireless_clients.is_wireless(client):
89  return client.rx_bytes_r / 1000000
90  return client.wired_rx_bytes_r / 1000000
91 
92 
93 @callback
94 def async_client_tx_value_fn(hub: UnifiHub, client: Client) -> float:
95  """Calculate transmission data transfer value."""
96  if hub.entity_loader.wireless_clients.is_wireless(client):
97  return client.tx_bytes_r / 1000000
98  return client.wired_tx_bytes_r / 1000000
99 
100 
101 @callback
102 def async_client_uptime_value_fn(hub: UnifiHub, client: Client) -> datetime:
103  """Calculate the uptime of the client."""
104  if client.uptime < 1000000000:
105  return dt_util.now() - timedelta(seconds=client.uptime)
106  return dt_util.utc_from_timestamp(float(client.uptime))
107 
108 
109 @callback
110 def async_wlan_client_value_fn(hub: UnifiHub, wlan: Wlan) -> int:
111  """Calculate the amount of clients connected to a wlan."""
112  return len(
113  [
114  client.mac
115  for client in hub.api.clients.values()
116  if client.essid == wlan.name
117  and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0)
118  < hub.config.option_detection_time
119  ]
120  )
121 
122 
123 @callback
124 def async_device_clients_value_fn(hub: UnifiHub, device: Device) -> int:
125  """Calculate the amount of clients connected to a device."""
126 
127  return len(
128  [
129  client.mac
130  for client in hub.api.clients.values()
131  if (
132  (
133  client.access_point_mac != ""
134  and client.access_point_mac == device.mac
135  )
136  or (client.access_point_mac == "" and client.switch_mac == device.mac)
137  )
138  and dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0)
139  < hub.config.option_detection_time
140  ]
141  )
142 
143 
144 @callback
145 def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | None:
146  """Calculate the approximate time the device started (based on uptime returned from API, in seconds)."""
147  if device.uptime <= 0:
148  # Library defaults to 0 if uptime is not provided, e.g. when offline
149  return None
150  return (dt_util.now() - timedelta(seconds=device.uptime)).replace(microsecond=0)
151 
152 
153 @callback
155  old: StateType | date | datetime | Decimal, new: datetime | float | str | None
156 ) -> bool:
157  """Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation."""
158  if isinstance(old, datetime) and isinstance(new, datetime):
159  return new != old and abs((new - old).total_seconds()) > 120
160  return old is None or (new != old)
161 
162 
163 @callback
164 def async_device_outlet_power_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
165  """Determine if an outlet has the power property."""
166  # At this time, an outlet_caps value of 3 is expected to indicate that the outlet
167  # supports metering
168  return hub.api.outlets[obj_id].caps == 3
169 
170 
171 @callback
172 def async_device_outlet_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
173  """Determine if a device supports reading overall power metrics."""
174  return hub.api.devices[obj_id].outlet_ac_power_budget is not None
175 
176 
177 @callback
178 def async_device_uplink_mac_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
179  """Determine if a device supports reading uplink MAC address."""
180  return "uplink_mac" in hub.api.devices[obj_id].raw.get("uplink", {})
181 
182 
184  stat_index: int, hub: UnifiHub, obj_id: str
185 ) -> bool:
186  """Determine if a device supports reading item at index in system stats."""
187  return (
188  "system-stats" in hub.api.devices[obj_id].raw
189  and hub.api.devices[obj_id].system_stats[stat_index] != ""
190  )
191 
192 
193 @callback
194 def async_client_is_connected_fn(hub: UnifiHub, obj_id: str) -> bool:
195  """Check if client was last seen recently."""
196  client = hub.api.clients[obj_id]
197 
198  if (
199  dt_util.utcnow() - dt_util.utc_from_timestamp(client.last_seen or 0)
200  > hub.config.option_detection_time
201  ):
202  return False
203 
204  return True
205 
206 
207 @callback
208 def async_device_state_value_fn(hub: UnifiHub, device: Device) -> str:
209  """Retrieve the state of the device."""
210  return DEVICE_STATES[device.state]
211 
212 
213 @callback
215  wan: Literal["WAN", "WAN2"],
216  monitor_target: str,
217  hub: UnifiHub,
218  obj_id: str,
219 ) -> bool:
220  """Determine if an device have a latency monitor."""
221  if (device := hub.api.devices[obj_id]) and device.uptime_stats:
222  return _device_wan_latency_monitor(wan, monitor_target, device) is not None
223  return False
224 
225 
226 @callback
228  wan: Literal["WAN", "WAN2"],
229  monitor_target: str,
230  hub: UnifiHub,
231  device: Device,
232 ) -> int | None:
233  """Retrieve the monitor target from WAN monitors."""
234  target = _device_wan_latency_monitor(wan, monitor_target, device)
235 
236  if TYPE_CHECKING:
237  # Checked by async_device_wan_latency_supported_fn
238  assert target
239 
240  return target.get("latency_average", 0)
241 
242 
243 @callback
245  wan: Literal["WAN", "WAN2"], monitor_target: str, device: Device
246 ) -> TypedDeviceUptimeStatsWanMonitor | None:
247  """Return the target of the WAN latency monitor."""
248  if device.uptime_stats and (uptime_stats_wan := device.uptime_stats.get(wan)):
249  for monitor in uptime_stats_wan["monitors"]:
250  if monitor_target in monitor["target"]:
251  return monitor
252  return None
253 
254 
255 def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]:
256  """Create WAN latency sensors from WAN monitor data."""
257 
258  def make_wan_latency_entity_description(
259  wan: Literal["WAN", "WAN2"], name: str, monitor_target: str
260  ) -> UnifiSensorEntityDescription:
261  name_wan = f"{name} {wan}"
262  return UnifiSensorEntityDescription[Devices, Device](
263  key=f"{name_wan} latency",
264  entity_category=EntityCategory.DIAGNOSTIC,
265  native_unit_of_measurement=UnitOfTime.MILLISECONDS,
266  state_class=SensorStateClass.MEASUREMENT,
267  device_class=SensorDeviceClass.DURATION,
268  entity_registry_enabled_default=False,
269  api_handler_fn=lambda api: api.devices,
270  available_fn=async_device_available_fn,
271  device_info_fn=async_device_device_info_fn,
272  name_fn=lambda device: f"{name_wan} latency",
273  object_fn=lambda api, obj_id: api.devices[obj_id],
274  supported_fn=partial(
275  async_device_wan_latency_supported_fn, wan, monitor_target
276  ),
277  unique_id_fn=lambda hub, obj_id: f"{slugify(name_wan)}_latency-{obj_id}",
278  value_fn=partial(async_device_wan_latency_value_fn, wan, monitor_target),
279  )
280 
281  wans: tuple[Literal["WAN"], Literal["WAN2"]] = ("WAN", "WAN2")
282  return tuple(
283  make_wan_latency_entity_description(wan, name, target)
284  for wan in wans
285  for name, target in (
286  ("Microsoft", "microsoft"),
287  ("Google", "google"),
288  ("Cloudflare", "1.1.1.1"),
289  )
290  )
291 
292 
293 @callback
295  temperature_name: str, hub: UnifiHub, device: Device
296 ) -> float:
297  """Retrieve the temperature of the device."""
298  return_value: float = 0
299  if device.temperatures:
300  temperature = _device_temperature(temperature_name, device.temperatures)
301  return_value = temperature if temperature is not None else 0
302  return return_value
303 
304 
305 @callback
307  temperature_name: str, hub: UnifiHub, obj_id: str
308 ) -> bool:
309  """Determine if an device have a temperatures."""
310  if (device := hub.api.devices[obj_id]) and device.temperatures:
311  return _device_temperature(temperature_name, device.temperatures) is not None
312  return False
313 
314 
315 @callback
317  temperature_name: str, temperatures: list[TypedDeviceTemperature]
318 ) -> float | None:
319  """Return the temperature of the device."""
320  for temperature in temperatures:
321  if temperature_name in temperature["name"]:
322  return temperature["value"]
323  return None
324 
325 
326 def make_device_temperatur_sensors() -> tuple[UnifiSensorEntityDescription, ...]:
327  """Create device temperature sensors."""
328 
329  def make_device_temperature_entity_description(
330  name: str,
331  ) -> UnifiSensorEntityDescription:
332  return UnifiSensorEntityDescription[Devices, Device](
333  key=f"Device {name} temperature",
334  device_class=SensorDeviceClass.TEMPERATURE,
335  entity_category=EntityCategory.DIAGNOSTIC,
336  state_class=SensorStateClass.MEASUREMENT,
337  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
338  entity_registry_enabled_default=False,
339  api_handler_fn=lambda api: api.devices,
340  available_fn=async_device_available_fn,
341  device_info_fn=async_device_device_info_fn,
342  name_fn=lambda device: f"{device.name} {name} Temperature",
343  object_fn=lambda api, obj_id: api.devices[obj_id],
344  supported_fn=partial(async_device_temperatures_supported_fn, name),
345  unique_id_fn=lambda hub, obj_id: f"temperature-{slugify(name)}-{obj_id}",
346  value_fn=partial(async_device_temperatures_value_fn, name),
347  )
348 
349  return tuple(
350  make_device_temperature_entity_description(name)
351  for name in (
352  "CPU",
353  "Local",
354  "PHY",
355  )
356  )
357 
358 
359 @dataclass(frozen=True, kw_only=True)
361  SensorEntityDescription, UnifiEntityDescription[HandlerT, ApiItemT]
362 ):
363  """Class describing UniFi sensor entity."""
364 
365  value_fn: Callable[[UnifiHub, ApiItemT], datetime | float | str | None]
366 
367  # Optional
368  is_connected_fn: Callable[[UnifiHub, str], bool] | None = None
369  """Calculate if source is connected."""
370  value_changed_fn: Callable[
371  [StateType | date | datetime | Decimal, datetime | float | str | None],
372  bool,
373  ] = lambda old, new: old != new
374  """Calculate whether a state change should be recorded."""
375 
376 
377 ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
378  UnifiSensorEntityDescription[Clients, Client](
379  key="Bandwidth sensor RX",
380  translation_key="client_bandwidth_rx",
381  device_class=SensorDeviceClass.DATA_RATE,
382  entity_category=EntityCategory.DIAGNOSTIC,
383  state_class=SensorStateClass.MEASUREMENT,
384  native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
385  allowed_fn=async_bandwidth_sensor_allowed_fn,
386  api_handler_fn=lambda api: api.clients,
387  device_info_fn=async_client_device_info_fn,
388  is_connected_fn=async_client_is_connected_fn,
389  name_fn=lambda _: "RX",
390  object_fn=lambda api, obj_id: api.clients[obj_id],
391  supported_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors,
392  unique_id_fn=lambda hub, obj_id: f"rx-{obj_id}",
393  value_fn=async_client_rx_value_fn,
394  ),
395  UnifiSensorEntityDescription[Clients, Client](
396  key="Bandwidth sensor TX",
397  translation_key="client_bandwidth_tx",
398  device_class=SensorDeviceClass.DATA_RATE,
399  entity_category=EntityCategory.DIAGNOSTIC,
400  state_class=SensorStateClass.MEASUREMENT,
401  native_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
402  allowed_fn=async_bandwidth_sensor_allowed_fn,
403  api_handler_fn=lambda api: api.clients,
404  device_info_fn=async_client_device_info_fn,
405  is_connected_fn=async_client_is_connected_fn,
406  name_fn=lambda _: "TX",
407  object_fn=lambda api, obj_id: api.clients[obj_id],
408  supported_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors,
409  unique_id_fn=lambda hub, obj_id: f"tx-{obj_id}",
410  value_fn=async_client_tx_value_fn,
411  ),
412  UnifiSensorEntityDescription[Ports, Port](
413  key="PoE port power sensor",
414  device_class=SensorDeviceClass.POWER,
415  entity_category=EntityCategory.DIAGNOSTIC,
416  state_class=SensorStateClass.MEASUREMENT,
417  native_unit_of_measurement=UnitOfPower.WATT,
418  entity_registry_enabled_default=False,
419  api_handler_fn=lambda api: api.ports,
420  available_fn=async_device_available_fn,
421  device_info_fn=async_device_device_info_fn,
422  name_fn=lambda port: f"{port.name} PoE Power",
423  object_fn=lambda api, obj_id: api.ports[obj_id],
424  supported_fn=lambda hub, obj_id: bool(hub.api.ports[obj_id].port_poe),
425  unique_id_fn=lambda hub, obj_id: f"poe_power-{obj_id}",
426  value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0",
427  ),
428  UnifiSensorEntityDescription[Ports, Port](
429  key="Port Bandwidth sensor RX",
430  translation_key="port_bandwidth_rx",
431  device_class=SensorDeviceClass.DATA_RATE,
432  entity_category=EntityCategory.DIAGNOSTIC,
433  entity_registry_enabled_default=False,
434  state_class=SensorStateClass.MEASUREMENT,
435  native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
436  suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
437  allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors,
438  api_handler_fn=lambda api: api.ports,
439  available_fn=async_device_available_fn,
440  device_info_fn=async_device_device_info_fn,
441  name_fn=lambda port: f"{port.name} RX",
442  object_fn=lambda api, obj_id: api.ports[obj_id],
443  unique_id_fn=lambda hub, obj_id: f"port_rx-{obj_id}",
444  value_fn=lambda hub, port: port.rx_bytes_r,
445  ),
446  UnifiSensorEntityDescription[Ports, Port](
447  key="Port Bandwidth sensor TX",
448  translation_key="port_bandwidth_tx",
449  device_class=SensorDeviceClass.DATA_RATE,
450  entity_category=EntityCategory.DIAGNOSTIC,
451  entity_registry_enabled_default=False,
452  state_class=SensorStateClass.MEASUREMENT,
453  native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
454  suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
455  allowed_fn=lambda hub, _: hub.config.option_allow_bandwidth_sensors,
456  api_handler_fn=lambda api: api.ports,
457  available_fn=async_device_available_fn,
458  device_info_fn=async_device_device_info_fn,
459  name_fn=lambda port: f"{port.name} TX",
460  object_fn=lambda api, obj_id: api.ports[obj_id],
461  unique_id_fn=lambda hub, obj_id: f"port_tx-{obj_id}",
462  value_fn=lambda hub, port: port.tx_bytes_r,
463  ),
464  UnifiSensorEntityDescription[Clients, Client](
465  key="Client uptime",
466  device_class=SensorDeviceClass.TIMESTAMP,
467  entity_category=EntityCategory.DIAGNOSTIC,
468  entity_registry_enabled_default=False,
469  allowed_fn=async_uptime_sensor_allowed_fn,
470  api_handler_fn=lambda api: api.clients,
471  device_info_fn=async_client_device_info_fn,
472  name_fn=lambda client: "Uptime",
473  object_fn=lambda api, obj_id: api.clients[obj_id],
474  supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors,
475  unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}",
476  value_fn=async_client_uptime_value_fn,
477  value_changed_fn=async_uptime_value_changed_fn,
478  ),
479  UnifiSensorEntityDescription[Wlans, Wlan](
480  key="WLAN clients",
481  translation_key="wlan_clients",
482  entity_category=EntityCategory.DIAGNOSTIC,
483  state_class=SensorStateClass.MEASUREMENT,
484  api_handler_fn=lambda api: api.wlans,
485  available_fn=async_wlan_available_fn,
486  device_info_fn=async_wlan_device_info_fn,
487  object_fn=lambda api, obj_id: api.wlans[obj_id],
488  should_poll=True,
489  unique_id_fn=lambda hub, obj_id: f"wlan_clients-{obj_id}",
490  value_fn=async_wlan_client_value_fn,
491  ),
492  UnifiSensorEntityDescription[Devices, Device](
493  key="Device clients",
494  translation_key="device_clients",
495  entity_category=EntityCategory.DIAGNOSTIC,
496  state_class=SensorStateClass.MEASUREMENT,
497  entity_registry_enabled_default=False,
498  api_handler_fn=lambda api: api.devices,
499  available_fn=async_device_available_fn,
500  device_info_fn=async_device_device_info_fn,
501  name_fn=lambda device: "Clients",
502  object_fn=lambda api, obj_id: api.devices[obj_id],
503  should_poll=True,
504  unique_id_fn=lambda hub, obj_id: f"device_clients-{obj_id}",
505  value_fn=async_device_clients_value_fn,
506  ),
507  UnifiSensorEntityDescription[Outlets, Outlet](
508  key="Outlet power metering",
509  device_class=SensorDeviceClass.POWER,
510  entity_category=EntityCategory.DIAGNOSTIC,
511  state_class=SensorStateClass.MEASUREMENT,
512  native_unit_of_measurement=UnitOfPower.WATT,
513  api_handler_fn=lambda api: api.outlets,
514  available_fn=async_device_available_fn,
515  device_info_fn=async_device_device_info_fn,
516  name_fn=lambda outlet: f"{outlet.name} Outlet Power",
517  object_fn=lambda api, obj_id: api.outlets[obj_id],
518  should_poll=True,
519  supported_fn=async_device_outlet_power_supported_fn,
520  unique_id_fn=lambda hub, obj_id: f"outlet_power-{obj_id}",
521  value_fn=lambda _, obj: obj.power if obj.relay_state else "0",
522  ),
523  UnifiSensorEntityDescription[Devices, Device](
524  key="SmartPower AC power budget",
525  device_class=SensorDeviceClass.POWER,
526  entity_category=EntityCategory.DIAGNOSTIC,
527  state_class=SensorStateClass.MEASUREMENT,
528  native_unit_of_measurement=UnitOfPower.WATT,
529  suggested_display_precision=1,
530  api_handler_fn=lambda api: api.devices,
531  available_fn=async_device_available_fn,
532  device_info_fn=async_device_device_info_fn,
533  name_fn=lambda device: "AC Power Budget",
534  object_fn=lambda api, obj_id: api.devices[obj_id],
535  supported_fn=async_device_outlet_supported_fn,
536  unique_id_fn=lambda hub, obj_id: f"ac_power_budget-{obj_id}",
537  value_fn=lambda hub, device: device.outlet_ac_power_budget,
538  ),
539  UnifiSensorEntityDescription[Devices, Device](
540  key="SmartPower AC power consumption",
541  device_class=SensorDeviceClass.POWER,
542  entity_category=EntityCategory.DIAGNOSTIC,
543  state_class=SensorStateClass.MEASUREMENT,
544  native_unit_of_measurement=UnitOfPower.WATT,
545  suggested_display_precision=1,
546  api_handler_fn=lambda api: api.devices,
547  available_fn=async_device_available_fn,
548  device_info_fn=async_device_device_info_fn,
549  name_fn=lambda device: "AC Power Consumption",
550  object_fn=lambda api, obj_id: api.devices[obj_id],
551  supported_fn=async_device_outlet_supported_fn,
552  unique_id_fn=lambda hub, obj_id: f"ac_power_conumption-{obj_id}",
553  value_fn=lambda hub, device: device.outlet_ac_power_consumption,
554  ),
555  UnifiSensorEntityDescription[Devices, Device](
556  key="Device uptime",
557  device_class=SensorDeviceClass.TIMESTAMP,
558  entity_category=EntityCategory.DIAGNOSTIC,
559  api_handler_fn=lambda api: api.devices,
560  available_fn=async_device_available_fn,
561  device_info_fn=async_device_device_info_fn,
562  name_fn=lambda device: "Uptime",
563  object_fn=lambda api, obj_id: api.devices[obj_id],
564  unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}",
565  value_fn=async_device_uptime_value_fn,
566  value_changed_fn=async_uptime_value_changed_fn,
567  ),
568  UnifiSensorEntityDescription[Devices, Device](
569  key="Device temperature",
570  device_class=SensorDeviceClass.TEMPERATURE,
571  entity_category=EntityCategory.DIAGNOSTIC,
572  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
573  api_handler_fn=lambda api: api.devices,
574  available_fn=async_device_available_fn,
575  device_info_fn=async_device_device_info_fn,
576  name_fn=lambda device: "Temperature",
577  object_fn=lambda api, obj_id: api.devices[obj_id],
578  supported_fn=lambda hub, obj_id: hub.api.devices[obj_id].has_temperature,
579  unique_id_fn=lambda hub, obj_id: f"device_temperature-{obj_id}",
580  value_fn=lambda hub, device: device.general_temperature,
581  ),
582  UnifiSensorEntityDescription[Devices, Device](
583  key="Device Uplink MAC",
584  translation_key="device_uplink_mac",
585  entity_category=EntityCategory.DIAGNOSTIC,
586  api_handler_fn=lambda api: api.devices,
587  available_fn=async_device_available_fn,
588  device_info_fn=async_device_device_info_fn,
589  name_fn=lambda device: "Uplink MAC",
590  object_fn=lambda api, obj_id: api.devices[obj_id],
591  unique_id_fn=lambda hub, obj_id: f"device_uplink_mac-{obj_id}",
592  supported_fn=async_device_uplink_mac_supported_fn,
593  value_fn=lambda hub, device: device.raw.get("uplink", {}).get("uplink_mac"),
594  is_connected_fn=lambda hub, obj_id: hub.api.devices[obj_id].state == 1,
595  ),
596  UnifiSensorEntityDescription[Devices, Device](
597  key="Device State",
598  translation_key="device_state",
599  device_class=SensorDeviceClass.ENUM,
600  entity_category=EntityCategory.DIAGNOSTIC,
601  api_handler_fn=lambda api: api.devices,
602  available_fn=async_device_available_fn,
603  device_info_fn=async_device_device_info_fn,
604  name_fn=lambda device: "State",
605  object_fn=lambda api, obj_id: api.devices[obj_id],
606  unique_id_fn=lambda hub, obj_id: f"device_state-{obj_id}",
607  value_fn=async_device_state_value_fn,
608  options=list(DEVICE_STATES.values()),
609  ),
610  UnifiSensorEntityDescription[Devices, Device](
611  key="Device CPU utilization",
612  translation_key="device_cpu_utilization",
613  entity_category=EntityCategory.DIAGNOSTIC,
614  native_unit_of_measurement=PERCENTAGE,
615  state_class=SensorStateClass.MEASUREMENT,
616  api_handler_fn=lambda api: api.devices,
617  available_fn=async_device_available_fn,
618  device_info_fn=async_device_device_info_fn,
619  name_fn=lambda device: "CPU utilization",
620  object_fn=lambda api, obj_id: api.devices[obj_id],
621  supported_fn=partial(device_system_stats_supported_fn, 0),
622  unique_id_fn=lambda hub, obj_id: f"cpu_utilization-{obj_id}",
623  value_fn=lambda hub, device: device.system_stats[0],
624  ),
625  UnifiSensorEntityDescription[Devices, Device](
626  key="Device memory utilization",
627  translation_key="device_memory_utilization",
628  entity_category=EntityCategory.DIAGNOSTIC,
629  native_unit_of_measurement=PERCENTAGE,
630  state_class=SensorStateClass.MEASUREMENT,
631  api_handler_fn=lambda api: api.devices,
632  available_fn=async_device_available_fn,
633  device_info_fn=async_device_device_info_fn,
634  name_fn=lambda device: "Memory utilization",
635  object_fn=lambda api, obj_id: api.devices[obj_id],
636  supported_fn=partial(device_system_stats_supported_fn, 1),
637  unique_id_fn=lambda hub, obj_id: f"memory_utilization-{obj_id}",
638  value_fn=lambda hub, device: device.system_stats[1],
639  ),
640 )
641 
642 ENTITY_DESCRIPTIONS += make_wan_latency_sensors() + make_device_temperatur_sensors()
643 
644 
646  hass: HomeAssistant,
647  config_entry: UnifiConfigEntry,
648  async_add_entities: AddEntitiesCallback,
649 ) -> None:
650  """Set up sensors for UniFi Network integration."""
651  config_entry.runtime_data.entity_loader.register_platform(
652  async_add_entities, UnifiSensorEntity, ENTITY_DESCRIPTIONS
653  )
654 
655 
656 class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity):
657  """Base representation of a UniFi sensor."""
658 
659  entity_description: UnifiSensorEntityDescription[HandlerT, ApiItemT]
660 
661  @callback
662  def _make_disconnected(self, *_: core_Event) -> None:
663  """No heart beat by device.
664 
665  Set sensor as unavailable when client device is disconnected
666  """
667  if self._attr_available_attr_available_attr_available:
668  self._attr_available_attr_available_attr_available = False
669  self.async_write_ha_stateasync_write_ha_state()
670 
671  @callback
672  def async_update_state(self, event: ItemEvent, obj_id: str) -> None:
673  """Update entity state.
674 
675  Update native_value.
676  """
677  description = self.entity_descriptionentity_description
678  obj = description.object_fn(self.apiapi, self._obj_id_obj_id)
679  # Update the value only if value is considered to have changed relative to its previous state
680  if description.value_changed_fn(
681  self.native_valuenative_value, (value := description.value_fn(self.hubhub, obj))
682  ):
683  self._attr_native_value_attr_native_value = value
684 
685  if description.is_connected_fn is not None:
686  # Send heartbeat if client is connected
687  if description.is_connected_fn(self.hubhub, self._obj_id_obj_id):
688  self.hubhub.update_heartbeat(
689  self._attr_unique_id_attr_unique_id,
690  dt_util.utcnow() + self.hubhub.config.option_detection_time,
691  )
692 
693  async def async_added_to_hass(self) -> None:
694  """Register callbacks."""
695  await super().async_added_to_hass()
696 
697  if self.entity_descriptionentity_description.is_connected_fn is not None:
698  # Register callback for missed heartbeat
699  self.async_on_removeasync_on_remove(
701  self.hasshass,
702  f"{self.hub.signal_heartbeat_missed}_{self.unique_id}",
703  self._make_disconnected_make_disconnected,
704  )
705  )
706 
707  async def async_will_remove_from_hass(self) -> None:
708  """Disconnect object when removed."""
709  await super().async_will_remove_from_hass()
710 
711  if self.entity_descriptionentity_description.is_connected_fn is not None:
712  # Remove heartbeat registration
713  self.hubhub.remove_heartbeat(self._attr_unique_id_attr_unique_id)
StateType|date|datetime|Decimal native_value(self)
Definition: __init__.py:460
None async_update_state(self, ItemEvent event, str obj_id)
Definition: sensor.py:672
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_bandwidth_sensor_allowed_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:70
float async_device_temperatures_value_fn(str temperature_name, UnifiHub hub, Device device)
Definition: sensor.py:296
float async_client_tx_value_fn(UnifiHub hub, Client client)
Definition: sensor.py:94
bool async_uptime_sensor_allowed_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:78
tuple[UnifiSensorEntityDescription,...] make_device_temperatur_sensors()
Definition: sensor.py:326
bool async_client_is_connected_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:194
int async_device_clients_value_fn(UnifiHub hub, Device device)
Definition: sensor.py:124
datetime async_client_uptime_value_fn(UnifiHub hub, Client client)
Definition: sensor.py:102
bool async_uptime_value_changed_fn(StateType|date|datetime|Decimal old, datetime|float|str|None new)
Definition: sensor.py:156
int async_wlan_client_value_fn(UnifiHub hub, Wlan wlan)
Definition: sensor.py:110
bool async_device_outlet_power_supported_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:164
str async_device_state_value_fn(UnifiHub hub, Device device)
Definition: sensor.py:208
tuple[UnifiSensorEntityDescription,...] make_wan_latency_sensors()
Definition: sensor.py:255
bool device_system_stats_supported_fn(int stat_index, UnifiHub hub, str obj_id)
Definition: sensor.py:185
bool async_device_uplink_mac_supported_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:178
float|None _device_temperature(str temperature_name, list[TypedDeviceTemperature] temperatures)
Definition: sensor.py:318
bool async_device_temperatures_supported_fn(str temperature_name, UnifiHub hub, str obj_id)
Definition: sensor.py:308
TypedDeviceUptimeStatsWanMonitor|None _device_wan_latency_monitor(Literal["WAN", "WAN2"] wan, str monitor_target, Device device)
Definition: sensor.py:246
bool async_device_wan_latency_supported_fn(Literal["WAN", "WAN2"] wan, str monitor_target, UnifiHub hub, str obj_id)
Definition: sensor.py:219
None async_setup_entry(HomeAssistant hass, UnifiConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:649
datetime|None async_device_uptime_value_fn(UnifiHub hub, Device device)
Definition: sensor.py:145
float async_client_rx_value_fn(UnifiHub hub, Client client)
Definition: sensor.py:86
int|None async_device_wan_latency_value_fn(Literal["WAN", "WAN2"] wan, str monitor_target, UnifiHub hub, Device device)
Definition: sensor.py:232
bool async_device_outlet_supported_fn(UnifiHub hub, str obj_id)
Definition: sensor.py:172
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103