"""Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from __future__ import annotations import logging from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN from .home_base import FreeboxHomeEntity from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rate_down", name="Freebox download speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, icon="mdi:download-network", ), SensorEntityDescription( key="rate_up", name="Freebox upload speed", device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND, icon="mdi:upload-network", ), ) CALL_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="missed", name="Freebox missed calls", icon="mdi:phone-missed", ), ) DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="partition_free_space", name="free space", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", ), ) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" router: FreeboxRouter = hass.data[DOMAIN][entry.unique_id] entities: list[SensorEntity] = [] _LOGGER.debug( "%s - %s - %s temperature sensors", router.name, router.mac, len(router.sensors_temperature), ) entities = [ FreeboxSensor( router, SensorEntityDescription( key=sensor_name, name=f"Freebox {sensor_name}", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), ) for sensor_name in router.sensors_temperature ] entities.extend( [FreeboxSensor(router, description) for description in CONNECTION_SENSORS] ) entities.extend( [FreeboxCallSensor(router, description) for description in CALL_SENSORS] ) _LOGGER.debug("%s - %s - %s disk(s)", router.name, router.mac, len(router.disks)) entities.extend( FreeboxDiskSensor(router, disk, partition, description) for disk in router.disks.values() for partition in disk["partitions"].values() for description in DISK_PARTITION_SENSORS ) for node in router.home_devices.values(): for endpoint in node["show_endpoints"]: if ( endpoint["name"] == "battery" and endpoint["ep_type"] == "signal" and endpoint.get("value") is not None ): entities.append(FreeboxBatterySensor(hass, router, node, endpoint)) if entities: async_add_entities(entities, True) class FreeboxSensor(SensorEntity): """Representation of a Freebox sensor.""" _attr_should_poll = False def __init__( self, router: FreeboxRouter, description: SensorEntityDescription ) -> None: """Initialize a Freebox sensor.""" self.entity_description = description self._router = router self._attr_unique_id = f"{router.mac} {description.name}" self._attr_device_info = router.device_info @callback def async_update_state(self) -> None: """Update the Freebox sensor.""" state = self._router.sensors[self.entity_description.key] if self.native_unit_of_measurement == UnitOfDataRate.KILOBYTES_PER_SECOND: self._attr_native_value = round(state / 1000, 2) else: self._attr_native_value = state @callback def async_on_demand_update(self) -> None: """Update state.""" self.async_update_state() self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Register state update callback.""" self.async_update_state() self.async_on_remove( async_dispatcher_connect( self.hass, self._router.signal_sensor_update, self.async_on_demand_update, ) ) class FreeboxCallSensor(FreeboxSensor): """Representation of a Freebox call sensor.""" def __init__( self, router: FreeboxRouter, description: SensorEntityDescription ) -> None: """Initialize a Freebox call sensor.""" super().__init__(router, description) self._call_list_for_type: list[dict[str, Any]] = [] @callback def async_update_state(self) -> None: """Update the Freebox call sensor.""" self._call_list_for_type = [] if self._router.call_list: for call in self._router.call_list: if not call["new"]: continue if self.entity_description.key == call["type"]: self._call_list_for_type.append(call) self._attr_native_value = len(self._call_list_for_type) @property def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { dt_util.utc_from_timestamp(call["datetime"]).isoformat(): call["name"] for call in self._call_list_for_type } class FreeboxDiskSensor(FreeboxSensor): """Representation of a Freebox disk sensor.""" def __init__( self, router: FreeboxRouter, disk: dict[str, Any], partition: dict[str, Any], description: SensorEntityDescription, ) -> None: """Initialize a Freebox disk sensor.""" super().__init__(router, description) self._disk_id = disk["id"] self._partition_id = partition["id"] self._attr_name = f"{partition['label']} {description.name}" self._attr_unique_id = ( f"{router.mac} {description.key} {disk['id']} {partition['id']}" ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, disk["id"])}, model=disk["model"], name=f"Disk {disk['id']}", sw_version=disk["firmware"], via_device=( DOMAIN, router.mac, ), ) @callback def async_update_state(self) -> None: """Update the Freebox disk sensor.""" value = None disk: dict[str, Any] = self._router.disks[self._disk_id] partition: dict[str, Any] = disk["partitions"][self._partition_id] if partition.get("total_bytes"): value = round(partition["free_bytes"] * 100 / partition["total_bytes"], 2) self._attr_native_value = value class FreeboxBatterySensor(FreeboxHomeEntity, SensorEntity): """Representation of a Freebox battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @property def native_value(self) -> int: """Return the current state of the device.""" return self.get_value("signal", "battery")