diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 0612e4e76f1..d0ac63fa9bb 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -46,6 +46,15 @@ CONNECTION_SENSORS = { }, } +CALL_SENSORS = { + "missed": { + SENSOR_NAME: "Freebox missed calls", + SENSOR_UNIT: None, + SENSOR_ICON: "mdi:phone-missed", + SENSOR_DEVICE_CLASS: None, + }, +} + TEMPERATURE_SENSOR_TEMPLATE = { SENSOR_NAME: None, SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index ea9919f5742..10c5b8eb2c5 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,6 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from datetime import datetime -import logging from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER @@ -14,8 +13,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN from .router import FreeboxRouter -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -65,9 +62,8 @@ class FreeboxDevice(ScannerEntity): self._active = False self._attrs = {} - self._unsub_dispatcher = None - - def update(self) -> None: + @callback + def async_update_state(self) -> None: """Update the Freebox device.""" device = self._router.devices[self._mac] self._active = device["active"] @@ -128,21 +124,24 @@ class FreeboxDevice(ScannerEntity): """No polling needed.""" return False - async def async_on_demand_update(self): + @callback + def async_on_demand_update(self): """Update state.""" - self.async_schedule_update_ha_state(True) + self.async_update_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._router.signal_device_update, self.async_on_demand_update + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, + ) ) - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() - def icon_for_freebox_device(device) -> str: - """Return a host icon from his type.""" + """Return a device icon from its type.""" return DEVICE_ICONS.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 7b4784c6ca4..3ebc3d754c3 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging from pathlib import Path -from typing import Dict, Optional +from typing import Any, Dict, List, Optional from aiofreepybox import Freepybox from aiofreepybox.api.wifi import Wifi @@ -47,9 +47,10 @@ class FreeboxRouter: self._sw_v = None self._attrs = {} - self.devices: Dict[str, any] = {} + self.devices: Dict[str, Any] = {} self.sensors_temperature: Dict[str, int] = {} self.sensors_connection: Dict[str, float] = {} + self.call_list: List[Dict[str, Any]] = [] self.listeners = [] @@ -81,7 +82,7 @@ class FreeboxRouter: async def update_devices(self) -> None: """Update Freebox devices.""" new_device = False - fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + fbx_devices: Dict[str, Any] = await self._api.lan.get_hosts_list() # Adds the Freebox itself fbx_devices.append( @@ -111,7 +112,7 @@ class FreeboxRouter: async def update_sensors(self) -> None: """Update Freebox sensors.""" # System sensors - syst_datas: Dict[str, any] = await self._api.system.get_config() + syst_datas: Dict[str, Any] = await self._api.system.get_config() # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. @@ -119,7 +120,7 @@ class FreeboxRouter: self.sensors_temperature[sensor["name"]] = sensor["value"] # Connection sensors - connection_datas: Dict[str, any] = await self._api.connection.get_status() + connection_datas: Dict[str, Any] = await self._api.connection.get_status() for sensor_key in CONNECTION_SENSORS: self.sensors_connection[sensor_key] = connection_datas[sensor_key] @@ -134,6 +135,8 @@ class FreeboxRouter: "serial": syst_datas["serial"], } + self.call_list = await self._api.call.get_call_list() + async_dispatcher_send(self.hass, self.signal_sensor_update) async def reboot(self) -> None: @@ -147,7 +150,7 @@ class FreeboxRouter: self._api = None @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> Dict[str, Any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, @@ -173,8 +176,8 @@ class FreeboxRouter: return f"{DOMAIN}-{self._host}-sensor-update" @property - def sensors(self) -> Wifi: - """Return the wifi.""" + def sensors(self) -> Dict[str, Any]: + """Return sensors.""" return {**self.sensors_temperature, **self.sensors_connection} @property diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index a3c5c32901c..dc0d808c438 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,14 +1,16 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -import logging from typing import Dict from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.dt as dt_util from .const import ( + CALL_SENSORS, CONNECTION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, @@ -19,8 +21,6 @@ from .const import ( ) from .router import FreeboxRouter -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -43,6 +43,9 @@ async def async_setup_entry( FreeboxSensor(router, sensor_key, CONNECTION_SENSORS[sensor_key]) ) + for sensor_key in CALL_SENSORS: + entities.append(FreeboxCallSensor(router, sensor_key, CALL_SENSORS[sensor_key])) + async_add_entities(entities, True) @@ -62,9 +65,8 @@ class FreeboxSensor(Entity): self._device_class = sensor[SENSOR_DEVICE_CLASS] self._unique_id = f"{self._router.mac} {self._name}" - self._unsub_dispatcher = None - - def update(self) -> None: + @callback + def async_update_state(self) -> None: """Update the Freebox sensor.""" state = self._router.sensors[self._sensor_type] if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: @@ -112,16 +114,50 @@ class FreeboxSensor(Entity): """No polling needed.""" return False - async def async_on_demand_update(self): + @callback + def async_on_demand_update(self): """Update state.""" - self.async_schedule_update_ha_state(True) + self.async_update_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._router.signal_sensor_update, self.async_on_demand_update + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_sensor_update, + self.async_on_demand_update, + ) ) - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() + +class FreeboxCallSensor(FreeboxSensor): + """Representation of a Freebox call sensor.""" + + def __init__( + self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any] + ) -> None: + """Initialize a Freebox call sensor.""" + self._call_list_for_type = [] + super().__init__(router, sensor_type, sensor) + + @callback + def async_update_state(self) -> None: + """Update the Freebox call sensor.""" + self._call_list_for_type = [] + for call in self._router.call_list: + if not call["new"]: + continue + if call["type"] == self._sensor_type: + self._call_list_for_type.append(call) + + self._state = len(self._call_list_for_type) + + @property + def device_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 + }