"""Support for UPnP/IGD Sensors.""" from __future__ import annotations from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND, TIME_SECONDS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import UpnpDataUpdateCoordinator, UpnpEntity, UpnpSensorEntityDescription from .const import ( BYTES_RECEIVED, BYTES_SENT, DATA_PACKETS, DATA_RATE_PACKETS_PER_SECOND, DOMAIN, KIBIBYTE, LOGGER, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, ROUTER_UPTIME, TIMESTAMP, WAN_STATUS, ) RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, name=f"{DATA_BYTES} received", icon="mdi:server-network", native_unit_of_measurement=DATA_BYTES, format="d", ), UpnpSensorEntityDescription( key=BYTES_SENT, name=f"{DATA_BYTES} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_BYTES, format="d", ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, name=f"{DATA_PACKETS} received", icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, format="d", ), UpnpSensorEntityDescription( key=PACKETS_SENT, name=f"{DATA_PACKETS} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_PACKETS, format="d", ), UpnpSensorEntityDescription( key=ROUTER_IP, name="External IP", icon="mdi:server-network", ), UpnpSensorEntityDescription( key=ROUTER_UPTIME, name="Uptime", icon="mdi:server-network", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, format="d", ), UpnpSensorEntityDescription( key=WAN_STATUS, name="wan status", icon="mdi:server-network", ), ) DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( key=BYTES_RECEIVED, unique_id="KiB/sec_received", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( key=BYTES_SENT, unique_id="KiB/sec_sent", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( key=PACKETS_RECEIVED, unique_id="packets/sec_received", name=f"{DATA_RATE_PACKETS_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( key=PACKETS_SENT, unique_id="packets/sec_sent", name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the UPnP/IGD sensors.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[UpnpSensor] = [ RawUpnpSensor( coordinator=coordinator, entity_description=entity_description, ) for entity_description in RAW_SENSORS if coordinator.data.get(entity_description.key) is not None ] entities.extend( [ DerivedUpnpSensor( coordinator=coordinator, entity_description=entity_description, ) for entity_description in DERIVED_SENSORS if coordinator.data.get(entity_description.key) is not None ] ) LOGGER.debug("Adding sensor entities: %s", entities) async_add_entities(entities) class UpnpSensor(UpnpEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" entity_description: UpnpSensorEntityDescription class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @property def native_value(self) -> str | None: """Return the state of the device.""" value = self.coordinator.data[self.entity_description.key] if value is None: return None return format(value, self.entity_description.format) class DerivedUpnpSensor(UpnpSensor): """Representation of a UNIT Sent/Received per second sensor.""" def __init__( self, coordinator: UpnpDataUpdateCoordinator, entity_description: UpnpSensorEntityDescription, ) -> None: """Initialize sensor.""" super().__init__(coordinator=coordinator, entity_description=entity_description) self._last_value = None self._last_timestamp = None def _has_overflowed(self, current_value) -> bool: """Check if value has overflowed.""" return current_value < self._last_value @property def native_value(self) -> str | None: """Return the state of the device.""" # Can't calculate any derivative if we have only one value. current_value = self.coordinator.data[self.entity_description.key] if current_value is None: return None current_timestamp = self.coordinator.data[TIMESTAMP] if self._last_value is None or self._has_overflowed(current_value): self._last_value = current_value self._last_timestamp = current_timestamp return None # Calculate derivative. delta_value = current_value - self._last_value if ( self.entity_description.native_unit_of_measurement == DATA_RATE_KIBIBYTES_PER_SECOND ): delta_value /= KIBIBYTE delta_time = current_timestamp - self._last_timestamp if delta_time.total_seconds() == 0: # Prevent division by 0. return None derived = delta_value / delta_time.total_seconds() # Store current values for future use. self._last_value = current_value self._last_timestamp = current_timestamp return format(derived, self.entity_description.format)