From 01ba578016ae4df37f18e7c48f83856edc29b05f Mon Sep 17 00:00:00 2001
From: Quentame <polletquentin74@me.com>
Date: Tue, 30 Jun 2020 21:55:46 +0200
Subject: [PATCH] Add missed call sensor to Freebox (#36895)

---
 homeassistant/components/freebox/const.py     |  9 +++
 .../components/freebox/device_tracker.py      | 29 +++++----
 homeassistant/components/freebox/router.py    | 19 +++---
 homeassistant/components/freebox/sensor.py    | 62 +++++++++++++++----
 4 files changed, 83 insertions(+), 36 deletions(-)

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
+        }
-- 
GitLab