From 333b36527f05e390be049d79a453108ade278547 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 31 Dec 2019 15:21:19 +0100 Subject: [PATCH 01/45] Add config flow to Freebox --- .../components/freebox/.translations/en.json | 20 +++ homeassistant/components/freebox/__init__.py | 92 ++++++----- .../components/freebox/config_flow.py | 83 ++++++++++ homeassistant/components/freebox/const.py | 20 +++ .../components/freebox/device_tracker.py | 148 ++++++++++++------ .../components/freebox/manifest.json | 1 + homeassistant/components/freebox/sensor.py | 20 ++- homeassistant/components/freebox/strings.json | 20 +++ homeassistant/components/freebox/switch.py | 19 ++- homeassistant/generated/config_flows.py | 1 + 10 files changed, 325 insertions(+), 99 deletions(-) create mode 100644 homeassistant/components/freebox/.translations/en.json create mode 100644 homeassistant/components/freebox/config_flow.py create mode 100644 homeassistant/components/freebox/const.py create mode 100644 homeassistant/components/freebox/strings.json diff --git a/homeassistant/components/freebox/.translations/en.json b/homeassistant/components/freebox/.translations/en.json new file mode 100644 index 0000000000000..17d1f7dad24c2 --- /dev/null +++ b/homeassistant/components/freebox/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Host already configured" + }, + "error": { + "already_configured": "Host already configured" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 58426334dea6f..d3d3f8751a7f8 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,22 +1,20 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging -import socket from aiofreepybox import Freepybox from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.typing import HomeAssistantType -_LOGGER = logging.getLogger(__name__) +from .const import API_VERSION, APP_DESC, CONFIG_FILE, DOMAIN, PLATFORMS -DOMAIN = "freebox" -DATA_FREEBOX = DOMAIN +_LOGGER = logging.getLogger(__name__) -FREEBOX_CONFIG_FILE = "freebox.conf" CONFIG_SCHEMA = vol.Schema( { @@ -29,7 +27,7 @@ async def async_setup(hass, config): - """Set up the Freebox component.""" + """Set up the Freebox component from legacy config file.""" conf = config.get(DOMAIN) async def discovery_dispatch(service, discovery_info): @@ -37,54 +35,68 @@ async def discovery_dispatch(service, discovery_info): host = discovery_info.get("properties", {}).get("api_domain") port = discovery_info.get("properties", {}).get("https_port") _LOGGER.info("Discovered Freebox server: %s:%s", host, port) - await async_setup_freebox(hass, config, host, port) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: host, CONF_PORT: port}, + ) + ) discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch) - if conf is not None: - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - await async_setup_freebox(hass, config, host, port) - - return True + if conf is None: + return True + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + ) + ) -async def async_setup_freebox(hass, config, host, port): - """Start up the Freebox component platforms.""" + return True - app_desc = { - "app_id": "hass", - "app_name": "Home Assistant", - "app_version": "0.65", - "device_name": socket.gethostname(), - } - token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = "v6" +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Freebox component.""" + token_file = hass.config.path(CONFIG_FILE) - fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) + fbx = Freepybox(APP_DESC, token_file, API_VERSION) try: - await fbx.open(host, port) + await fbx.open(entry.data[CONF_HOST], entry.data[CONF_PORT]) except HttpRequestError: _LOGGER.exception("Failed to connect to Freebox") - else: - hass.data[DATA_FREEBOX] = fbx + return False - async def async_freebox_reboot(call): - """Handle reboot service call.""" - await fbx.system.reboot() + hass.data[DOMAIN] = fbx - hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) - - hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + for platform in PLATFORMS: hass.async_create_task( - async_load_platform(hass, "device_tracker", DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(entry, platform) ) - hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config)) - async def close_fbx(event): - """Close Freebox connection on HA Stop.""" - await fbx.close() + # Services + async def async_freebox_reboot(call): + """Handle reboot service call.""" + await fbx.system.reboot() + + hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) + async def close_fbx(event): + """Close Freebox connection on HA Stop.""" + await fbx.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Freebox component.""" + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(entry, platform) + + fbx = hass.data[DOMAIN] + await fbx.close() + return True diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py new file mode 100644 index 0000000000000..b625a3dc9d5a4 --- /dev/null +++ b/homeassistant/components/freebox/config_flow.py @@ -0,0 +1,83 @@ +"""Config flow to configure the Freebox integration.""" +import logging + +from aiofreepybox import Freepybox +from aiofreepybox.exceptions import HttpRequestError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import API_VERSION, APP_DESC, CONFIG_FILE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize Freebox config flow.""" + + def _configuration_exists(self, host: str) -> bool: + """Return True if host exists in configuration.""" + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == host: + return True + return False + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_PORT, default=user_input.get(CONF_PORT, "")): int, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, None) + + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + if self._configuration_exists(host): + errors["base"] = "already_configured" + return self._show_setup_form(user_input, errors) + + token_file = self.hass.config.path(CONFIG_FILE) + + fbx = Freepybox(APP_DESC, token_file, API_VERSION) + + try: + await fbx.open(host, port) + except HttpRequestError: + _LOGGER.exception("Failed to connect to Freebox") + errors["base"] = "connection_failed" + return self._show_setup_form(user_input, errors) + + return self.async_create_entry( + title=host, data={CONF_HOST: host, CONF_PORT: port}, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + if self._configuration_exists(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py new file mode 100644 index 0000000000000..f59f416d98641 --- /dev/null +++ b/homeassistant/components/freebox/const.py @@ -0,0 +1,20 @@ +"""Freebox component constants.""" +import socket + +DOMAIN = "freebox" +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" + +APP_DESC = { + "app_id": "hass", + "app_name": "Home Assistant", + "app_version": "0.104", + "device_name": socket.gethostname(), +} +API_VERSION = "v6" +CONFIG_FILE = "freebox.conf" + +PLATFORMS = ["device_tracker", "sensor", "switch"] + +# to store the cookie +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 63cf869990daf..2e721a40999e7 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,65 +1,109 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -from collections import namedtuple import logging +from typing import Dict -from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_FREEBOX +from .const import DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) async def async_get_scanner(hass, config): - """Validate the configuration and return a Freebox scanner.""" - scanner = FreeboxDeviceScanner(hass.data[DATA_FREEBOX]) - await scanner.async_connect() - return scanner if scanner.success_init else None - - -Device = namedtuple("Device", ["id", "name", "ip"]) - - -def _build_device(device_dict): - return Device( - device_dict["l2ident"]["id"], - device_dict["primary_name"], - device_dict["l3connectivities"][0]["addr"], - ) - - -class FreeboxDeviceScanner(DeviceScanner): - """Queries the Freebox device.""" + """Old way of setting up the platform.""" + pass - def __init__(self, fbx): - """Initialize the scanner.""" - self.last_results = {} - self.success_init = False - self.connection = fbx - async def async_connect(self): - """Initialize connection to the router.""" - # Test the router is accessible. - data = await self.connection.lan.get_hosts_list() - self.success_init = data is not None - - async def async_scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - await self.async_update_info() - return [device.id for device in self.last_results] - - async def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - name = next( - (result.name for result in self.last_results if result.id == device), None +async def async_setup_entry(hass, entry, async_add_entities): + """Validate the configuration and return a Freebox scanner.""" + fbx = hass.data[DOMAIN] + devices = await fbx.lan.get_hosts_list() + + _LOGGER.error(devices) + for device in devices: + _LOGGER.debug("Adding device_tracker for %s", device["primary_name"]) + + async_add_entities([FreeboxTrackerEntity(device)]) + + +class FreeboxTrackerEntity(TrackerEntity): + """Represent a tracked device.""" + + def __init__(self, device: Dict[str, any]): + """Set up the Freebox tracker entity.""" + self._device = device + self._unsub_dispatcher = None + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device["l2ident"]["id"] # MAC address + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._device["primary_name"] + + @property + def latitude(self): + """Return latitude value of the device.""" + return self.hass.config.latitude + + @property + def longitude(self): + """Return longitude value of the device.""" + return self.hass.config.longitude + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + @property + def source_type(self) -> str: + """Return the source type of the device.""" + return SOURCE_TYPE_ROUTER + + @property + def icon(self) -> str: + """Return the icon.""" + return icon_for_freebox_device(self._device) + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + } + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self.async_write_ha_state ) - return name - - async def async_update_info(self): - """Ensure the information from the Freebox router is up to date.""" - _LOGGER.debug("Checking Devices") - - hosts = await self.connection.lan.get_hosts_list() - - last_results = [_build_device(device) for device in hosts if device["active"]] - self.last_results = last_results + 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 battery icon valid identifier.""" + switcher = { + "freebox_player": "mdi:", + "laptop": "mdi:", + "multimedia_device": "mdi:", + "nas": "mdi:", + "networking_device": "mdi:", + "other": "mdi:", + "printer": "mdi:", + "smartphone": "mdi:", + "television": "mdi:", + "vg_console": "mdi:", + "workstation": "mdi:", + } + + return switcher.get(device["host_type"], "mdi:") diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 7a66490c90d41..bc5492d57d46b 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -1,6 +1,7 @@ { "domain": "freebox", "name": "Freebox", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": ["aiofreepybox==0.0.8"], "dependencies": [], diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 0653120b49c74..5a24d88654d23 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,17 +1,23 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging -from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND +from aiofreepybox import Freepybox + from homeassistant.helpers.entity import Entity -from . import DATA_FREEBOX +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the platform.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up the sensors.""" - fbx = hass.data[DATA_FREEBOX] + fbx = hass.data[DOMAIN] async_add_entities([FbxRXSensor(fbx), FbxTXSensor(fbx)], True) @@ -22,11 +28,17 @@ class FbxSensor(Entity): _unit = None _icon = None - def __init__(self, fbx): + def __init__(self, fbx: Freepybox): """Initialize the sensor.""" self._fbx = fbx self._state = None self._datas = None + self._unique_id = f"{fbx._access.base_url} {self._name}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id @property def name(self): diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json new file mode 100644 index 0000000000000..f47f1fbc110ea --- /dev/null +++ b/homeassistant/components/freebox/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "Freebox", + "step": { + "user": { + "title": "Freebox", + "data": { + "host": "Host", + "port": "Port" + } + } + }, + "error":{ + "already_configured": "Host already configured" + }, + "abort":{ + "already_configured": "Host already configured" + } + } +} diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 062d6a699feb9..f1a9ae616fc97 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -1,27 +1,40 @@ """Support for Freebox Delta, Revolution and Mini 4K.""" import logging +from aiofreepybox import Freepybox + from homeassistant.components.switch import SwitchDevice -from . import DATA_FREEBOX +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the platform.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up the switch.""" - fbx = hass.data[DATA_FREEBOX] + fbx = hass.data[DOMAIN] async_add_entities([FbxWifiSwitch(fbx)], True) class FbxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, fbx): + def __init__(self, fbx: Freepybox): """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None self._fbx = fbx + self._unique_id = f"{fbx._access.base_url} {self._name}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id @property def name(self): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 91fda9f1c3210..0018b85efcd1e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ "elgato", "emulated_roku", "esphome", + "freebox", "garmin_connect", "gdacs", "geofency", From 7f0be6de636c72bb4250d0468a179b97a6406871 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 2 Jan 2020 13:17:56 +0100 Subject: [PATCH 02/45] Add manufacturer in device_tracker info --- homeassistant/components/freebox/device_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 2e721a40999e7..d62c03a7ea967 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -77,6 +77,7 @@ def device_info(self) -> Dict[str, any]: return { "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, + "manufacturer": self._device["vendor_name"], } async def async_added_to_hass(self): From fbf4de9fa3a7a59ad99ac1c19f8be2faa5bc9c61 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 2 Jan 2020 13:50:50 +0100 Subject: [PATCH 03/45] Add device_info to sensor + switch --- homeassistant/components/freebox/sensor.py | 20 +++++++++++++--- homeassistant/components/freebox/switch.py | 27 +++++++++++++++------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 5a24d88654d23..22294ef3211a9 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,5 +1,6 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging +from typing import Dict from aiofreepybox import Freepybox @@ -18,7 +19,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up the sensors.""" fbx = hass.data[DOMAIN] - async_add_entities([FbxRXSensor(fbx), FbxTXSensor(fbx)], True) + fbx_conf = await fbx.system.get_config() + async_add_entities([FbxRXSensor(fbx, fbx_conf), FbxTXSensor(fbx, fbx_conf)], True) class FbxSensor(Entity): @@ -28,12 +30,14 @@ class FbxSensor(Entity): _unit = None _icon = None - def __init__(self, fbx: Freepybox): + def __init__(self, fbx: Freepybox, fbx_conf: Dict): """Initialize the sensor.""" self._fbx = fbx + self._fbx_name = fbx_conf["model_info"]["pretty_name"] + self._fbx_sw_v = fbx_conf["firmware_version"] self._state = None self._datas = None - self._unique_id = f"{fbx._access.base_url} {self._name}" + self._unique_id = f"{self._fbx._access.base_url} {self._name}" @property def unique_id(self) -> str: @@ -60,6 +64,16 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._fbx._access.base_url)}, + "name": self._fbx_name, + "manufacturer": "Freebox SAS", + "sw_version": self._fbx_sw_v, + } + async def async_update(self): """Fetch status from freebox.""" self._datas = await self._fbx.connection.get_status() diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index f1a9ae616fc97..af35f7310a51a 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -1,7 +1,9 @@ """Support for Freebox Delta, Revolution and Mini 4K.""" import logging +from typing import Dict from aiofreepybox import Freepybox +from aiofreepybox.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchDevice @@ -18,18 +20,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up the switch.""" fbx = hass.data[DOMAIN] - async_add_entities([FbxWifiSwitch(fbx)], True) + fbx_conf = await fbx.system.get_config() + async_add_entities([FbxWifiSwitch(fbx, fbx_conf)], True) class FbxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, fbx: Freepybox): + def __init__(self, fbx: Freepybox, fbx_conf: Dict): """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None self._fbx = fbx - self._unique_id = f"{fbx._access.base_url} {self._name}" + self._fbx_name = fbx_conf["model_info"]["pretty_name"] + self._fbx_sw_v = fbx_conf["firmware_version"] + self._unique_id = f"{self._fbx._access.base_url} {self._name}" @property def unique_id(self) -> str: @@ -46,18 +51,24 @@ def is_on(self): """Return true if device is on.""" return self._state + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._fbx._access.base_url)}, + "name": self._fbx_name, + "manufacturer": "Freebox SAS", + "sw_version": self._fbx_sw_v, + } + async def _async_set_state(self, enabled): """Turn the switch on or off.""" - from aiofreepybox.exceptions import InsufficientPermissionsError - wifi_config = {"enabled": enabled} try: await self._fbx.wifi.set_global_config(wifi_config) except InsufficientPermissionsError: _LOGGER.warning( - "Home Assistant does not have permissions to" - " modify the Freebox settings. Please refer" - " to documentation." + "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation." ) async def async_turn_on(self, **kwargs): From 16fcbd4a27ca59475acbdaaef67f884afde36fa9 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 2 Jan 2020 13:57:43 +0100 Subject: [PATCH 04/45] Add device_info: connections --- homeassistant/components/freebox/device_tracker.py | 9 ++++++++- homeassistant/components/freebox/sensor.py | 3 +++ homeassistant/components/freebox/switch.py | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index d62c03a7ea967..c91c48f356879 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,6 +4,7 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN, TRACKER_UPDATE @@ -39,7 +40,7 @@ def __init__(self, device: Dict[str, any]): @property def unique_id(self) -> str: """Return a unique ID.""" - return self._device["l2ident"]["id"] # MAC address + return self.mac @property def name(self) -> str: @@ -75,11 +76,17 @@ def icon(self) -> str: def device_info(self) -> Dict[str, any]: """Return the device information.""" return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "manufacturer": self._device["vendor_name"], } + @property + def mac(self) -> str: + """Return the MAC address.""" + return self._device["l2ident"]["id"] + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 22294ef3211a9..2727bc8c55435 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -4,6 +4,7 @@ from aiofreepybox import Freepybox +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import Entity from .const import DOMAIN @@ -33,6 +34,7 @@ class FbxSensor(Entity): def __init__(self, fbx: Freepybox, fbx_conf: Dict): """Initialize the sensor.""" self._fbx = fbx + self._fbx_mac = fbx_conf["mac"] self._fbx_name = fbx_conf["model_info"]["pretty_name"] self._fbx_sw_v = fbx_conf["firmware_version"] self._state = None @@ -68,6 +70,7 @@ def state(self): def device_info(self) -> Dict[str, any]: """Return the device information.""" return { + "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, "identifiers": {(DOMAIN, self._fbx._access.base_url)}, "name": self._fbx_name, "manufacturer": "Freebox SAS", diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index af35f7310a51a..c8a4a4199ccad 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -6,6 +6,7 @@ from aiofreepybox.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN @@ -32,6 +33,7 @@ def __init__(self, fbx: Freepybox, fbx_conf: Dict): self._name = "Freebox WiFi" self._state = None self._fbx = fbx + self._fbx_mac = fbx_conf["mac"] self._fbx_name = fbx_conf["model_info"]["pretty_name"] self._fbx_sw_v = fbx_conf["firmware_version"] self._unique_id = f"{self._fbx._access.base_url} {self._name}" @@ -55,6 +57,7 @@ def is_on(self): def device_info(self) -> Dict[str, any]: """Return the device information.""" return { + "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, "identifiers": {(DOMAIN, self._fbx._access.base_url)}, "name": self._fbx_name, "manufacturer": "Freebox SAS", From eb542d87ac0389b2a60083384c4b1ba310afaaf1 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:02:35 +0100 Subject: [PATCH 05/45] Add config_flow test + update .coveragerc --- .coveragerc | 5 +- requirements_test_all.txt | 3 + tests/components/freebox/__init__.py | 1 + tests/components/freebox/test_config_flow.py | 92 ++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/components/freebox/__init__.py create mode 100644 tests/components/freebox/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 89da763f3cad5..7bd5ed007319f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -244,7 +244,10 @@ omit = homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py - homeassistant/components/freebox/* + homeassistant/components/freebox/__init__.py + homeassistant/components/freebox/device_tracker.py + homeassistant/components/freebox/sensor.py + homeassistant/components/freebox/switch.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritzbox/* homeassistant/components/fritzbox_callmonitor/sensor.py diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ae7cb1c5821f..40359d3a3e171 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,6 +61,9 @@ aiobotocore==0.11.1 # homeassistant.components.esphome aioesphomeapi==2.6.1 +# homeassistant.components.freebox +aiofreepybox==0.0.8 + # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.25 diff --git a/tests/components/freebox/__init__.py b/tests/components/freebox/__init__.py new file mode 100644 index 0000000000000..727b60ae78a85 --- /dev/null +++ b/tests/components/freebox/__init__.py @@ -0,0 +1 @@ +"""Tests for the Freebox component.""" diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py new file mode 100644 index 0000000000000..a1b995627d123 --- /dev/null +++ b/tests/components/freebox/test_config_flow.py @@ -0,0 +1,92 @@ +"""Tests for the Freebox config flow.""" +import asyncio +from unittest.mock import MagicMock, patch + +from aiofreepybox.exceptions import HttpRequestError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.freebox import config_flow +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +HOST = "myrouter.freeboxos.fr" +PORT = 1234 + + +@pytest.fixture(name="connect") +def mock_controller_connect(): + """Mock a successful connection.""" + with patch( + "homeassistant.components.freebox.config_flow.Freepybox" + ) as service_mock: + service_mock.return_value.open = MagicMock(return_value=asyncio.Future()) + service_mock.return_value.open.return_value.set_result(None) + yield service_mock + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.FreeboxFlowHandler() + flow.hass = hass + return flow + + +async def test_user(hass, connect): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + +async def test_import(hass, connect): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + +async def test_abort_if_already_setup(hass): + """Test we abort if component is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry(domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}).add_to_hass( + hass + ) + + # Should fail, same HOST (import) + result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same HOST (flow) + result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "already_configured"} + + +async def test_abort_on_connection_failed(hass): + """Test when we have errors during connection.""" + flow = init_config_flow(hass) + + with patch( + "homeassistant.components.freebox.config_flow.Freepybox.open", + side_effect=HttpRequestError(), + ): + result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "connection_failed"} From 1b8eb6f15c83da18cfe68292209973e577f82de9 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:09:00 +0100 Subject: [PATCH 06/45] Typing --- homeassistant/components/freebox/device_tracker.py | 8 ++++++-- homeassistant/components/freebox/sensor.py | 6 +++++- homeassistant/components/freebox/switch.py | 6 +++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index c91c48f356879..3d17f53e2ecef 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,8 +4,10 @@ from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN, TRACKER_UPDATE @@ -17,8 +19,10 @@ async def async_get_scanner(hass, config): pass -async def async_setup_entry(hass, entry, async_add_entities): - """Validate the configuration and return a Freebox scanner.""" +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the device_tracker.""" fbx = hass.data[DOMAIN] devices = await fbx.lan.get_hosts_list() diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 2727bc8c55435..2d35a047aad95 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -4,8 +4,10 @@ from aiofreepybox import Freepybox +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN @@ -17,7 +19,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= pass -async def async_setup_entry(hass, entry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the sensors.""" fbx = hass.data[DOMAIN] fbx_conf = await fbx.system.get_config() diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index c8a4a4199ccad..fb0bc45c0d242 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -6,7 +6,9 @@ from aiofreepybox.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.typing import HomeAssistantType from .const import DOMAIN @@ -18,7 +20,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= pass -async def async_setup_entry(hass, entry, async_add_entities) -> None: +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the switch.""" fbx = hass.data[DOMAIN] fbx_conf = await fbx.system.get_config() From c16e63b2aa62bff8b0c083d580a5c4ad49f1c66e Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:39:49 +0100 Subject: [PATCH 07/45] Add device_type icon --- .../components/freebox/device_tracker.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 3d17f53e2ecef..39618506d873e 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -103,19 +103,24 @@ async def async_will_remove_from_hass(self): def icon_for_freebox_device(device) -> str: - """Return a battery icon valid identifier.""" + """Return a host icon from his type.""" switcher = { - "freebox_player": "mdi:", - "laptop": "mdi:", - "multimedia_device": "mdi:", - "nas": "mdi:", - "networking_device": "mdi:", - "other": "mdi:", - "printer": "mdi:", - "smartphone": "mdi:", - "television": "mdi:", - "vg_console": "mdi:", - "workstation": "mdi:", + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", } - return switcher.get(device["host_type"], "mdi:") + return switcher.get(device["host_type"], "mdi:help-network") From 6cbdd200a82950e4e73cba182f20c7e7a5c516c1 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:40:42 +0100 Subject: [PATCH 08/45] Remove one error log --- homeassistant/components/freebox/device_tracker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 39618506d873e..603958d88fd29 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -26,7 +26,6 @@ async def async_setup_entry( fbx = hass.data[DOMAIN] devices = await fbx.lan.get_hosts_list() - _LOGGER.error(devices) for device in devices: _LOGGER.debug("Adding device_tracker for %s", device["primary_name"]) From 2225da626223e2cb0551aa428bc904f6aac38f5b Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:43:46 +0100 Subject: [PATCH 09/45] Fix pylint --- homeassistant/components/freebox/sensor.py | 4 ++-- homeassistant/components/freebox/switch.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 2d35a047aad95..d93688282937f 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -43,7 +43,7 @@ def __init__(self, fbx: Freepybox, fbx_conf: Dict): self._fbx_sw_v = fbx_conf["firmware_version"] self._state = None self._datas = None - self._unique_id = f"{self._fbx._access.base_url} {self._name}" + self._unique_id = f"{self._fbx_mac} {self._name}" @property def unique_id(self) -> str: @@ -75,7 +75,7 @@ def device_info(self) -> Dict[str, any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, - "identifiers": {(DOMAIN, self._fbx._access.base_url)}, + "identifiers": {(DOMAIN, self._fbx_mac)}, "name": self._fbx_name, "manufacturer": "Freebox SAS", "sw_version": self._fbx_sw_v, diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index fb0bc45c0d242..5860d2529ccbb 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -40,7 +40,7 @@ def __init__(self, fbx: Freepybox, fbx_conf: Dict): self._fbx_mac = fbx_conf["mac"] self._fbx_name = fbx_conf["model_info"]["pretty_name"] self._fbx_sw_v = fbx_conf["firmware_version"] - self._unique_id = f"{self._fbx._access.base_url} {self._name}" + self._unique_id = f"{self._fbx_mac} {self._name}" @property def unique_id(self) -> str: @@ -62,7 +62,7 @@ def device_info(self) -> Dict[str, any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, - "identifiers": {(DOMAIN, self._fbx._access.base_url)}, + "identifiers": {(DOMAIN, self._fbx_mac)}, "name": self._fbx_name, "manufacturer": "Freebox SAS", "sw_version": self._fbx_sw_v, From 28086a337b7362dd081426bd09f494a6c1b58dcd Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sun, 5 Jan 2020 16:51:45 +0100 Subject: [PATCH 10/45] Add myself as CODEOWNER --- CODEOWNERS | 2 +- homeassistant/components/freebox/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6714c948402bd..a00bc8288a595 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -120,7 +120,7 @@ homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/foursquare/* @robbiet480 -homeassistant/components/freebox/* @snoof85 +homeassistant/components/freebox/* @snoof85 @Quentame homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index bc5492d57d46b..1bfb4924a78b6 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -6,5 +6,5 @@ "requirements": ["aiofreepybox==0.0.8"], "dependencies": [], "after_dependencies": ["discovery"], - "codeowners": ["@snoof85"] + "codeowners": ["@snoof85", "@Quentame"] } From 2222fac1e44dbda8400dc41915cffb07b46f7f05 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 6 Jan 2020 14:02:58 +0100 Subject: [PATCH 11/45] Handle sync in one place --- homeassistant/components/freebox/__init__.py | 307 +++++++++++++++++- homeassistant/components/freebox/const.py | 65 +++- .../components/freebox/device_tracker.py | 63 ++-- homeassistant/components/freebox/sensor.py | 85 ++--- homeassistant/components/freebox/switch.py | 25 +- 5 files changed, 421 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index d3d3f8751a7f8..ceb97c140dc47 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,7 +1,10 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from datetime import datetime, timedelta import logging +from typing import Dict from aiofreepybox import Freepybox +from aiofreepybox.api.wifi import Wifi from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol @@ -9,12 +12,30 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from .const import API_VERSION, APP_DESC, CONFIG_FILE, DOMAIN, PLATFORMS +from .const import ( + API_VERSION, + APP_DESC, + CONFIG_FILE, + CONN_SENSORS, + DOMAIN, + PLATFORMS, + SENSOR_DEVICE_CLASS, + SENSOR_ICON, + SENSOR_NAME, + SENSOR_UNIT, + SENSOR_UPDATE, + TEMP_SENSORS, + TRACKER_UPDATE, +) _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=30) CONFIG_SCHEMA = vol.Schema( { @@ -61,10 +82,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Freebox component.""" token_file = hass.config.path(CONFIG_FILE) - fbx = Freepybox(APP_DESC, token_file, API_VERSION) + fbx = FreeboxRouter(hass, entry, token_file) try: - await fbx.open(entry.data[CONF_HOST], entry.data[CONF_PORT]) + await fbx.setup() except HttpRequestError: _LOGGER.exception("Failed to connect to Freebox") return False @@ -79,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): # Services async def async_freebox_reboot(call): """Handle reboot service call.""" - await fbx.system.reboot() + await fbx.reboot() hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) @@ -100,3 +121,281 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): fbx = hass.data[DOMAIN] await fbx.close() return True + + +class FreeboxRouter: + """Representation of a Freebox router.""" + + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, token_file: str): + """Initialize a Freebox router.""" + self.hass = hass + self._entry = entry + self._host = entry.data[CONF_HOST] + self._port = entry.data[CONF_PORT] + self._token_file = token_file + + self._api: Freepybox = None + self._name = None + self._mac = None + self._sw_v = None + + # Devices + self._devices: Dict[str, FreeboxDevice] = {} + + # Sensors + self._temp_sensors: Dict[str, FreeboxSensor] = {} + self._conn_sensors: Dict[str, FreeboxSensor] = {} + + async def setup(self) -> None: + """Set up a Freebox router.""" + self._api = Freepybox(APP_DESC, self._token_file, API_VERSION) + + await self._api.open(self._host, self._port) + + # System + fbx_config = await self._api.system.get_config() + self._mac = fbx_config["mac"] + self._name = fbx_config["model_info"]["pretty_name"] + self._sw_v = fbx_config["firmware_version"] + + # Devices & sensors + await self.update_all() + async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + + async def update_all(self, now=None) -> None: + """Update all Freebox platforms.""" + await self.update_devices() + await self.update_sensors() + + async def update_devices(self) -> None: + """Update Freebox devices.""" + if self._api is None: + return + + fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + for fbx_device in fbx_devices: + device_name = fbx_device["primary_name"] + device_mac = fbx_device["l2ident"]["id"] + + if self._devices.get(device_mac) is not None: + # Seen device -> updating + _LOGGER.debug("Updating Freebox device: %s", device_name) + self._devices[device_mac].update(fbx_device) + else: + # New device, should be unique + _LOGGER.debug( + "Adding Freebox device: %s [MAC: %s]", device_name, device_mac, + ) + self._devices[device_mac] = FreeboxDevice(fbx_device) + self._devices[device_mac].update(fbx_device) + + dispatcher_send(self.hass, TRACKER_UPDATE) + + async def update_sensors(self) -> None: + """Update Freebox sensors.""" + if self._api is None: + return + + # System sensors + syst_datas: Dict[str, any] = await self._api.system.get_config() + temp_datas = {item["id"]: item for item in syst_datas["sensors"]} + + for sensor_key, sensor_attrs in TEMP_SENSORS.items(): + if temp_datas.get(sensor_key) is None: + continue + + if self._temp_sensors.get(sensor_key) is not None: + # Seen sensor -> updating + _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) + self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + else: + # New sensor, should be unique + _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) + self._temp_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + + # Connection sensors + conn_datas: Dict[str, any] = await self._api.connection.get_status() + for sensor_key, sensor_attrs in CONN_SENSORS.items(): + if self._conn_sensors.get(sensor_key) is not None: + # Seen sensor -> updating + _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) + self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + else: + # New sensor, should be unique + _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) + self._conn_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + + dispatcher_send(self.hass, SENSOR_UPDATE) + + async def reboot(self) -> None: + """Reboot the Freebox.""" + await self._api.system.reboot() + + async def close(self) -> None: + """Close the connection.""" + await self._api.close() + self._api = None + + @property + def name(self) -> str: + """Return the router name.""" + return self._name + + @property + def mac(self) -> str: + """Return the router MAC address.""" + return self._mac + + @property + def firmware_version(self) -> str: + """Return the router sofware version.""" + return self._sw_v + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "identifiers": {(DOMAIN, self.mac)}, + "name": self.name, + "manufacturer": "Freebox SAS", + "sw_version": self.firmware_version, + } + + @property + def devices(self) -> Dict[str, any]: + """Return all devices.""" + return self._devices + + @property + def sensors(self) -> Dict[str, any]: + """Return all sensors.""" + return {**self._temp_sensors, **self._conn_sensors} + + @property + def wifi(self) -> Wifi: + """Return the wifi.""" + return self._api.wifi + + +class FreeboxDevice: + """Representation of a Freebox device.""" + + def __init__(self, device: Dict[str, any]): + """Initialize a Freebox device.""" + self._name = device["primary_name"] + self._mac = device["l2ident"]["id"] + self._manufacturer = device["vendor_name"] + self._icon = icon_for_freebox_device(device) + self._reachable = device["reachable"] + + self._attrs = { + "last_time_reachable": datetime.fromtimestamp(device["last_time_reachable"]) + } + + def update(self, device: Dict[str, any]) -> None: + """Update the Freebox device.""" + self._reachable = device["reachable"] + self._attrs["last_time_reachable"] = datetime.fromtimestamp( + device["last_time_reachable"] + ) + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def mac(self) -> str: + """Return the MAC address.""" + return self._mac + + @property + def manufacturer(self) -> str: + """Return the manufacturer.""" + return self._manufacturer + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def reachable(self) -> str: + """Return the reachability (present or not on the network).""" + return self._reachable + + @property + def state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs + + +class FreeboxSensor: + """Representation of a Freebox sensor.""" + + def __init__(self, sensor: Dict[str, any]): + """Initialize a Freebox sensor.""" + self._state = None + self._name = sensor[SENSOR_NAME] + self._unit = sensor[SENSOR_UNIT] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + + def update(self, state: any) -> None: + """Update the Freebox sensor.""" + if self._unit == "KB/s": + self._state = round(state / 1000, 2) + else: + self._state = state + + @property + def state(self) -> str: + """Return the state.""" + return self._state + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def unit(self) -> str: + """Return the unit.""" + return self._unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class + + +def icon_for_freebox_device(device) -> str: + """Return a host icon from his type.""" + switcher = { + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", + } + + return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index f59f416d98641..42e49802627c4 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -1,13 +1,16 @@ """Freebox component constants.""" import socket +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS + DOMAIN = "freebox" TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +SENSOR_UPDATE = f"{DOMAIN}_sensor_update" APP_DESC = { "app_id": "hass", "app_name": "Home Assistant", - "app_version": "0.104", + "app_version": "0.105", "device_name": socket.gethostname(), } API_VERSION = "v6" @@ -18,3 +21,63 @@ # to store the cookie STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 + +# Sensor +SENSOR_NAME = "name" +SENSOR_UNIT = "unit" +SENSOR_ICON = "icon" +SENSOR_DEVICE_CLASS = "device_class" + +CONN_SENSORS = { + "rate_down": { + SENSOR_NAME: "Freebox download speed", + SENSOR_UNIT: "KB/s", + SENSOR_ICON: "mdi:download-network", + SENSOR_DEVICE_CLASS: None, + }, + "rate_up": { + SENSOR_NAME: "Freebox upload speed", + SENSOR_UNIT: "KB/s", + SENSOR_ICON: "mdi:upload-network", + SENSOR_DEVICE_CLASS: None, + }, + "ipv4": { + SENSOR_NAME: "Freebox IPv4", + SENSOR_UNIT: None, + SENSOR_ICON: "mdi:ip", + SENSOR_DEVICE_CLASS: None, + }, + "ipv6": { + SENSOR_NAME: "Freebox IPv6", + SENSOR_UNIT: None, + SENSOR_ICON: "mdi:ip", + SENSOR_DEVICE_CLASS: None, + }, +} + +TEMP_SENSORS = { + "temp_hdd": { + SENSOR_NAME: "Freebox HDD temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "temp_sw": { + SENSOR_NAME: "Freebox switch temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "temp_cpum": { + SENSOR_NAME: "Freebox CPU M temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + "temp_cpub": { + SENSOR_NAME: "Freebox CPU B temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, +} diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 603958d88fd29..74ba217c5ec70 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -9,6 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType +from . import FreeboxDevice from .const import DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) @@ -23,19 +24,15 @@ async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up the device_tracker.""" - fbx = hass.data[DOMAIN] - devices = await fbx.lan.get_hosts_list() - - for device in devices: - _LOGGER.debug("Adding device_tracker for %s", device["primary_name"]) - + for device in hass.data[DOMAIN].devices.values(): + _LOGGER.debug("Adding device_tracker for %s", device.name) async_add_entities([FreeboxTrackerEntity(device)]) class FreeboxTrackerEntity(TrackerEntity): """Represent a tracked device.""" - def __init__(self, device: Dict[str, any]): + def __init__(self, device: FreeboxDevice): """Set up the Freebox tracker entity.""" self._device = device self._unsub_dispatcher = None @@ -43,22 +40,26 @@ def __init__(self, device: Dict[str, any]): @property def unique_id(self) -> str: """Return a unique ID.""" - return self.mac + return self._device.mac @property def name(self) -> str: """Return the name of the device.""" - return self._device["primary_name"] + return self._device.name @property def latitude(self): """Return latitude value of the device.""" - return self.hass.config.latitude + if self._device.reachable: + return self.hass.config.latitude + return None @property def longitude(self): """Return longitude value of the device.""" - return self.hass.config.longitude + if self._device.reachable: + return self.hass.config.longitude + return None @property def should_poll(self) -> bool: @@ -73,23 +74,23 @@ def source_type(self) -> str: @property def icon(self) -> str: """Return the icon.""" - return icon_for_freebox_device(self._device) + return self._device.icon + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the device state attributes.""" + return self._device.state_attributes @property def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, - "manufacturer": self._device["vendor_name"], + "manufacturer": self._device.manufacturer, } - @property - def mac(self) -> str: - """Return the MAC address.""" - return self._device["l2ident"]["id"] - async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( @@ -99,27 +100,3 @@ async def async_added_to_hass(self): 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.""" - switcher = { - "freebox_delta": "mdi:television-guide", - "freebox_hd": "mdi:television-guide", - "freebox_mini": "mdi:television-guide", - "freebox_player": "mdi:television-guide", - "ip_camera": "mdi:cctv", - "ip_phone": "mdi:phone-voip", - "laptop": "mdi:laptop", - "multimedia_device": "mdi:play-network", - "nas": "mdi:nas", - "networking_device": "mdi:network", - "printer": "mdi:printer", - "smartphone": "mdi:cellphone", - "tablet": "mdi:tablet", - "television": "mdi:television", - "vg_console": "mdi:gamepad-variant", - "workstation": "mdi:desktop-tower-monitor", - } - - return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index d93688282937f..b4d4f25420203 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -2,14 +2,13 @@ import logging from typing import Dict -from aiofreepybox import Freepybox - from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN +from . import FreeboxRouter, FreeboxSensor +from .const import DOMAIN, SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) @@ -24,26 +23,24 @@ async def async_setup_entry( ) -> None: """Set up the sensors.""" fbx = hass.data[DOMAIN] - fbx_conf = await fbx.system.get_config() - async_add_entities([FbxRXSensor(fbx, fbx_conf), FbxTXSensor(fbx, fbx_conf)], True) + + entities = [] + + for sensor in fbx.sensors.values(): + entities.append(FbxSensor(fbx, sensor)) + + async_add_entities(entities, True) class FbxSensor(Entity): """Representation of a freebox sensor.""" - _name = "generic" - _unit = None - _icon = None - - def __init__(self, fbx: Freepybox, fbx_conf: Dict): + def __init__(self, fbx: FreeboxRouter, fbx_sensor: FreeboxSensor): """Initialize the sensor.""" self._fbx = fbx - self._fbx_mac = fbx_conf["mac"] - self._fbx_name = fbx_conf["model_info"]["pretty_name"] - self._fbx_sw_v = fbx_conf["firmware_version"] - self._state = None - self._datas = None - self._unique_id = f"{self._fbx_mac} {self._name}" + self._fbx_sensor = fbx_sensor + self._unique_id = f"{self._fbx.mac} {self._fbx_sensor.name}" + self._unsub_dispatcher = None @property def unique_id(self) -> str: @@ -53,62 +50,34 @@ def unique_id(self) -> str: @property def name(self): """Return the name of the sensor.""" - return self._name + return self._fbx_sensor.name @property def unit_of_measurement(self): """Return the unit of the sensor.""" - return self._unit + return self._fbx_sensor.unit @property def icon(self): """Return the icon of the sensor.""" - return self._icon + return self._fbx_sensor.icon @property def state(self): """Return the state of the sensor.""" - return self._state + return self._fbx_sensor.state @property def device_info(self) -> Dict[str, any]: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, - "identifiers": {(DOMAIN, self._fbx_mac)}, - "name": self._fbx_name, - "manufacturer": "Freebox SAS", - "sw_version": self._fbx_sw_v, - } - - async def async_update(self): - """Fetch status from freebox.""" - self._datas = await self._fbx.connection.get_status() - - -class FbxRXSensor(FbxSensor): - """Update the Freebox RxSensor.""" - - _name = "Freebox download speed" - _unit = DATA_RATE_KILOBYTES_PER_SECOND - _icon = "mdi:download-network" - - async def async_update(self): - """Get the value from fetched datas.""" - await super().async_update() - if self._datas is not None: - self._state = round(self._datas["rate_down"] / 1000, 2) - - -class FbxTXSensor(FbxSensor): - """Update the Freebox TxSensor.""" + return self._fbx.device_info - _name = "Freebox upload speed" - _unit = DATA_RATE_KILOBYTES_PER_SECOND - _icon = "mdi:upload-network" + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, SENSOR_UPDATE, self.async_write_ha_state + ) - async def async_update(self): - """Get the value from fetched datas.""" - await super().async_update() - if self._datas is not None: - self._state = round(self._datas["rate_up"] / 1000, 2) + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 5860d2529ccbb..17e57b7efa618 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -2,14 +2,13 @@ import logging from typing import Dict -from aiofreepybox import Freepybox from aiofreepybox.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.typing import HomeAssistantType +from . import FreeboxRouter from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,22 +24,18 @@ async def async_setup_entry( ) -> None: """Set up the switch.""" fbx = hass.data[DOMAIN] - fbx_conf = await fbx.system.get_config() - async_add_entities([FbxWifiSwitch(fbx, fbx_conf)], True) + async_add_entities([FbxWifiSwitch(fbx)], True) class FbxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, fbx: Freepybox, fbx_conf: Dict): + def __init__(self, fbx: FreeboxRouter): """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None self._fbx = fbx - self._fbx_mac = fbx_conf["mac"] - self._fbx_name = fbx_conf["model_info"]["pretty_name"] - self._fbx_sw_v = fbx_conf["firmware_version"] - self._unique_id = f"{self._fbx_mac} {self._name}" + self._unique_id = f"{self._fbx.mac} {self._name}" @property def unique_id(self) -> str: @@ -60,15 +55,9 @@ def is_on(self): @property def device_info(self) -> Dict[str, any]: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._fbx_mac)}, - "identifiers": {(DOMAIN, self._fbx_mac)}, - "name": self._fbx_name, - "manufacturer": "Freebox SAS", - "sw_version": self._fbx_sw_v, - } - - async def _async_set_state(self, enabled): + return self._fbx.device_info + + async def _async_set_state(self, enabled: bool): """Turn the switch on or off.""" wifi_config = {"enabled": enabled} try: From 3452eb7a828fc2833e9f8b2046e44df3ee7c370a Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 9 Jan 2020 13:10:55 +0100 Subject: [PATCH 12/45] Separate the Freebox[Router/Device/Sensor] from __init__.py --- .coveragerc | 1 + homeassistant/components/freebox/__init__.py | 303 +---------------- homeassistant/components/freebox/const.py | 2 +- .../components/freebox/device_tracker.py | 2 +- homeassistant/components/freebox/router.py | 310 ++++++++++++++++++ homeassistant/components/freebox/sensor.py | 2 +- homeassistant/components/freebox/switch.py | 2 +- 7 files changed, 317 insertions(+), 305 deletions(-) create mode 100644 homeassistant/components/freebox/router.py diff --git a/.coveragerc b/.coveragerc index 7bd5ed007319f..61f9718eac0e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -246,6 +246,7 @@ omit = homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/__init__.py homeassistant/components/freebox/device_tracker.py + homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/device_tracker.py diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index ceb97c140dc47..ebfb68970e5d0 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,10 +1,6 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -from datetime import datetime, timedelta import logging -from typing import Dict -from aiofreepybox import Freepybox -from aiofreepybox.api.wifi import Wifi from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol @@ -12,30 +8,13 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from .const import ( - API_VERSION, - APP_DESC, - CONFIG_FILE, - CONN_SENSORS, - DOMAIN, - PLATFORMS, - SENSOR_DEVICE_CLASS, - SENSOR_ICON, - SENSOR_NAME, - SENSOR_UNIT, - SENSOR_UPDATE, - TEMP_SENSORS, - TRACKER_UPDATE, -) +from .const import CONFIG_FILE, DOMAIN, PLATFORMS +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=30) CONFIG_SCHEMA = vol.Schema( { @@ -121,281 +100,3 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): fbx = hass.data[DOMAIN] await fbx.close() return True - - -class FreeboxRouter: - """Representation of a Freebox router.""" - - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, token_file: str): - """Initialize a Freebox router.""" - self.hass = hass - self._entry = entry - self._host = entry.data[CONF_HOST] - self._port = entry.data[CONF_PORT] - self._token_file = token_file - - self._api: Freepybox = None - self._name = None - self._mac = None - self._sw_v = None - - # Devices - self._devices: Dict[str, FreeboxDevice] = {} - - # Sensors - self._temp_sensors: Dict[str, FreeboxSensor] = {} - self._conn_sensors: Dict[str, FreeboxSensor] = {} - - async def setup(self) -> None: - """Set up a Freebox router.""" - self._api = Freepybox(APP_DESC, self._token_file, API_VERSION) - - await self._api.open(self._host, self._port) - - # System - fbx_config = await self._api.system.get_config() - self._mac = fbx_config["mac"] - self._name = fbx_config["model_info"]["pretty_name"] - self._sw_v = fbx_config["firmware_version"] - - # Devices & sensors - await self.update_all() - async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) - - async def update_all(self, now=None) -> None: - """Update all Freebox platforms.""" - await self.update_devices() - await self.update_sensors() - - async def update_devices(self) -> None: - """Update Freebox devices.""" - if self._api is None: - return - - fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() - for fbx_device in fbx_devices: - device_name = fbx_device["primary_name"] - device_mac = fbx_device["l2ident"]["id"] - - if self._devices.get(device_mac) is not None: - # Seen device -> updating - _LOGGER.debug("Updating Freebox device: %s", device_name) - self._devices[device_mac].update(fbx_device) - else: - # New device, should be unique - _LOGGER.debug( - "Adding Freebox device: %s [MAC: %s]", device_name, device_mac, - ) - self._devices[device_mac] = FreeboxDevice(fbx_device) - self._devices[device_mac].update(fbx_device) - - dispatcher_send(self.hass, TRACKER_UPDATE) - - async def update_sensors(self) -> None: - """Update Freebox sensors.""" - if self._api is None: - return - - # System sensors - syst_datas: Dict[str, any] = await self._api.system.get_config() - temp_datas = {item["id"]: item for item in syst_datas["sensors"]} - - for sensor_key, sensor_attrs in TEMP_SENSORS.items(): - if temp_datas.get(sensor_key) is None: - continue - - if self._temp_sensors.get(sensor_key) is not None: - # Seen sensor -> updating - _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) - else: - # New sensor, should be unique - _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._temp_sensors[sensor_key] = FreeboxSensor(sensor_attrs) - self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) - - # Connection sensors - conn_datas: Dict[str, any] = await self._api.connection.get_status() - for sensor_key, sensor_attrs in CONN_SENSORS.items(): - if self._conn_sensors.get(sensor_key) is not None: - # Seen sensor -> updating - _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) - else: - # New sensor, should be unique - _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._conn_sensors[sensor_key] = FreeboxSensor(sensor_attrs) - self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) - - dispatcher_send(self.hass, SENSOR_UPDATE) - - async def reboot(self) -> None: - """Reboot the Freebox.""" - await self._api.system.reboot() - - async def close(self) -> None: - """Close the connection.""" - await self._api.close() - self._api = None - - @property - def name(self) -> str: - """Return the router name.""" - return self._name - - @property - def mac(self) -> str: - """Return the router MAC address.""" - return self._mac - - @property - def firmware_version(self) -> str: - """Return the router sofware version.""" - return self._sw_v - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, - "identifiers": {(DOMAIN, self.mac)}, - "name": self.name, - "manufacturer": "Freebox SAS", - "sw_version": self.firmware_version, - } - - @property - def devices(self) -> Dict[str, any]: - """Return all devices.""" - return self._devices - - @property - def sensors(self) -> Dict[str, any]: - """Return all sensors.""" - return {**self._temp_sensors, **self._conn_sensors} - - @property - def wifi(self) -> Wifi: - """Return the wifi.""" - return self._api.wifi - - -class FreeboxDevice: - """Representation of a Freebox device.""" - - def __init__(self, device: Dict[str, any]): - """Initialize a Freebox device.""" - self._name = device["primary_name"] - self._mac = device["l2ident"]["id"] - self._manufacturer = device["vendor_name"] - self._icon = icon_for_freebox_device(device) - self._reachable = device["reachable"] - - self._attrs = { - "last_time_reachable": datetime.fromtimestamp(device["last_time_reachable"]) - } - - def update(self, device: Dict[str, any]) -> None: - """Update the Freebox device.""" - self._reachable = device["reachable"] - self._attrs["last_time_reachable"] = datetime.fromtimestamp( - device["last_time_reachable"] - ) - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def mac(self) -> str: - """Return the MAC address.""" - return self._mac - - @property - def manufacturer(self) -> str: - """Return the manufacturer.""" - return self._manufacturer - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def reachable(self) -> str: - """Return the reachability (present or not on the network).""" - return self._reachable - - @property - def state_attributes(self) -> Dict[str, any]: - """Return the attributes.""" - return self._attrs - - -class FreeboxSensor: - """Representation of a Freebox sensor.""" - - def __init__(self, sensor: Dict[str, any]): - """Initialize a Freebox sensor.""" - self._state = None - self._name = sensor[SENSOR_NAME] - self._unit = sensor[SENSOR_UNIT] - self._icon = sensor[SENSOR_ICON] - self._device_class = sensor[SENSOR_DEVICE_CLASS] - - def update(self, state: any) -> None: - """Update the Freebox sensor.""" - if self._unit == "KB/s": - self._state = round(state / 1000, 2) - else: - self._state = state - - @property - def state(self) -> str: - """Return the state.""" - return self._state - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def unit(self) -> str: - """Return the unit.""" - return self._unit - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def device_class(self) -> str: - """Return the device_class.""" - return self._device_class - - -def icon_for_freebox_device(device) -> str: - """Return a host icon from his type.""" - switcher = { - "freebox_delta": "mdi:television-guide", - "freebox_hd": "mdi:television-guide", - "freebox_mini": "mdi:television-guide", - "freebox_player": "mdi:television-guide", - "ip_camera": "mdi:cctv", - "ip_phone": "mdi:phone-voip", - "laptop": "mdi:laptop", - "multimedia_device": "mdi:play-network", - "nas": "mdi:nas", - "networking_device": "mdi:network", - "printer": "mdi:printer", - "smartphone": "mdi:cellphone", - "tablet": "mdi:tablet", - "television": "mdi:television", - "vg_console": "mdi:gamepad-variant", - "workstation": "mdi:desktop-tower-monitor", - } - - return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 42e49802627c4..7fe364d538ea3 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -10,7 +10,7 @@ APP_DESC = { "app_id": "hass", "app_name": "Home Assistant", - "app_version": "0.105", + "app_version": "0.104", "device_name": socket.gethostname(), } API_VERSION = "v6" diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 74ba217c5ec70..070d859ec7eb1 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -9,8 +9,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from . import FreeboxDevice from .const import DOMAIN, TRACKER_UPDATE +from .router import FreeboxDevice _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py new file mode 100644 index 0000000000000..7b24e27036527 --- /dev/null +++ b/homeassistant/components/freebox/router.py @@ -0,0 +1,310 @@ +"""Represent the Freebox router and its devices and sensors.""" +from datetime import datetime, timedelta +import logging +from typing import Dict + +from aiofreepybox import Freepybox +from aiofreepybox.api.wifi import Wifi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + API_VERSION, + APP_DESC, + CONN_SENSORS, + DOMAIN, + SENSOR_DEVICE_CLASS, + SENSOR_ICON, + SENSOR_NAME, + SENSOR_UNIT, + SENSOR_UPDATE, + TEMP_SENSORS, + TRACKER_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=30) + + +class FreeboxRouter: + """Representation of a Freebox router.""" + + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, token_file: str): + """Initialize a Freebox router.""" + self.hass = hass + self._entry = entry + self._host = entry.data[CONF_HOST] + self._port = entry.data[CONF_PORT] + self._token_file = token_file + + self._api: Freepybox = None + self._name = None + self._mac = None + self._sw_v = None + + # Devices + self._devices: Dict[str, FreeboxDevice] = {} + + # Sensors + self._temp_sensors: Dict[str, FreeboxSensor] = {} + self._conn_sensors: Dict[str, FreeboxSensor] = {} + + async def setup(self) -> None: + """Set up a Freebox router.""" + self._api = Freepybox(APP_DESC, self._token_file, API_VERSION) + + await self._api.open(self._host, self._port) + + # System + fbx_config = await self._api.system.get_config() + self._mac = fbx_config["mac"] + self._name = fbx_config["model_info"]["pretty_name"] + self._sw_v = fbx_config["firmware_version"] + + # Devices & sensors + await self.update_all() + async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + + async def update_all(self, now=None) -> None: + """Update all Freebox platforms.""" + await self.update_devices() + await self.update_sensors() + + async def update_devices(self) -> None: + """Update Freebox devices.""" + if self._api is None: + return + + fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + for fbx_device in fbx_devices: + device_name = fbx_device["primary_name"] + device_mac = fbx_device["l2ident"]["id"] + + if self._devices.get(device_mac) is not None: + # Seen device -> updating + _LOGGER.debug("Updating Freebox device: %s", device_name) + self._devices[device_mac].update(fbx_device) + else: + # New device, should be unique + _LOGGER.debug( + "Adding Freebox device: %s [MAC: %s]", device_name, device_mac, + ) + self._devices[device_mac] = FreeboxDevice(fbx_device) + self._devices[device_mac].update(fbx_device) + + dispatcher_send(self.hass, TRACKER_UPDATE) + + async def update_sensors(self) -> None: + """Update Freebox sensors.""" + if self._api is None: + return + + # System sensors + syst_datas: Dict[str, any] = await self._api.system.get_config() + temp_datas = {item["id"]: item for item in syst_datas["sensors"]} + + for sensor_key, sensor_attrs in TEMP_SENSORS.items(): + if temp_datas.get(sensor_key) is None: + continue + + if self._temp_sensors.get(sensor_key) is not None: + # Seen sensor -> updating + _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) + self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + else: + # New sensor, should be unique + _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) + self._temp_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + + # Connection sensors + conn_datas: Dict[str, any] = await self._api.connection.get_status() + for sensor_key, sensor_attrs in CONN_SENSORS.items(): + if self._conn_sensors.get(sensor_key) is not None: + # Seen sensor -> updating + _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) + self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + else: + # New sensor, should be unique + _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) + self._conn_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + + dispatcher_send(self.hass, SENSOR_UPDATE) + + async def reboot(self) -> None: + """Reboot the Freebox.""" + await self._api.system.reboot() + + async def close(self) -> None: + """Close the connection.""" + await self._api.close() + self._api = None + + @property + def name(self) -> str: + """Return the router name.""" + return self._name + + @property + def mac(self) -> str: + """Return the router MAC address.""" + return self._mac + + @property + def firmware_version(self) -> str: + """Return the router sofware version.""" + return self._sw_v + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "identifiers": {(DOMAIN, self.mac)}, + "name": self.name, + "manufacturer": "Freebox SAS", + "sw_version": self.firmware_version, + } + + @property + def devices(self) -> Dict[str, any]: + """Return all devices.""" + return self._devices + + @property + def sensors(self) -> Dict[str, any]: + """Return all sensors.""" + return {**self._temp_sensors, **self._conn_sensors} + + @property + def wifi(self) -> Wifi: + """Return the wifi.""" + return self._api.wifi + + +class FreeboxDevice: + """Representation of a Freebox device.""" + + def __init__(self, device: Dict[str, any]): + """Initialize a Freebox device.""" + self._name = device["primary_name"] + self._mac = device["l2ident"]["id"] + self._manufacturer = device["vendor_name"] + self._icon = icon_for_freebox_device(device) + self._reachable = device["reachable"] + + self._attrs = { + "last_time_reachable": datetime.fromtimestamp(device["last_time_reachable"]) + } + + def update(self, device: Dict[str, any]) -> None: + """Update the Freebox device.""" + self._reachable = device["reachable"] + self._attrs["last_time_reachable"] = datetime.fromtimestamp( + device["last_time_reachable"] + ) + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def mac(self) -> str: + """Return the MAC address.""" + return self._mac + + @property + def manufacturer(self) -> str: + """Return the manufacturer.""" + return self._manufacturer + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def reachable(self) -> str: + """Return the reachability (present or not on the network).""" + return self._reachable + + @property + def state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs + + +class FreeboxSensor: + """Representation of a Freebox sensor.""" + + def __init__(self, sensor: Dict[str, any]): + """Initialize a Freebox sensor.""" + self._state = None + self._name = sensor[SENSOR_NAME] + self._unit = sensor[SENSOR_UNIT] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + + def update(self, state: any) -> None: + """Update the Freebox sensor.""" + if self._unit == "KB/s": + self._state = round(state / 1000, 2) + else: + self._state = state + + @property + def state(self) -> str: + """Return the state.""" + return self._state + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def unit(self) -> str: + """Return the unit.""" + return self._unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class + + +def icon_for_freebox_device(device) -> str: + """Return a host icon from his type.""" + switcher = { + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", + } + + return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index b4d4f25420203..aa51e8c6a9df8 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -7,8 +7,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from . import FreeboxRouter, FreeboxSensor from .const import DOMAIN, SENSOR_UPDATE +from .router import FreeboxRouter, FreeboxSensor _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 17e57b7efa618..0895f4a897d14 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import FreeboxRouter from .const import DOMAIN +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) From c8a793075d8eba621a81c06b6da2d519d3df362b Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 10 Jan 2020 13:14:45 +0100 Subject: [PATCH 13/45] Make temperature sensors auto-discovered --- homeassistant/components/freebox/const.py | 30 ++++------------------ homeassistant/components/freebox/router.py | 17 ++++++++---- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 7fe364d538ea3..aa518f1646ce4 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -55,29 +55,9 @@ }, } -TEMP_SENSORS = { - "temp_hdd": { - SENSOR_NAME: "Freebox HDD temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_ICON: "mdi:thermometer", - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - "temp_sw": { - SENSOR_NAME: "Freebox switch temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_ICON: "mdi:thermometer", - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - "temp_cpum": { - SENSOR_NAME: "Freebox CPU M temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_ICON: "mdi:thermometer", - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - "temp_cpub": { - SENSOR_NAME: "Freebox CPU B temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_ICON: "mdi:thermometer", - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, +TEMP_SENSOR_TEMPLATE = { + SENSOR_NAME: None, + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 7b24e27036527..7e34f70401300 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -23,7 +23,7 @@ SENSOR_NAME, SENSOR_UNIT, SENSOR_UPDATE, - TEMP_SENSORS, + TEMP_SENSOR_TEMPLATE, TRACKER_UPDATE, ) @@ -108,10 +108,9 @@ async def update_sensors(self) -> None: # System sensors syst_datas: Dict[str, any] = await self._api.system.get_config() temp_datas = {item["id"]: item for item in syst_datas["sensors"]} + # According to the doc it is only temperature sensors in celsius degree, name and id of the sensors may vary under Freebox devices - for sensor_key, sensor_attrs in TEMP_SENSORS.items(): - if temp_datas.get(sensor_key) is None: - continue + for sensor_key, sensor_attrs in temp_datas.items(): if self._temp_sensors.get(sensor_key) is not None: # Seen sensor -> updating @@ -120,7 +119,15 @@ async def update_sensors(self) -> None: else: # New sensor, should be unique _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._temp_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._temp_sensors[sensor_key] = FreeboxSensor( + { + **TEMP_SENSOR_TEMPLATE, + **{ + SENSOR_NAME: f"Freebox {sensor_attrs['name']}", + "value": sensor_attrs["value"], + }, + } + ) self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) # Connection sensors From c3a8dd6bfd3c8ff13e39b70c22bd504e28ee5f0f Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 10 Jan 2020 13:28:49 +0100 Subject: [PATCH 14/45] Use device activity instead of reachablility for device_tracker --- .../components/freebox/device_tracker.py | 4 +-- homeassistant/components/freebox/router.py | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 070d859ec7eb1..9439e9bd67dbb 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -50,14 +50,14 @@ def name(self) -> str: @property def latitude(self): """Return latitude value of the device.""" - if self._device.reachable: + if self._device.active: return self.hass.config.latitude return None @property def longitude(self): """Return longitude value of the device.""" - if self._device.reachable: + if self._device.active: return self.hass.config.longitude return None diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 7e34f70401300..ab660f55d0466 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -205,18 +205,28 @@ def __init__(self, device: Dict[str, any]): self._mac = device["l2ident"]["id"] self._manufacturer = device["vendor_name"] self._icon = icon_for_freebox_device(device) - self._reachable = device["reachable"] + self._active = device["active"] + self._reachable = device["reachable"] self._attrs = { - "last_time_reachable": datetime.fromtimestamp(device["last_time_reachable"]) + "reachable": self._reachable, + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), } def update(self, device: Dict[str, any]) -> None: """Update the Freebox device.""" + self._active = device["active"] self._reachable = device["reachable"] - self._attrs["last_time_reachable"] = datetime.fromtimestamp( - device["last_time_reachable"] - ) + self._attrs = { + "reachable": self._reachable, + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), + } @property def name(self) -> str: @@ -239,8 +249,13 @@ def icon(self) -> str: return self._icon @property - def reachable(self) -> str: - """Return the reachability (present or not on the network).""" + def active(self) -> bool: + """Return true if the host sends traffic to the Freebox.""" + return self._active + + @property + def reachable(self) -> bool: + """Return true if the host can receive traffic from the Freebox.""" return self._reachable @property From d970b7adfc16ba5aefe03b53519ca11263cfd176 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 9 Jan 2020 14:02:47 +0100 Subject: [PATCH 15/45] Add link step to config flow --- .../components/freebox/.translations/en.json | 9 ++- .../components/freebox/config_flow.py | 58 +++++++++++++++---- homeassistant/components/freebox/const.py | 2 +- homeassistant/components/freebox/router.py | 3 +- homeassistant/components/freebox/strings.json | 9 ++- tests/components/freebox/test_config_flow.py | 58 ++++++++++++++++--- 6 files changed, 116 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/freebox/.translations/en.json b/homeassistant/components/freebox/.translations/en.json index 17d1f7dad24c2..0294be87abdd2 100644 --- a/homeassistant/components/freebox/.translations/en.json +++ b/homeassistant/components/freebox/.translations/en.json @@ -4,9 +4,16 @@ "already_configured": "Host already configured" }, "error": { - "already_configured": "Host already configured" + "already_configured": "Host already configured", + "connection_failed": "Failed to connect, please try again", + "register_failed": "Failed to register, please try again", + "unknown": "Unknown error: please retry later" }, "step": { + "link": { + "description": "Click \"Submit\", then touch the right arrow on the router to register Freebox with Home Assistant.\n\n![Location of button on the router](/static/images/config_freebox.png)", + "title": "Link Freebox router" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index b625a3dc9d5a4..0c188b18540b4 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -2,7 +2,7 @@ import logging from aiofreepybox import Freepybox -from aiofreepybox.exceptions import HttpRequestError +from aiofreepybox.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol from homeassistant import config_entries @@ -21,6 +21,8 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize Freebox config flow.""" + self._host = None + self._port = None def _configuration_exists(self, host: str) -> bool: """Return True if host exists in configuration.""" @@ -51,29 +53,63 @@ async def async_step_user(self, user_input=None): errors = {} if user_input is None: - return self._show_setup_form(user_input, None) + return self._show_setup_form(user_input, errors) - host = user_input[CONF_HOST] - port = user_input[CONF_PORT] + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] - if self._configuration_exists(host): + if self._configuration_exists(self._host): errors["base"] = "already_configured" return self._show_setup_form(user_input, errors) + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with the Freebox router. + + Given a configured host, will ask the user to press the button + to connect to the router. + """ + errors = {} + + if user_input is None: + return self.async_show_form(step_id="link") + token_file = self.hass.config.path(CONFIG_FILE) fbx = Freepybox(APP_DESC, token_file, API_VERSION) try: - await fbx.open(host, port) + # Check connection and authentification + await fbx.open(self._host, self._port) + + # Check permissions + await fbx.system.get_config() + await fbx.lan.get_hosts_list() + await self.hass.async_block_till_done() + + # Close connection + await fbx.close() + + return self.async_create_entry( + title=self._host, data={CONF_HOST: self._host, CONF_PORT: self._port}, + ) + + except AuthorizationError as error: + _LOGGER.error(error) + errors["base"] = "register_failed" + except HttpRequestError: - _LOGGER.exception("Failed to connect to Freebox") + _LOGGER.error("Error connecting to the Freebox router at %s", self._host) errors["base"] = "connection_failed" - return self._show_setup_form(user_input, errors) - return self.async_create_entry( - title=host, data={CONF_HOST: host, CONF_PORT: port}, - ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error connecting with Freebox router at %s", self._host + ) + errors["base"] = "unknown" + + return self.async_show_form(step_id="link", errors=errors) async def async_step_import(self, user_input=None): """Import a config entry.""" diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index aa518f1646ce4..8189d022abb07 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -10,7 +10,7 @@ APP_DESC = { "app_id": "hass", "app_name": "Home Assistant", - "app_version": "0.104", + "app_version": "0.105", "device_name": socket.gethostname(), } API_VERSION = "v6" diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index ab660f55d0466..b678c2a8d7364 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -151,7 +151,8 @@ async def reboot(self) -> None: async def close(self) -> None: """Close the connection.""" - await self._api.close() + if self._api is not None: + await self._api.close() self._api = None @property diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index f47f1fbc110ea..c4574fd690b9a 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -8,10 +8,17 @@ "host": "Host", "port": "Port" } + }, + "link": { + "title": "Link Freebox router", + "description": "Click \"Submit\", then touch the right arrow on the router to register Freebox with Home Assistant.\n\n![Location of button on the router](/static/images/config_freebox.png)" } }, "error":{ - "already_configured": "Host already configured" + "already_configured": "Host already configured", + "register_failed": "Failed to register, please try again", + "connection_failed": "Failed to connect, please try again", + "unknown": "Unknown error: please retry later" }, "abort":{ "already_configured": "Host already configured" diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index a1b995627d123..faf88fad27a5d 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -2,7 +2,11 @@ import asyncio from unittest.mock import MagicMock, patch -from aiofreepybox.exceptions import HttpRequestError +from aiofreepybox.exceptions import ( + AuthorizationError, + HttpRequestError, + InvalidTokenError, +) import pytest from homeassistant import data_entry_flow @@ -24,6 +28,19 @@ def mock_controller_connect(): ) as service_mock: service_mock.return_value.open = MagicMock(return_value=asyncio.Future()) service_mock.return_value.open.return_value.set_result(None) + + service_mock.return_value.system.get_config = MagicMock( + return_value=asyncio.Future() + ) + service_mock.return_value.system.get_config.return_value.set_result(None) + + service_mock.return_value.lan.get_hosts_list = MagicMock( + return_value=asyncio.Future() + ) + service_mock.return_value.lan.get_hosts_list.return_value.set_result(None) + + service_mock.return_value.close = MagicMock(return_value=asyncio.Future()) + service_mock.return_value.close.return_value.set_result(None) yield service_mock @@ -44,10 +61,8 @@ async def test_user(hass, connect): # test with all provided result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" async def test_import(hass, connect): @@ -55,6 +70,16 @@ async def test_import(hass, connect): flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + +async def test_link(hass, connect): + """Test linking.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + + result = await flow.async_step_link({}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -79,14 +104,31 @@ async def test_abort_if_already_setup(hass): assert result["errors"] == {"base": "already_configured"} -async def test_abort_on_connection_failed(hass): - """Test when we have errors during connection.""" +async def test_abort_on_link_failed(hass): + """Test when we have errors during linking the router.""" flow = init_config_flow(hass) + await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + + with patch( + "homeassistant.components.freebox.config_flow.Freepybox.open", + side_effect=AuthorizationError(), + ): + result = await flow.async_step_link({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "register_failed"} with patch( "homeassistant.components.freebox.config_flow.Freepybox.open", side_effect=HttpRequestError(), ): - result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + result = await flow.async_step_link({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_failed"} + + with patch( + "homeassistant.components.freebox.config_flow.Freepybox.open", + side_effect=InvalidTokenError(), + ): + result = await flow.async_step_link({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From 5b0261324b7bdc32d5e375a8db9ba9790bc9df16 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 4 Feb 2020 14:01:01 +0100 Subject: [PATCH 16/45] Fix comment --- homeassistant/components/freebox/config_flow.py | 2 +- homeassistant/components/freebox/router.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 0c188b18540b4..4417d6c5d0b30 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -80,7 +80,7 @@ async def async_step_link(self, user_input=None): fbx = Freepybox(APP_DESC, token_file, API_VERSION) try: - # Check connection and authentification + # Open connection and check authentication await fbx.open(self._host, self._port) # Check permissions diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index b678c2a8d7364..0059f318811f3 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -167,7 +167,7 @@ def mac(self) -> str: @property def firmware_version(self) -> str: - """Return the router sofware version.""" + """Return the router software version.""" return self._sw_v @property From b22305673c7bfc1a4d0c5689c2d232b3296449ee Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sat, 11 Jan 2020 16:10:51 +0100 Subject: [PATCH 17/45] Store token file in .storage Depending on host if list of Freebox integration on the future without breaking change --- homeassistant/components/freebox/__init__.py | 6 ++---- .../components/freebox/config_flow.py | 14 +++++++++++--- homeassistant/components/freebox/const.py | 1 - homeassistant/components/freebox/router.py | 18 +++++++++++++++--- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index ebfb68970e5d0..95a7b02113823 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.typing import HomeAssistantType -from .const import CONFIG_FILE, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) @@ -59,9 +59,7 @@ async def discovery_dispatch(service, discovery_info): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Freebox component.""" - token_file = hass.config.path(CONFIG_FILE) - - fbx = FreeboxRouter(hass, entry, token_file) + fbx = FreeboxRouter(hass, entry) try: await fbx.setup() diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 4417d6c5d0b30..970faa920d15b 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the Freebox integration.""" import logging +import os from aiofreepybox import Freepybox from aiofreepybox.exceptions import AuthorizationError, HttpRequestError @@ -7,8 +8,9 @@ from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.util import slugify -from .const import API_VERSION, APP_DESC, CONFIG_FILE, DOMAIN +from .const import API_VERSION, APP_DESC, DOMAIN, STORAGE_KEY, STORAGE_VERSION _LOGGER = logging.getLogger(__name__) @@ -75,10 +77,16 @@ async def async_step_link(self, user_input=None): if user_input is None: return self.async_show_form(step_id="link") - token_file = self.hass.config.path(CONFIG_FILE) + freebox_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - fbx = Freepybox(APP_DESC, token_file, API_VERSION) + if not os.path.exists(freebox_dir.path): + await self.hass.async_add_executor_job(os.makedirs, freebox_dir.path) + + token_file = self.hass.config.path( + f"{freebox_dir.path}/{slugify(self._host)}.conf" + ) + fbx = Freepybox(APP_DESC, token_file, API_VERSION) try: # Open connection and check authentication await fbx.open(self._host, self._port) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 8189d022abb07..65f15535e5c3e 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -14,7 +14,6 @@ "device_name": socket.gethostname(), } API_VERSION = "v6" -CONFIG_FILE = "freebox.conf" PLATFORMS = ["device_tracker", "sensor", "switch"] diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 0059f318811f3..3838aa81b34c3 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -1,6 +1,7 @@ """Represent the Freebox router and its devices and sensors.""" from datetime import datetime, timedelta import logging +import os from typing import Dict from aiofreepybox import Freepybox @@ -12,6 +13,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify from .const import ( API_VERSION, @@ -23,6 +25,8 @@ SENSOR_NAME, SENSOR_UNIT, SENSOR_UPDATE, + STORAGE_KEY, + STORAGE_VERSION, TEMP_SENSOR_TEMPLATE, TRACKER_UPDATE, ) @@ -35,13 +39,12 @@ class FreeboxRouter: """Representation of a Freebox router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, token_file: str): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): """Initialize a Freebox router.""" self.hass = hass self._entry = entry self._host = entry.data[CONF_HOST] self._port = entry.data[CONF_PORT] - self._token_file = token_file self._api: Freepybox = None self._name = None @@ -57,7 +60,16 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, token_file: str) async def setup(self) -> None: """Set up a Freebox router.""" - self._api = Freepybox(APP_DESC, self._token_file, API_VERSION) + freebox_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + if not os.path.exists(freebox_dir.path): + await self.hass.async_add_executor_job(os.makedirs, freebox_dir.path) + + token_file = self.hass.config.path( + f"{freebox_dir.path}/{slugify(self._host)}.conf" + ) + + self._api = Freepybox(APP_DESC, token_file, API_VERSION) await self._api.open(self._host, self._port) From af4722cf89431e5b883fd8a292a79bf2a3b848dd Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sat, 11 Jan 2020 17:58:16 +0100 Subject: [PATCH 18/45] Remove IP sensors + add Freebox router as a device with attrs : IPs, conection type, uptime, version & serial --- homeassistant/components/freebox/const.py | 12 ---- homeassistant/components/freebox/router.py | 74 ++++++++++++++++------ 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 65f15535e5c3e..5fac7bae571ff 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -40,18 +40,6 @@ SENSOR_ICON: "mdi:upload-network", SENSOR_DEVICE_CLASS: None, }, - "ipv4": { - SENSOR_NAME: "Freebox IPv4", - SENSOR_UNIT: None, - SENSOR_ICON: "mdi:ip", - SENSOR_DEVICE_CLASS: None, - }, - "ipv6": { - SENSOR_NAME: "Freebox IPv6", - SENSOR_UNIT: None, - SENSOR_ICON: "mdi:ip", - SENSOR_DEVICE_CLASS: None, - }, } TEMP_SENSOR_TEMPLATE = { diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 3838aa81b34c3..348c8f133e309 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -57,6 +57,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): # Sensors self._temp_sensors: Dict[str, FreeboxSensor] = {} self._conn_sensors: Dict[str, FreeboxSensor] = {} + self._attrs = {} async def setup(self) -> None: """Set up a Freebox router.""" @@ -85,8 +86,8 @@ async def setup(self) -> None: async def update_all(self, now=None) -> None: """Update all Freebox platforms.""" - await self.update_devices() await self.update_sensors() + await self.update_devices() async def update_devices(self) -> None: """Update Freebox devices.""" @@ -94,6 +95,19 @@ async def update_devices(self) -> None: return fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + + # Adds the Freebox itself + fbx_devices.append( + { + "primary_name": self.name, + "l2ident": {"id": self.mac}, + "vendor_name": self.manufacturer, + "host_type": "router", + "active": True, + "attrs": self._attrs, + } + ) + for fbx_device in fbx_devices: device_name = fbx_device["primary_name"] device_mac = fbx_device["l2ident"]["id"] @@ -123,7 +137,6 @@ async def update_sensors(self) -> None: # According to the doc it is only temperature sensors in celsius degree, name and id of the sensors may vary under Freebox devices for sensor_key, sensor_attrs in temp_datas.items(): - if self._temp_sensors.get(sensor_key) is not None: # Seen sensor -> updating _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) @@ -155,6 +168,17 @@ async def update_sensors(self) -> None: self._conn_sensors[sensor_key] = FreeboxSensor(sensor_attrs) self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + self._attrs = { + "IPv4": conn_datas.get("ipv4"), + "IPv6": conn_datas.get("ipv6"), + "connection_type": conn_datas["media"], + "uptime": datetime.fromtimestamp( + round(datetime.now().timestamp()) - syst_datas["uptime_val"] + ), + "firmware_version": self.firmware_version, + "serial": syst_datas["serial"], + } + dispatcher_send(self.hass, SENSOR_UPDATE) async def reboot(self) -> None: @@ -172,6 +196,11 @@ def name(self) -> str: """Return the router name.""" return self._name + @property + def manufacturer(self) -> str: + """Return the router manufacturer.""" + return "Freebox SAS" + @property def mac(self) -> str: """Return the router MAC address.""" @@ -189,7 +218,7 @@ def device_info(self) -> Dict[str, any]: "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, "identifiers": {(DOMAIN, self.mac)}, "name": self.name, - "manufacturer": "Freebox SAS", + "manufacturer": self.manufacturer, "sw_version": self.firmware_version, } @@ -220,26 +249,32 @@ def __init__(self, device: Dict[str, any]): self._icon = icon_for_freebox_device(device) self._active = device["active"] - self._reachable = device["reachable"] - self._attrs = { - "reachable": self._reachable, - "last_time_reachable": datetime.fromtimestamp( - device["last_time_reachable"] - ), - "last_time_activity": datetime.fromtimestamp(device["last_activity"]), - } + if device.get("attrs") is None: + self._reachable = device["reachable"] + self._attrs = { + "reachable": self._reachable, + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), + } + else: + self._attrs = device["attrs"] def update(self, device: Dict[str, any]) -> None: """Update the Freebox device.""" self._active = device["active"] - self._reachable = device["reachable"] - self._attrs = { - "reachable": self._reachable, - "last_time_reachable": datetime.fromtimestamp( - device["last_time_reachable"] - ), - "last_time_activity": datetime.fromtimestamp(device["last_activity"]), - } + if device.get("attrs") is None: + self._reachable = device["reachable"] + self._attrs = { + "reachable": self._reachable, + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), + } + else: + self._attrs = device["attrs"] @property def name(self) -> str: @@ -335,6 +370,7 @@ def icon_for_freebox_device(device) -> str: "nas": "mdi:nas", "networking_device": "mdi:network", "printer": "mdi:printer", + "router": "mdi:router-wireless", "smartphone": "mdi:cellphone", "tablet": "mdi:tablet", "television": "mdi:television", From 1941916429edb3442aa19e039c7e16d7802dcac5 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sat, 11 Jan 2020 21:04:47 +0100 Subject: [PATCH 19/45] Add sensor should_poll=False --- homeassistant/components/freebox/device_tracker.py | 10 +++++----- homeassistant/components/freebox/sensor.py | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 9439e9bd67dbb..c939bbdb59472 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -61,11 +61,6 @@ def longitude(self): return self.hass.config.longitude return None - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def source_type(self) -> str: """Return the source type of the device.""" @@ -91,6 +86,11 @@ def device_info(self) -> Dict[str, any]: "manufacturer": self._device.manufacturer, } + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index aa51e8c6a9df8..17cc36cc3e541 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -72,6 +72,11 @@ def device_info(self) -> Dict[str, any]: """Return the device information.""" return self._fbx.device_info + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( From 2b02a1f70c3cde664179bb9679cd63a35573cc1f Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 12 Feb 2020 20:43:34 +0100 Subject: [PATCH 20/45] Use config_entry.unique_id --- .../components/freebox/config_flow.py | 19 ++--- tests/components/freebox/test_config_flow.py | 76 +++++++++++-------- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 970faa920d15b..46767a389cce2 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -10,7 +10,8 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.util import slugify -from .const import API_VERSION, APP_DESC, DOMAIN, STORAGE_KEY, STORAGE_VERSION +from .const import API_VERSION, APP_DESC, STORAGE_KEY, STORAGE_VERSION +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -26,13 +27,6 @@ def __init__(self): self._host = None self._port = None - def _configuration_exists(self, host: str) -> bool: - """Return True if host exists in configuration.""" - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == host: - return True - return False - def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" @@ -60,9 +54,9 @@ async def async_step_user(self, user_input=None): self._host = user_input[CONF_HOST] self._port = user_input[CONF_PORT] - if self._configuration_exists(self._host): - errors["base"] = "already_configured" - return self._show_setup_form(user_input, errors) + # Check if already configured + await self.async_set_unique_id(self._host) + self._abort_if_unique_id_configured() return await self.async_step_link() @@ -121,7 +115,4 @@ async def async_step_link(self, user_input=None): async def async_step_import(self, user_input=None): """Import a config entry.""" - if self._configuration_exists(user_input[CONF_HOST]): - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index faf88fad27a5d..898a5e99fca4a 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -10,8 +10,8 @@ import pytest from homeassistant import data_entry_flow -from homeassistant.components.freebox import config_flow from homeassistant.components.freebox.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT from tests.common import MockConfigEntry @@ -44,43 +44,47 @@ def mock_controller_connect(): yield service_mock -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.FreeboxFlowHandler() - flow.hass = hass - return flow - - async def test_user(hass, connect): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" # test with all provided - result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" async def test_import(hass, connect): """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" async def test_link(hass, connect): """Test linking.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == HOST assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -88,32 +92,42 @@ async def test_link(hass, connect): async def test_abort_if_already_setup(hass): """Test we abort if component is already setup.""" - flow = init_config_flow(hass) - MockConfigEntry(domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}).add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}, unique_id=HOST + ).add_to_hass(hass) # Should fail, same HOST (import) - result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" # Should fail, same HOST (flow) - result = await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "already_configured"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_abort_on_link_failed(hass): """Test when we have errors during linking the router.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_HOST: HOST, CONF_PORT: PORT}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) with patch( "homeassistant.components.freebox.config_flow.Freepybox.open", side_effect=AuthorizationError(), ): - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "register_failed"} @@ -121,7 +135,7 @@ async def test_abort_on_link_failed(hass): "homeassistant.components.freebox.config_flow.Freepybox.open", side_effect=HttpRequestError(), ): - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_failed"} @@ -129,6 +143,6 @@ async def test_abort_on_link_failed(hass): "homeassistant.components.freebox.config_flow.Freepybox.open", side_effect=InvalidTokenError(), ): - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} From 0223084aab727317333781e203e15a4d99f23436 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 20 Jan 2020 20:14:21 +0100 Subject: [PATCH 21/45] Test typing --- tests/components/freebox/test_config_flow.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 898a5e99fca4a..2cd5a306ff64a 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -13,6 +13,7 @@ from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ def mock_controller_connect(): yield service_mock -async def test_user(hass, connect): +async def test_user(hass: HomeAssistantType, connect: MagicMock): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None, @@ -53,7 +54,6 @@ async def test_user(hass, connect): assert result["step_id"] == "user" # test with all provided - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -63,7 +63,7 @@ async def test_user(hass, connect): assert result["step_id"] == "link" -async def test_import(hass, connect): +async def test_import(hass: HomeAssistantType, connect: MagicMock): """Test import step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -74,7 +74,7 @@ async def test_import(hass, connect): assert result["step_id"] == "link" -async def test_link(hass, connect): +async def test_link(hass: HomeAssistantType, connect: MagicMock): """Test linking.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -90,7 +90,7 @@ async def test_link(hass, connect): assert result["data"][CONF_PORT] == PORT -async def test_abort_if_already_setup(hass): +async def test_abort_if_already_setup(hass: HomeAssistantType): """Test we abort if component is already setup.""" MockConfigEntry( domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}, unique_id=HOST @@ -115,7 +115,7 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_configured" -async def test_abort_on_link_failed(hass): +async def test_abort_on_link_failed(hass: HomeAssistantType): """Test when we have errors during linking the router.""" result = await hass.config_entries.flow.async_init( DOMAIN, From 8a420cf6bec45f90a076a6097850b00f9dc4e1e0 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 27 Jan 2020 21:18:00 +0100 Subject: [PATCH 22/45] Handle devices with no name --- homeassistant/components/freebox/const.py | 2 ++ homeassistant/components/freebox/router.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 5fac7bae571ff..7754bbde38c54 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -17,6 +17,8 @@ PLATFORMS = ["device_tracker", "sensor", "switch"] +DEFAULT_DEVICE_NAME = "Unknown device" + # to store the cookie STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 348c8f133e309..92e173f14f602 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -19,6 +19,7 @@ API_VERSION, APP_DESC, CONN_SENSORS, + DEFAULT_DEVICE_NAME, DOMAIN, SENSOR_DEVICE_CLASS, SENSOR_ICON, @@ -109,7 +110,7 @@ async def update_devices(self) -> None: ) for fbx_device in fbx_devices: - device_name = fbx_device["primary_name"] + device_name = fbx_device["primary_name"].strip() or DEFAULT_DEVICE_NAME device_mac = fbx_device["l2ident"]["id"] if self._devices.get(device_mac) is not None: @@ -243,7 +244,7 @@ class FreeboxDevice: def __init__(self, device: Dict[str, any]): """Initialize a Freebox device.""" - self._name = device["primary_name"] + self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME self._mac = device["l2ident"]["id"] self._manufacturer = device["vendor_name"] self._icon = icon_for_freebox_device(device) From 1dd3f026c115a9411059f7b19da5338a245d149e Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 4 Feb 2020 13:54:49 +0100 Subject: [PATCH 23/45] None is the default for data --- tests/components/freebox/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 2cd5a306ff64a..ac24011199338 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -48,7 +48,7 @@ def mock_controller_connect(): async def test_user(hass: HomeAssistantType, connect: MagicMock): """Test user config.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=None, + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" From 8c56ac1b2ff97c6dac2f6dd9b5c0931cad6c7929 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 12 Feb 2020 21:23:12 +0100 Subject: [PATCH 24/45] Add async_unload_entry with asyncio --- homeassistant/components/freebox/__init__.py | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 95a7b02113823..fa869672d8f9b 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +import asyncio import logging from aiofreepybox.exceptions import HttpRequestError @@ -91,10 +92,18 @@ async def close_fbx(event): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): - """Unload Freebox component.""" - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + fbx = hass.data[DOMAIN] + await fbx.close() + hass.data.pop(DOMAIN) - fbx = hass.data[DOMAIN] - await fbx.close() - return True + return unload_ok From b36d8ddb2f108323b18c1a4e0480c049ad633ef0 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Fri, 14 Feb 2020 01:00:05 +0100 Subject: [PATCH 25/45] Add and use bunch of data size and rate related constants (#31781) --- homeassistant/components/freebox/const.py | 12 ++++++++---- homeassistant/components/freebox/router.py | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 7754bbde38c54..eb71c61fee04a 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -1,7 +1,11 @@ """Freebox component constants.""" import socket -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ( + DATA_RATE_KILOBYTES_PER_SECOND, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) DOMAIN = "freebox" TRACKER_UPDATE = f"{DOMAIN}_tracker_update" @@ -10,7 +14,7 @@ APP_DESC = { "app_id": "hass", "app_name": "Home Assistant", - "app_version": "0.105", + "app_version": "0.106", "device_name": socket.gethostname(), } API_VERSION = "v6" @@ -32,13 +36,13 @@ CONN_SENSORS = { "rate_down": { SENSOR_NAME: "Freebox download speed", - SENSOR_UNIT: "KB/s", + SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, SENSOR_ICON: "mdi:download-network", SENSOR_DEVICE_CLASS: None, }, "rate_up": { SENSOR_NAME: "Freebox upload speed", - SENSOR_UNIT: "KB/s", + SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, SENSOR_ICON: "mdi:upload-network", SENSOR_DEVICE_CLASS: None, }, diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 92e173f14f602..2b423a865cbb8 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -8,7 +8,7 @@ from aiofreepybox.api.wifi import Wifi from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, DATA_RATE_KILOBYTES_PER_SECOND from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -326,7 +326,7 @@ def __init__(self, sensor: Dict[str, any]): def update(self, state: any) -> None: """Update the Freebox sensor.""" - if self._unit == "KB/s": + if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: self._state = round(state / 1000, 2) else: self._state = state From 746105c8df814851faef869a2326a9d1d4921e58 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 19 Feb 2020 13:11:27 +0100 Subject: [PATCH 26/45] Review --- homeassistant/components/freebox/__init__.py | 4 +- .../components/freebox/config_flow.py | 4 ++ homeassistant/components/freebox/const.py | 4 +- homeassistant/components/freebox/router.py | 69 +++++++++---------- tests/components/freebox/test_config_flow.py | 13 +++- 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index fa869672d8f9b..ec3b28febcb7e 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.typing import HomeAssistantType @@ -39,7 +39,7 @@ async def discovery_dispatch(service, discovery_info): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_IMPORT}, + context={"source": SOURCE_DISCOVERY}, data={CONF_HOST: host, CONF_PORT: port}, ) ) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 46767a389cce2..963bae61f8df3 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -116,3 +116,7 @@ async def async_step_link(self, user_input=None): async def async_step_import(self, user_input=None): """Import a config entry.""" return await self.async_step_user(user_input) + + async def async_step_discovery(self, user_input=None): + """Initialize step from discovery.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index eb71c61fee04a..226465d3caa89 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -33,7 +33,7 @@ SENSOR_ICON = "icon" SENSOR_DEVICE_CLASS = "device_class" -CONN_SENSORS = { +CONNECTION_SENSORS = { "rate_down": { SENSOR_NAME: "Freebox download speed", SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, @@ -48,7 +48,7 @@ }, } -TEMP_SENSOR_TEMPLATE = { +TEMPERATURE_SENSOR_TEMPLATE = { SENSOR_NAME: None, SENSOR_UNIT: TEMP_CELSIUS, SENSOR_ICON: "mdi:thermometer", diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 2b423a865cbb8..8f2e10a940725 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -18,7 +18,7 @@ from .const import ( API_VERSION, APP_DESC, - CONN_SENSORS, + CONNECTION_SENSORS, DEFAULT_DEVICE_NAME, DOMAIN, SENSOR_DEVICE_CLASS, @@ -28,7 +28,7 @@ SENSOR_UPDATE, STORAGE_KEY, STORAGE_VERSION, - TEMP_SENSOR_TEMPLATE, + TEMPERATURE_SENSOR_TEMPLATE, TRACKER_UPDATE, ) @@ -56,8 +56,8 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self._devices: Dict[str, FreeboxDevice] = {} # Sensors - self._temp_sensors: Dict[str, FreeboxSensor] = {} - self._conn_sensors: Dict[str, FreeboxSensor] = {} + self._temperature_sensors: Dict[str, FreeboxSensor] = {} + self._connection_sensors: Dict[str, FreeboxSensor] = {} self._attrs = {} async def setup(self) -> None: @@ -123,7 +123,6 @@ async def update_devices(self) -> None: "Adding Freebox device: %s [MAC: %s]", device_name, device_mac, ) self._devices[device_mac] = FreeboxDevice(fbx_device) - self._devices[device_mac].update(fbx_device) dispatcher_send(self.hass, TRACKER_UPDATE) @@ -134,45 +133,54 @@ async def update_sensors(self) -> None: # System sensors syst_datas: Dict[str, any] = await self._api.system.get_config() - temp_datas = {item["id"]: item for item in syst_datas["sensors"]} - # According to the doc it is only temperature sensors in celsius degree, name and id of the sensors may vary under Freebox devices + temperature_datas = {item["id"]: item for item in syst_datas["sensors"]} + # According to the doc it is only temperature sensors in celsius degree. + # Name and id of the sensors may vary under Freebox devices. - for sensor_key, sensor_attrs in temp_datas.items(): - if self._temp_sensors.get(sensor_key) is not None: + for sensor_key, sensor_attrs in temperature_datas.items(): + if self._temperature_sensors.get(sensor_key) is not None: # Seen sensor -> updating _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + self._temperature_sensors[sensor_key].update( + temperature_datas[sensor_key]["value"] + ) else: # New sensor, should be unique _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._temp_sensors[sensor_key] = FreeboxSensor( + self._temperature_sensors[sensor_key] = FreeboxSensor( { - **TEMP_SENSOR_TEMPLATE, + **TEMPERATURE_SENSOR_TEMPLATE, **{ SENSOR_NAME: f"Freebox {sensor_attrs['name']}", "value": sensor_attrs["value"], }, } ) - self._temp_sensors[sensor_key].update(temp_datas[sensor_key]["value"]) + self._temperature_sensors[sensor_key].update( + temperature_datas[sensor_key]["value"] + ) # Connection sensors - conn_datas: Dict[str, any] = await self._api.connection.get_status() - for sensor_key, sensor_attrs in CONN_SENSORS.items(): - if self._conn_sensors.get(sensor_key) is not None: + connection_datas: Dict[str, any] = await self._api.connection.get_status() + for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): + if self._connection_sensors.get(sensor_key) is not None: # Seen sensor -> updating _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + self._connection_sensors[sensor_key].update( + connection_datas[sensor_key] + ) else: # New sensor, should be unique _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._conn_sensors[sensor_key] = FreeboxSensor(sensor_attrs) - self._conn_sensors[sensor_key].update(conn_datas[sensor_key]) + self._connection_sensors[sensor_key] = FreeboxSensor(sensor_attrs) + self._connection_sensors[sensor_key].update( + connection_datas[sensor_key] + ) self._attrs = { - "IPv4": conn_datas.get("ipv4"), - "IPv6": conn_datas.get("ipv6"), - "connection_type": conn_datas["media"], + "IPv4": connection_datas.get("ipv4"), + "IPv6": connection_datas.get("ipv6"), + "connection_type": connection_datas["media"], "uptime": datetime.fromtimestamp( round(datetime.now().timestamp()) - syst_datas["uptime_val"] ), @@ -231,7 +239,7 @@ def devices(self) -> Dict[str, any]: @property def sensors(self) -> Dict[str, any]: """Return all sensors.""" - return {**self._temp_sensors, **self._conn_sensors} + return {**self._temperature_sensors, **self._connection_sensors} @property def wifi(self) -> Wifi: @@ -249,23 +257,13 @@ def __init__(self, device: Dict[str, any]): self._manufacturer = device["vendor_name"] self._icon = icon_for_freebox_device(device) - self._active = device["active"] - if device.get("attrs") is None: - self._reachable = device["reachable"] - self._attrs = { - "reachable": self._reachable, - "last_time_reachable": datetime.fromtimestamp( - device["last_time_reachable"] - ), - "last_time_activity": datetime.fromtimestamp(device["last_activity"]), - } - else: - self._attrs = device["attrs"] + self.update(device) def update(self, device: Dict[str, any]) -> None: """Update the Freebox device.""" self._active = device["active"] if device.get("attrs") is None: + # device self._reachable = device["reachable"] self._attrs = { "reachable": self._reachable, @@ -275,6 +273,7 @@ def update(self, device: Dict[str, any]) -> None: "last_time_activity": datetime.fromtimestamp(device["last_activity"]), } else: + # router self._attrs = device["attrs"] @property diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index ac24011199338..6a4c4b8224b37 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant import data_entry_flow from homeassistant.components.freebox.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.typing import HomeAssistantType @@ -74,6 +74,17 @@ async def test_import(hass: HomeAssistantType, connect: MagicMock): assert result["step_id"] == "link" +async def test_discovery(hass: HomeAssistantType, connect: MagicMock): + """Test discovery step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + async def test_link(hass: HomeAssistantType, connect: MagicMock): """Test linking.""" result = await hass.config_entries.flow.async_init( From b48cf9dfd14a4aab2bc4b0a1a846038836e5925e Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 19 Feb 2020 13:15:42 +0100 Subject: [PATCH 27/45] Remove useless "already_configured" error string --- homeassistant/components/freebox/.translations/en.json | 1 - homeassistant/components/freebox/strings.json | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/freebox/.translations/en.json b/homeassistant/components/freebox/.translations/en.json index 0294be87abdd2..75d925e2f7ac0 100644 --- a/homeassistant/components/freebox/.translations/en.json +++ b/homeassistant/components/freebox/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "Host already configured" }, "error": { - "already_configured": "Host already configured", "connection_failed": "Failed to connect, please try again", "register_failed": "Failed to register, please try again", "unknown": "Unknown error: please retry later" diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index c4574fd690b9a..867a497d02fbf 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -15,7 +15,6 @@ } }, "error":{ - "already_configured": "Host already configured", "register_failed": "Failed to register, please try again", "connection_failed": "Failed to connect, please try again", "unknown": "Unknown error: please retry later" From 90716ed0eb647c8d802b9cae237d4a09232295a2 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 20 Feb 2020 13:16:21 +0100 Subject: [PATCH 28/45] Review : merge 2 device & 2 sensor classes --- .../components/freebox/device_tracker.py | 118 ++++++++++--- homeassistant/components/freebox/router.py | 156 ++---------------- homeassistant/components/freebox/sensor.py | 71 +++++--- 3 files changed, 153 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index c939bbdb59472..d06d5d2b367c5 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,4 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +from datetime import datetime import logging from typing import Dict @@ -9,8 +10,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, TRACKER_UPDATE -from .router import FreeboxDevice +from .const import DEFAULT_DEVICE_NAME, DOMAIN, TRACKER_UPDATE _LOGGER = logging.getLogger(__name__) @@ -24,66 +24,113 @@ async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up the device_tracker.""" - for device in hass.data[DOMAIN].devices.values(): - _LOGGER.debug("Adding device_tracker for %s", device.name) - async_add_entities([FreeboxTrackerEntity(device)]) + fbx = hass.data[DOMAIN] + entities = [] -class FreeboxTrackerEntity(TrackerEntity): - """Represent a tracked device.""" + for device in fbx.devices.values(): + entities.append(device) - def __init__(self, device: FreeboxDevice): - """Set up the Freebox tracker entity.""" - self._device = device + async_add_entities(entities) + + +class FreeboxDevice(TrackerEntity): + """Representation of a Freebox device.""" + + def __init__(self, device: Dict[str, any]): + """Initialize a Freebox device.""" + self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME + self._mac = device["l2ident"]["id"] + self._manufacturer = device["vendor_name"] + self._icon = icon_for_freebox_device(device) self._unsub_dispatcher = None + self.update_state(device) + + def update_state(self, device: Dict[str, any]) -> None: + """Update the Freebox device.""" + self._active = device["active"] + if device.get("attrs") is None: + # device + self._reachable = device["reachable"] + self._attrs = { + "reachable": self._reachable, + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), + } + else: + # router + self._attrs = device["attrs"] + @property def unique_id(self) -> str: """Return a unique ID.""" - return self._device.mac + return self.mac @property def name(self) -> str: - """Return the name of the device.""" - return self._device.name + """Return the name.""" + return self._name @property def latitude(self): - """Return latitude value of the device.""" - if self._device.active: + """Return the latitude.""" + if self.active: return self.hass.config.latitude return None @property def longitude(self): - """Return longitude value of the device.""" - if self._device.active: + """Return the longitude.""" + if self.active: return self.hass.config.longitude return None @property def source_type(self) -> str: - """Return the source type of the device.""" + """Return the source type.""" return SOURCE_TYPE_ROUTER + @property + def mac(self) -> str: + """Return the MAC address.""" + return self._mac + + @property + def manufacturer(self) -> str: + """Return the manufacturer.""" + return self._manufacturer + @property def icon(self) -> str: """Return the icon.""" - return self._device.icon + return self._icon + + @property + def active(self) -> bool: + """Return true if the host sends traffic to the Freebox.""" + return self._active + + @property + def reachable(self) -> bool: + """Return true if the host can receive traffic from the Freebox.""" + return self._reachable @property def device_state_attributes(self) -> Dict[str, any]: - """Return the device state attributes.""" - return self._device.state_attributes + """Return the attributes.""" + return self._attrs @property def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, - "manufacturer": self._device.manufacturer, + "manufacturer": self.manufacturer, } @property @@ -100,3 +147,28 @@ async def async_added_to_hass(self): 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.""" + switcher = { + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "router": "mdi:router-wireless", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", + } + + return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 8f2e10a940725..ffea224127e43 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -8,7 +8,7 @@ from aiofreepybox.api.wifi import Wifi from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, DATA_RATE_KILOBYTES_PER_SECOND +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -21,16 +21,15 @@ CONNECTION_SENSORS, DEFAULT_DEVICE_NAME, DOMAIN, - SENSOR_DEVICE_CLASS, - SENSOR_ICON, SENSOR_NAME, - SENSOR_UNIT, SENSOR_UPDATE, STORAGE_KEY, STORAGE_VERSION, TEMPERATURE_SENSOR_TEMPLATE, TRACKER_UPDATE, ) +from .device_tracker import FreeboxDevice +from .sensor import FreeboxSensor _LOGGER = logging.getLogger(__name__) @@ -116,7 +115,7 @@ async def update_devices(self) -> None: if self._devices.get(device_mac) is not None: # Seen device -> updating _LOGGER.debug("Updating Freebox device: %s", device_name) - self._devices[device_mac].update(fbx_device) + self._devices[device_mac].update_state(fbx_device) else: # New device, should be unique _LOGGER.debug( @@ -141,22 +140,23 @@ async def update_sensors(self) -> None: if self._temperature_sensors.get(sensor_key) is not None: # Seen sensor -> updating _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._temperature_sensors[sensor_key].update( + self._temperature_sensors[sensor_key].update_state( temperature_datas[sensor_key]["value"] ) else: # New sensor, should be unique _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) self._temperature_sensors[sensor_key] = FreeboxSensor( + self, { **TEMPERATURE_SENSOR_TEMPLATE, **{ SENSOR_NAME: f"Freebox {sensor_attrs['name']}", "value": sensor_attrs["value"], }, - } + }, ) - self._temperature_sensors[sensor_key].update( + self._temperature_sensors[sensor_key].update_state( temperature_datas[sensor_key]["value"] ) @@ -166,14 +166,14 @@ async def update_sensors(self) -> None: if self._connection_sensors.get(sensor_key) is not None: # Seen sensor -> updating _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._connection_sensors[sensor_key].update( + self._connection_sensors[sensor_key].update_state( connection_datas[sensor_key] ) else: # New sensor, should be unique _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._connection_sensors[sensor_key] = FreeboxSensor(sensor_attrs) - self._connection_sensors[sensor_key].update( + self._connection_sensors[sensor_key] = FreeboxSensor(self, sensor_attrs) + self._connection_sensors[sensor_key].update_state( connection_datas[sensor_key] ) @@ -245,137 +245,3 @@ def sensors(self) -> Dict[str, any]: def wifi(self) -> Wifi: """Return the wifi.""" return self._api.wifi - - -class FreeboxDevice: - """Representation of a Freebox device.""" - - def __init__(self, device: Dict[str, any]): - """Initialize a Freebox device.""" - self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME - self._mac = device["l2ident"]["id"] - self._manufacturer = device["vendor_name"] - self._icon = icon_for_freebox_device(device) - - self.update(device) - - def update(self, device: Dict[str, any]) -> None: - """Update the Freebox device.""" - self._active = device["active"] - if device.get("attrs") is None: - # device - self._reachable = device["reachable"] - self._attrs = { - "reachable": self._reachable, - "last_time_reachable": datetime.fromtimestamp( - device["last_time_reachable"] - ), - "last_time_activity": datetime.fromtimestamp(device["last_activity"]), - } - else: - # router - self._attrs = device["attrs"] - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def mac(self) -> str: - """Return the MAC address.""" - return self._mac - - @property - def manufacturer(self) -> str: - """Return the manufacturer.""" - return self._manufacturer - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def active(self) -> bool: - """Return true if the host sends traffic to the Freebox.""" - return self._active - - @property - def reachable(self) -> bool: - """Return true if the host can receive traffic from the Freebox.""" - return self._reachable - - @property - def state_attributes(self) -> Dict[str, any]: - """Return the attributes.""" - return self._attrs - - -class FreeboxSensor: - """Representation of a Freebox sensor.""" - - def __init__(self, sensor: Dict[str, any]): - """Initialize a Freebox sensor.""" - self._state = None - self._name = sensor[SENSOR_NAME] - self._unit = sensor[SENSOR_UNIT] - self._icon = sensor[SENSOR_ICON] - self._device_class = sensor[SENSOR_DEVICE_CLASS] - - def update(self, state: any) -> None: - """Update the Freebox sensor.""" - if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: - self._state = round(state / 1000, 2) - else: - self._state = state - - @property - def state(self) -> str: - """Return the state.""" - return self._state - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def unit(self) -> str: - """Return the unit.""" - return self._unit - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def device_class(self) -> str: - """Return the device_class.""" - return self._device_class - - -def icon_for_freebox_device(device) -> str: - """Return a host icon from his type.""" - switcher = { - "freebox_delta": "mdi:television-guide", - "freebox_hd": "mdi:television-guide", - "freebox_mini": "mdi:television-guide", - "freebox_player": "mdi:television-guide", - "ip_camera": "mdi:cctv", - "ip_phone": "mdi:phone-voip", - "laptop": "mdi:laptop", - "multimedia_device": "mdi:play-network", - "nas": "mdi:nas", - "networking_device": "mdi:network", - "printer": "mdi:printer", - "router": "mdi:router-wireless", - "smartphone": "mdi:cellphone", - "tablet": "mdi:tablet", - "television": "mdi:television", - "vg_console": "mdi:gamepad-variant", - "workstation": "mdi:desktop-tower-monitor", - } - - return switcher.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 17cc36cc3e541..381ead802f72b 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -3,12 +3,19 @@ from typing import Dict from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN, SENSOR_UPDATE -from .router import FreeboxRouter, FreeboxSensor +from .const import ( + DOMAIN, + SENSOR_DEVICE_CLASS, + SENSOR_ICON, + SENSOR_NAME, + SENSOR_UNIT, + SENSOR_UPDATE, +) _LOGGER = logging.getLogger(__name__) @@ -27,50 +34,66 @@ async def async_setup_entry( entities = [] for sensor in fbx.sensors.values(): - entities.append(FbxSensor(fbx, sensor)) + entities.append(sensor) - async_add_entities(entities, True) + async_add_entities(entities) -class FbxSensor(Entity): - """Representation of a freebox sensor.""" +class FreeboxSensor(Entity): + """Representation of a Freebox sensor.""" - def __init__(self, fbx: FreeboxRouter, fbx_sensor: FreeboxSensor): - """Initialize the sensor.""" - self._fbx = fbx - self._fbx_sensor = fbx_sensor - self._unique_id = f"{self._fbx.mac} {self._fbx_sensor.name}" + def __init__(self, fbx_router, sensor: Dict[str, any]): + """Initialize a Freebox sensor.""" + self._state = None + self._router = fbx_router + self._name = sensor[SENSOR_NAME] + self._unit = sensor[SENSOR_UNIT] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + self._unique_id = f"{self._router.mac} {self._name}" self._unsub_dispatcher = None + def update_state(self, state: any) -> None: + """Update the Freebox sensor.""" + if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: + self._state = round(state / 1000, 2) + else: + self._state = state + @property def unique_id(self) -> str: """Return a unique ID.""" return self._unique_id @property - def name(self): - """Return the name of the sensor.""" - return self._fbx_sensor.name + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def state(self) -> str: + """Return the state.""" + return self._state @property - def unit_of_measurement(self): - """Return the unit of the sensor.""" - return self._fbx_sensor.unit + def unit_of_measurement(self) -> str: + """Return the unit.""" + return self._unit @property - def icon(self): - """Return the icon of the sensor.""" - return self._fbx_sensor.icon + def icon(self) -> str: + """Return the icon.""" + return self._icon @property - def state(self): - """Return the state of the sensor.""" - return self._fbx_sensor.state + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class @property def device_info(self) -> Dict[str, any]: """Return the device information.""" - return self._fbx.device_info + return self._router.device_info @property def should_poll(self) -> bool: From 1bb3e57d63c4342803cd35a95fae38edc94575ce Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 24 Feb 2020 23:38:52 +0100 Subject: [PATCH 29/45] Entities from platforms --- homeassistant/components/freebox/__init__.py | 19 +-- homeassistant/components/freebox/const.py | 23 ++- .../components/freebox/device_tracker.py | 133 ++++++++---------- homeassistant/components/freebox/router.py | 106 ++++++-------- homeassistant/components/freebox/sensor.py | 54 +++++-- homeassistant/components/freebox/switch.py | 25 ++-- 6 files changed, 180 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index ec3b28febcb7e..7df9512c5d6eb 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -60,15 +60,16 @@ async def discovery_dispatch(service, discovery_info): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Freebox component.""" - fbx = FreeboxRouter(hass, entry) + router = FreeboxRouter(hass, entry) try: - await fbx.setup() + await router.setup() except HttpRequestError: _LOGGER.exception("Failed to connect to Freebox") return False - hass.data[DOMAIN] = fbx + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.unique_id] = router for platform in PLATFORMS: hass.async_create_task( @@ -76,17 +77,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): ) # Services - async def async_freebox_reboot(call): + async def async_reboot(call): """Handle reboot service call.""" - await fbx.reboot() + await router.reboot() - hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) + hass.services.async_register(DOMAIN, "reboot", async_reboot) - async def close_fbx(event): + async def async_close_connection(event): """Close Freebox connection on HA Stop.""" - await fbx.close() + await router.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) return True diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 226465d3caa89..0612e4e76f19a 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -8,8 +8,6 @@ ) DOMAIN = "freebox" -TRACKER_UPDATE = f"{DOMAIN}_tracker_update" -SENSOR_UPDATE = f"{DOMAIN}_sensor_update" APP_DESC = { "app_id": "hass", @@ -54,3 +52,24 @@ SENSOR_ICON: "mdi:thermometer", SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, } + +# Icons +DEVICE_ICONS = { + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "router": "mdi:router-wireless", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", +} diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index d06d5d2b367c5..c61f3665c60cf 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,57 +4,81 @@ from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .const import DEFAULT_DEVICE_NAME, DOMAIN, TRACKER_UPDATE +from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -async def async_get_scanner(hass, config): - """Old way of setting up the platform.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: - """Set up the device_tracker.""" - fbx = hass.data[DOMAIN] + """Set up device tracker for Freebox component.""" + router = hass.data[DOMAIN][entry.unique_id] + tracked = {} + + @callback + def update_router(): + """Update the values of the router.""" + add_entities(router, async_add_entities, tracked) + + router.listeners.append( + async_dispatcher_connect(hass, router.signal_device_new, update_router) + ) + + update_router() + - entities = [] +@callback +def add_entities(router, async_add_entities, tracked): + """Add new tracker entities from the router.""" + new_tracked = [] - for device in fbx.devices.values(): - entities.append(device) + for mac, device in router.devices.items(): + if mac in tracked: + continue - async_add_entities(entities) + tracked[mac] = FreeboxDevice(router, device) + new_tracked.append(tracked[mac]) + if new_tracked: + async_add_entities(new_tracked) -class FreeboxDevice(TrackerEntity): + +class FreeboxDevice(ScannerEntity): """Representation of a Freebox device.""" - def __init__(self, device: Dict[str, any]): + def __init__(self, router: FreeboxRouter, device: Dict[str, any]): """Initialize a Freebox device.""" + self._router = router self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME self._mac = device["l2ident"]["id"] self._manufacturer = device["vendor_name"] self._icon = icon_for_freebox_device(device) + self._active = False + self._attrs = {} + self._unsub_dispatcher = None + _LOGGER.error("ADDED_DEVICE : %s", self.name) - self.update_state(device) + self.update() - def update_state(self, device: Dict[str, any]) -> None: + def update(self) -> None: """Update the Freebox device.""" + device = self._router.devices[self._mac] self._active = device["active"] if device.get("attrs") is None: # device - self._reachable = device["reachable"] self._attrs = { - "reachable": self._reachable, + "active": self._active, + "reachable": device["reachable"], "last_time_reachable": datetime.fromtimestamp( device["last_time_reachable"] ), @@ -64,10 +88,12 @@ def update_state(self, device: Dict[str, any]) -> None: # router self._attrs = device["attrs"] + _LOGGER.error("UPDATED_DEVICE : %s", self.name) + @property def unique_id(self) -> str: """Return a unique ID.""" - return self.mac + return self._mac @property def name(self) -> str: @@ -75,49 +101,20 @@ def name(self) -> str: return self._name @property - def latitude(self): - """Return the latitude.""" - if self.active: - return self.hass.config.latitude - return None - - @property - def longitude(self): - """Return the longitude.""" - if self.active: - return self.hass.config.longitude - return None + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._active @property def source_type(self) -> str: """Return the source type.""" return SOURCE_TYPE_ROUTER - @property - def mac(self) -> str: - """Return the MAC address.""" - return self._mac - - @property - def manufacturer(self) -> str: - """Return the manufacturer.""" - return self._manufacturer - @property def icon(self) -> str: """Return the icon.""" return self._icon - @property - def active(self) -> bool: - """Return true if the host sends traffic to the Freebox.""" - return self._active - - @property - def reachable(self) -> bool: - """Return true if the host can receive traffic from the Freebox.""" - return self._reachable - @property def device_state_attributes(self) -> Dict[str, any]: """Return the attributes.""" @@ -127,10 +124,10 @@ def device_state_attributes(self) -> Dict[str, any]: def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, - "manufacturer": self.manufacturer, + "manufacturer": self._manufacturer, } @property @@ -138,10 +135,14 @@ def should_poll(self) -> bool: """No polling needed.""" return False + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, TRACKER_UPDATE, self.async_write_ha_state + self.hass, self._router.signal_device_update, self.async_on_demand_update ) async def async_will_remove_from_hass(self): @@ -151,24 +152,4 @@ async def async_will_remove_from_hass(self): def icon_for_freebox_device(device) -> str: """Return a host icon from his type.""" - switcher = { - "freebox_delta": "mdi:television-guide", - "freebox_hd": "mdi:television-guide", - "freebox_mini": "mdi:television-guide", - "freebox_player": "mdi:television-guide", - "ip_camera": "mdi:cctv", - "ip_phone": "mdi:phone-voip", - "laptop": "mdi:laptop", - "multimedia_device": "mdi:play-network", - "nas": "mdi:nas", - "networking_device": "mdi:network", - "printer": "mdi:printer", - "router": "mdi:router-wireless", - "smartphone": "mdi:cellphone", - "tablet": "mdi:tablet", - "television": "mdi:television", - "vg_console": "mdi:gamepad-variant", - "workstation": "mdi:desktop-tower-monitor", - } - - return switcher.get(device["host_type"], "mdi:help-network") + 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 ffea224127e43..97052d36ff9f8 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -5,12 +5,13 @@ from typing import Dict from aiofreepybox import Freepybox +from aiofreepybox.api.system import System from aiofreepybox.api.wifi import Wifi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify @@ -19,17 +20,10 @@ API_VERSION, APP_DESC, CONNECTION_SENSORS, - DEFAULT_DEVICE_NAME, DOMAIN, - SENSOR_NAME, - SENSOR_UPDATE, STORAGE_KEY, STORAGE_VERSION, - TEMPERATURE_SENSOR_TEMPLATE, - TRACKER_UPDATE, ) -from .device_tracker import FreeboxDevice -from .sensor import FreeboxSensor _LOGGER = logging.getLogger(__name__) @@ -50,14 +44,16 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): self._name = None self._mac = None self._sw_v = None + self._attrs = {} # Devices - self._devices: Dict[str, FreeboxDevice] = {} + self._devices: Dict[str, any] = {} # Sensors - self._temperature_sensors: Dict[str, FreeboxSensor] = {} - self._connection_sensors: Dict[str, FreeboxSensor] = {} - self._attrs = {} + self._temperature_sensors: Dict[str, any] = {} + self._connection_sensors: Dict[str, any] = {} + + self.listeners = [] async def setup(self) -> None: """Set up a Freebox router.""" @@ -91,9 +87,8 @@ async def update_all(self, now=None) -> None: async def update_devices(self) -> None: """Update Freebox devices.""" - if self._api is None: - return - + _LOGGER.warning("ROUTER_UPDATE_DEVICES") + new_device = False fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() # Adds the Freebox itself @@ -109,26 +104,20 @@ async def update_devices(self) -> None: ) for fbx_device in fbx_devices: - device_name = fbx_device["primary_name"].strip() or DEFAULT_DEVICE_NAME device_mac = fbx_device["l2ident"]["id"] + self._devices[device_mac] = fbx_device - if self._devices.get(device_mac) is not None: - # Seen device -> updating - _LOGGER.debug("Updating Freebox device: %s", device_name) - self._devices[device_mac].update_state(fbx_device) - else: - # New device, should be unique - _LOGGER.debug( - "Adding Freebox device: %s [MAC: %s]", device_name, device_mac, - ) - self._devices[device_mac] = FreeboxDevice(fbx_device) + if self._devices.get(device_mac) is None: + new_device = True - dispatcher_send(self.hass, TRACKER_UPDATE) + async_dispatcher_send(self.hass, self.signal_device_update) + + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) async def update_sensors(self) -> None: """Update Freebox sensors.""" - if self._api is None: - return + _LOGGER.warning("ROUTER_UPDATE_SENSORS") # System sensors syst_datas: Dict[str, any] = await self._api.system.get_config() @@ -137,45 +126,12 @@ async def update_sensors(self) -> None: # Name and id of the sensors may vary under Freebox devices. for sensor_key, sensor_attrs in temperature_datas.items(): - if self._temperature_sensors.get(sensor_key) is not None: - # Seen sensor -> updating - _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._temperature_sensors[sensor_key].update_state( - temperature_datas[sensor_key]["value"] - ) - else: - # New sensor, should be unique - _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._temperature_sensors[sensor_key] = FreeboxSensor( - self, - { - **TEMPERATURE_SENSOR_TEMPLATE, - **{ - SENSOR_NAME: f"Freebox {sensor_attrs['name']}", - "value": sensor_attrs["value"], - }, - }, - ) - self._temperature_sensors[sensor_key].update_state( - temperature_datas[sensor_key]["value"] - ) + self._temperature_sensors[sensor_key] = sensor_attrs["value"] # Connection sensors connection_datas: Dict[str, any] = await self._api.connection.get_status() for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): - if self._connection_sensors.get(sensor_key) is not None: - # Seen sensor -> updating - _LOGGER.debug("Updating Freebox sensor: %s", sensor_key) - self._connection_sensors[sensor_key].update_state( - connection_datas[sensor_key] - ) - else: - # New sensor, should be unique - _LOGGER.debug("Adding Freebox sensor: %s", sensor_key) - self._connection_sensors[sensor_key] = FreeboxSensor(self, sensor_attrs) - self._connection_sensors[sensor_key].update_state( - connection_datas[sensor_key] - ) + self._connection_sensors[sensor_key] = connection_datas[sensor_key] self._attrs = { "IPv4": connection_datas.get("ipv4"), @@ -188,7 +144,7 @@ async def update_sensors(self) -> None: "serial": syst_datas["serial"], } - dispatcher_send(self.hass, SENSOR_UPDATE) + async_dispatcher_send(self.hass, self.signal_sensor_update) async def reboot(self) -> None: """Reboot the Freebox.""" @@ -241,6 +197,26 @@ def sensors(self) -> Dict[str, any]: """Return all sensors.""" return {**self._temperature_sensors, **self._connection_sensors} + @property + def signal_device_new(self) -> str: + """Event specific per Freebox entry to signal new device.""" + return f"{DOMAIN}-{self._host}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per Freebox entry to signal updates in devices.""" + return f"{DOMAIN}-{self._host}-device-update" + + @property + def signal_sensor_update(self) -> str: + """Event specific per Freebox entry to signal updates in sensors.""" + return f"{DOMAIN}-{self._host}-sensor-update" + + @property + def system(self) -> System: + """Return the system.""" + return self._api.system + @property def wifi(self) -> Wifi: """Return the wifi.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 381ead802f72b..ca102ad0d4310 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -9,32 +9,47 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( + CONNECTION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, SENSOR_ICON, SENSOR_NAME, SENSOR_UNIT, - SENSOR_UPDATE, + TEMPERATURE_SENSOR_TEMPLATE, ) +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up the platform.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up the sensors.""" - fbx = hass.data[DOMAIN] - + router = hass.data[DOMAIN][entry.unique_id] entities = [] - for sensor in fbx.sensors.values(): - entities.append(sensor) + # System sensors + syst_datas: Dict[str, any] = await router.system.get_config() + temperature_datas = {item["id"]: item for item in syst_datas["sensors"]} + # According to the doc it is only temperature sensors in celsius degree. + # Name and id of the sensors may vary under Freebox devices. + + for sensor_key, sensor_attrs in temperature_datas.items(): + entities.append( + FreeboxSensor( + router, + sensor_key, + { + **TEMPERATURE_SENSOR_TEMPLATE, + **{SENSOR_NAME: f"Freebox {sensor_attrs['name']}"}, + }, + ) + ) + + # Connection sensors + for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): + entities.append(FreeboxSensor(router, sensor_key, sensor_attrs)) async_add_entities(entities) @@ -42,23 +57,30 @@ async def async_setup_entry( class FreeboxSensor(Entity): """Representation of a Freebox sensor.""" - def __init__(self, fbx_router, sensor: Dict[str, any]): + def __init__(self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any]): """Initialize a Freebox sensor.""" self._state = None - self._router = fbx_router + self._router = router + self._sensor_type = sensor_type self._name = sensor[SENSOR_NAME] self._unit = sensor[SENSOR_UNIT] self._icon = sensor[SENSOR_ICON] self._device_class = sensor[SENSOR_DEVICE_CLASS] self._unique_id = f"{self._router.mac} {self._name}" + self._unsub_dispatcher = None + _LOGGER.error("ADDED_SENSOR : %s", self.name) - def update_state(self, state: any) -> None: + self.update() + + def update(self) -> None: """Update the Freebox sensor.""" + state = self._router.sensors[self._sensor_type] if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: self._state = round(state / 1000, 2) else: self._state = state + _LOGGER.error("UPDATED_SENSOR : %s", self.name) @property def unique_id(self) -> str: @@ -100,10 +122,14 @@ def should_poll(self) -> bool: """No polling needed.""" return False + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, SENSOR_UPDATE, self.async_write_ha_state + self.hass, self._router.signal_sensor_update, self.async_on_demand_update ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 0895f4a897d14..b9c24e3f2ae93 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -14,28 +14,24 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up the platform.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: """Set up the switch.""" - fbx = hass.data[DOMAIN] - async_add_entities([FbxWifiSwitch(fbx)], True) + router = hass.data[DOMAIN][entry.unique_id] + async_add_entities([FreeboxWifiSwitch(router)], True) -class FbxWifiSwitch(SwitchDevice): +class FreeboxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, fbx: FreeboxRouter): + def __init__(self, router: FreeboxRouter): """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None - self._fbx = fbx - self._unique_id = f"{self._fbx.mac} {self._name}" + self._router = router + self._unique_id = f"{self._router.mac} {self._name}" + _LOGGER.error("ADDED_SWITCH : %s", self.name) @property def unique_id(self) -> str: @@ -55,13 +51,13 @@ def is_on(self): @property def device_info(self) -> Dict[str, any]: """Return the device information.""" - return self._fbx.device_info + return self._router.device_info async def _async_set_state(self, enabled: bool): """Turn the switch on or off.""" wifi_config = {"enabled": enabled} try: - await self._fbx.wifi.set_global_config(wifi_config) + await self._router.wifi.set_global_config(wifi_config) except InsufficientPermissionsError: _LOGGER.warning( "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation." @@ -77,6 +73,7 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Get the state and update it.""" - datas = await self._fbx.wifi.get_global_config() + datas = await self._router.wifi.get_global_config() active = datas["enabled"] self._state = bool(active) + _LOGGER.error("UPDATED_SWITCH : %s", self.name) From fd5091f4a79568fe8378bd72ff5cfe819700d9f8 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 25 Feb 2020 08:45:50 +0100 Subject: [PATCH 30/45] Fix unload + add device after setup + clean loggers --- homeassistant/components/freebox/__init__.py | 4 ++-- homeassistant/components/freebox/device_tracker.py | 3 --- homeassistant/components/freebox/router.py | 6 ++---- homeassistant/components/freebox/sensor.py | 2 -- homeassistant/components/freebox/switch.py | 2 -- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 7df9512c5d6eb..6931475e9995c 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -103,8 +103,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): ) ) if unload_ok: - fbx = hass.data[DOMAIN] - await fbx.close() + router = hass.data[DOMAIN][entry.unique_id] + await router.close() hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index c61f3665c60cf..0e41b6e454cdb 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -66,7 +66,6 @@ def __init__(self, router: FreeboxRouter, device: Dict[str, any]): self._attrs = {} self._unsub_dispatcher = None - _LOGGER.error("ADDED_DEVICE : %s", self.name) self.update() @@ -88,8 +87,6 @@ def update(self) -> None: # router self._attrs = device["attrs"] - _LOGGER.error("UPDATED_DEVICE : %s", self.name) - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 97052d36ff9f8..4855b148dc9f9 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -87,7 +87,6 @@ async def update_all(self, now=None) -> None: async def update_devices(self) -> None: """Update Freebox devices.""" - _LOGGER.warning("ROUTER_UPDATE_DEVICES") new_device = False fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() @@ -105,11 +104,12 @@ async def update_devices(self) -> None: for fbx_device in fbx_devices: device_mac = fbx_device["l2ident"]["id"] - self._devices[device_mac] = fbx_device if self._devices.get(device_mac) is None: new_device = True + self._devices[device_mac] = fbx_device + async_dispatcher_send(self.hass, self.signal_device_update) if new_device: @@ -117,8 +117,6 @@ async def update_devices(self) -> None: async def update_sensors(self) -> None: """Update Freebox sensors.""" - _LOGGER.warning("ROUTER_UPDATE_SENSORS") - # System sensors syst_datas: Dict[str, any] = await self._api.system.get_config() temperature_datas = {item["id"]: item for item in syst_datas["sensors"]} diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index ca102ad0d4310..6b8b76acfa25e 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -69,7 +69,6 @@ def __init__(self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, an self._unique_id = f"{self._router.mac} {self._name}" self._unsub_dispatcher = None - _LOGGER.error("ADDED_SENSOR : %s", self.name) self.update() @@ -80,7 +79,6 @@ def update(self) -> None: self._state = round(state / 1000, 2) else: self._state = state - _LOGGER.error("UPDATED_SENSOR : %s", self.name) @property def unique_id(self) -> str: diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index b9c24e3f2ae93..67a442210c28a 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -31,7 +31,6 @@ def __init__(self, router: FreeboxRouter): self._state = None self._router = router self._unique_id = f"{self._router.mac} {self._name}" - _LOGGER.error("ADDED_SWITCH : %s", self.name) @property def unique_id(self) -> str: @@ -76,4 +75,3 @@ async def async_update(self): datas = await self._router.wifi.get_global_config() active = datas["enabled"] self._state = bool(active) - _LOGGER.error("UPDATED_SWITCH : %s", self.name) From 41b1aac79480a0b3535cfd6ec924b799faa37da1 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 25 Feb 2020 08:47:26 +0100 Subject: [PATCH 31/45] async_add_entities True --- homeassistant/components/freebox/device_tracker.py | 4 +--- homeassistant/components/freebox/sensor.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 0e41b6e454cdb..055f0abfa3f73 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -49,7 +49,7 @@ def add_entities(router, async_add_entities, tracked): new_tracked.append(tracked[mac]) if new_tracked: - async_add_entities(new_tracked) + async_add_entities(new_tracked, True) class FreeboxDevice(ScannerEntity): @@ -67,8 +67,6 @@ def __init__(self, router: FreeboxRouter, device: Dict[str, any]): self._unsub_dispatcher = None - self.update() - def update(self) -> None: """Update the Freebox device.""" device = self._router.devices[self._mac] diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 6b8b76acfa25e..99bb8d1ccf931 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry( for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): entities.append(FreeboxSensor(router, sensor_key, sensor_attrs)) - async_add_entities(entities) + async_add_entities(entities, True) class FreeboxSensor(Entity): @@ -70,8 +70,6 @@ def __init__(self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, an self._unsub_dispatcher = None - self.update() - def update(self) -> None: """Update the Freebox sensor.""" state = self._router.sensors[self._sensor_type] From 6c19898ddb47a34341c39f5cf89fccb4226ea6e4 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 25 Feb 2020 13:12:12 +0100 Subject: [PATCH 32/45] Review --- .../components/freebox/device_tracker.py | 6 +++--- tests/components/freebox/test_config_flow.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 055f0abfa3f73..55f548c8e9109 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -22,7 +22,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for Freebox component.""" router = hass.data[DOMAIN][entry.unique_id] - tracked = {} + tracked = [] @callback def update_router(): @@ -45,8 +45,8 @@ def add_entities(router, async_add_entities, tracked): if mac in tracked: continue - tracked[mac] = FreeboxDevice(router, device) - new_tracked.append(tracked[mac]) + new_tracked.append(FreeboxDevice(router, device)) + tracked.append(mac) if new_tracked: async_add_entities(new_tracked, True) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 6a4c4b8224b37..0d76632dd9025 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,19 +1,19 @@ """Tests for the Freebox config flow.""" import asyncio -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock from aiofreepybox.exceptions import ( AuthorizationError, HttpRequestError, InvalidTokenError, ) +from asynctest import patch import pytest from homeassistant import data_entry_flow from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry @@ -45,7 +45,7 @@ def mock_controller_connect(): yield service_mock -async def test_user(hass: HomeAssistantType, connect: MagicMock): +async def test_user(hass, connect): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -63,7 +63,7 @@ async def test_user(hass: HomeAssistantType, connect: MagicMock): assert result["step_id"] == "link" -async def test_import(hass: HomeAssistantType, connect: MagicMock): +async def test_import(hass, connect): """Test import step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -74,7 +74,7 @@ async def test_import(hass: HomeAssistantType, connect: MagicMock): assert result["step_id"] == "link" -async def test_discovery(hass: HomeAssistantType, connect: MagicMock): +async def test_discovery(hass, connect): """Test discovery step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -85,7 +85,7 @@ async def test_discovery(hass: HomeAssistantType, connect: MagicMock): assert result["step_id"] == "link" -async def test_link(hass: HomeAssistantType, connect: MagicMock): +async def test_link(hass, connect): """Test linking.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -101,7 +101,7 @@ async def test_link(hass: HomeAssistantType, connect: MagicMock): assert result["data"][CONF_PORT] == PORT -async def test_abort_if_already_setup(hass: HomeAssistantType): +async def test_abort_if_already_setup(hass): """Test we abort if component is already setup.""" MockConfigEntry( domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}, unique_id=HOST @@ -126,7 +126,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): assert result["reason"] == "already_configured" -async def test_abort_on_link_failed(hass: HomeAssistantType): +async def test_abort_on_link_failed(hass): """Test when we have errors during linking the router.""" result = await hass.config_entries.flow.async_init( DOMAIN, From 275d221aaec2cb375dc175ee34c77640480694d3 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Tue, 25 Feb 2020 14:03:07 +0100 Subject: [PATCH 33/45] Use pathlib + refactor get_api --- .../components/freebox/config_flow.py | 19 ++------------- homeassistant/components/freebox/router.py | 23 ++++++++++--------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 963bae61f8df3..3300dbc504167 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -1,17 +1,14 @@ """Config flow to configure the Freebox integration.""" import logging -import os -from aiofreepybox import Freepybox from aiofreepybox.exceptions import AuthorizationError, HttpRequestError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.util import slugify -from .const import API_VERSION, APP_DESC, STORAGE_KEY, STORAGE_VERSION from .const import DOMAIN # pylint: disable=unused-import +from .router import get_api _LOGGER = logging.getLogger(__name__) @@ -68,19 +65,7 @@ async def async_step_link(self, user_input=None): """ errors = {} - if user_input is None: - return self.async_show_form(step_id="link") - - freebox_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - - if not os.path.exists(freebox_dir.path): - await self.hass.async_add_executor_job(os.makedirs, freebox_dir.path) - - token_file = self.hass.config.path( - f"{freebox_dir.path}/{slugify(self._host)}.conf" - ) - - fbx = Freepybox(APP_DESC, token_file, API_VERSION) + fbx = await get_api(self.hass, self._host) try: # Open connection and check authentication await fbx.open(self._host, self._port) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 4855b148dc9f9..3ca730e603b05 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -1,7 +1,7 @@ """Represent the Freebox router and its devices and sensors.""" from datetime import datetime, timedelta import logging -import os +from pathlib import Path from typing import Dict from aiofreepybox import Freepybox @@ -57,16 +57,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): async def setup(self) -> None: """Set up a Freebox router.""" - freebox_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - - if not os.path.exists(freebox_dir.path): - await self.hass.async_add_executor_job(os.makedirs, freebox_dir.path) - - token_file = self.hass.config.path( - f"{freebox_dir.path}/{slugify(self._host)}.conf" - ) - - self._api = Freepybox(APP_DESC, token_file, API_VERSION) + self._api = await get_api(self.hass, self._host) await self._api.open(self._host, self._port) @@ -219,3 +210,13 @@ def system(self) -> System: def wifi(self) -> Wifi: """Return the wifi.""" return self._api.wifi + + +async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: + """Get the Freebox API.""" + freebox_path = Path(hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path) + freebox_path.mkdir(exist_ok=True) + + token_file = Path(f"{freebox_path}/{slugify(host)}.conf") + + return Freepybox(APP_DESC, token_file, API_VERSION) From 98ebe6d97d056d6ceac742f7c30d9615d5cc7e71 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 26 Feb 2020 08:20:43 +0100 Subject: [PATCH 34/45] device_tracker set + tests with CoroutineMock() --- .../components/freebox/config_flow.py | 3 ++ .../components/freebox/device_tracker.py | 4 +- tests/components/freebox/test_config_flow.py | 43 ++++++------------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 3300dbc504167..b2d1a0ab771c8 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -63,6 +63,9 @@ async def async_step_link(self, user_input=None): Given a configured host, will ask the user to press the button to connect to the router. """ + if user_input is None: + return self.async_show_form(step_id="link") + errors = {} fbx = await get_api(self.hass, self._host) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 55f548c8e9109..9a3df54c101a3 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -22,7 +22,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for Freebox component.""" router = hass.data[DOMAIN][entry.unique_id] - tracked = [] + tracked = set() @callback def update_router(): @@ -46,7 +46,7 @@ def add_entities(router, async_add_entities, tracked): continue new_tracked.append(FreeboxDevice(router, device)) - tracked.append(mac) + tracked.add(mac) if new_tracked: async_add_entities(new_tracked, True) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 0d76632dd9025..68e787e1ba05b 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,13 +1,10 @@ """Tests for the Freebox config flow.""" -import asyncio -from unittest.mock import MagicMock - from aiofreepybox.exceptions import ( AuthorizationError, HttpRequestError, InvalidTokenError, ) -from asynctest import patch +from asynctest import CoroutineMock, patch import pytest from homeassistant import data_entry_flow @@ -24,28 +21,16 @@ @pytest.fixture(name="connect") def mock_controller_connect(): """Mock a successful connection.""" - with patch( - "homeassistant.components.freebox.config_flow.Freepybox" - ) as service_mock: - service_mock.return_value.open = MagicMock(return_value=asyncio.Future()) - service_mock.return_value.open.return_value.set_result(None) - - service_mock.return_value.system.get_config = MagicMock( - return_value=asyncio.Future() - ) - service_mock.return_value.system.get_config.return_value.set_result(None) - - service_mock.return_value.lan.get_hosts_list = MagicMock( - return_value=asyncio.Future() - ) - service_mock.return_value.lan.get_hosts_list.return_value.set_result(None) - - service_mock.return_value.close = MagicMock(return_value=asyncio.Future()) - service_mock.return_value.close.return_value.set_result(None) + with patch("homeassistant.components.freebox.router.Freepybox") as service_mock: + service_mock.return_value.open = CoroutineMock() + service_mock.return_value.system.get_config = CoroutineMock() + service_mock.return_value.lan.get_hosts_list = CoroutineMock() + service_mock.return_value.connection.get_status = CoroutineMock() + service_mock.return_value.close = CoroutineMock() yield service_mock -async def test_user(hass, connect): +async def test_user(hass): """Test user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -63,7 +48,7 @@ async def test_user(hass, connect): assert result["step_id"] == "link" -async def test_import(hass, connect): +async def test_import(hass): """Test import step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -74,7 +59,7 @@ async def test_import(hass, connect): assert result["step_id"] == "link" -async def test_discovery(hass, connect): +async def test_discovery(hass): """Test discovery step.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -126,7 +111,7 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_configured" -async def test_abort_on_link_failed(hass): +async def test_on_link_failed(hass): """Test when we have errors during linking the router.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -135,7 +120,7 @@ async def test_abort_on_link_failed(hass): ) with patch( - "homeassistant.components.freebox.config_flow.Freepybox.open", + "homeassistant.components.freebox.router.Freepybox.open", side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -143,7 +128,7 @@ async def test_abort_on_link_failed(hass): assert result["errors"] == {"base": "register_failed"} with patch( - "homeassistant.components.freebox.config_flow.Freepybox.open", + "homeassistant.components.freebox.router.Freepybox.open", side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -151,7 +136,7 @@ async def test_abort_on_link_failed(hass): assert result["errors"] == {"base": "connection_failed"} with patch( - "homeassistant.components.freebox.config_flow.Freepybox.open", + "homeassistant.components.freebox.router.Freepybox.open", side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) From f9fd400a02ef54864723dcae1008bfa666344fbc Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 26 Feb 2020 08:35:35 +0100 Subject: [PATCH 35/45] Removing active & reachable from tracker attrs --- homeassistant/components/freebox/device_tracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 9a3df54c101a3..45390e5527f7c 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -74,8 +74,6 @@ def update(self) -> None: if device.get("attrs") is None: # device self._attrs = { - "active": self._active, - "reachable": device["reachable"], "last_time_reachable": datetime.fromtimestamp( device["last_time_reachable"] ), From 6a20db77b8c0d98ccfed0dc24c96ee584669c25d Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 26 Feb 2020 12:46:43 +0100 Subject: [PATCH 36/45] Review --- homeassistant/components/freebox/__init__.py | 2 +- homeassistant/components/freebox/router.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 6931475e9995c..dba435341b736 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -28,7 +28,7 @@ async def async_setup(hass, config): - """Set up the Freebox component from legacy config file.""" + """Set up the Freebox component.""" conf = config.get(DOMAIN) async def discovery_dispatch(service, discovery_info): diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 3ca730e603b05..236edf42caf0d 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -33,7 +33,7 @@ class FreeboxRouter: """Representation of a Freebox router.""" - def __init__(self, hass: HomeAssistantType, entry: ConfigEntry): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: """Initialize a Freebox router.""" self.hass = hass self._entry = entry From 4b2bd3131c08ee8dfd0a63da51f38399b1730fa4 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 26 Feb 2020 12:52:43 +0100 Subject: [PATCH 37/45] Fix pipeline --- tests/components/freebox/conftest.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/components/freebox/conftest.py diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py new file mode 100644 index 0000000000000..e813469cbbfb4 --- /dev/null +++ b/tests/components/freebox/conftest.py @@ -0,0 +1,11 @@ +"""Test helpers for Freebox.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_path(): + """Mock path lib.""" + with patch("homeassistant.components.freebox.router.Path"): + yield From 53789be47f94b23a60d857724ec66c614062a9d4 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 26 Feb 2020 23:35:20 +0100 Subject: [PATCH 38/45] typing --- homeassistant/components/freebox/device_tracker.py | 2 +- homeassistant/components/freebox/sensor.py | 4 +++- homeassistant/components/freebox/switch.py | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 45390e5527f7c..ea9919f57420f 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -55,7 +55,7 @@ def add_entities(router, async_add_entities, tracked): class FreeboxDevice(ScannerEntity): """Representation of a Freebox device.""" - def __init__(self, router: FreeboxRouter, device: Dict[str, any]): + def __init__(self, router: FreeboxRouter, device: Dict[str, any]) -> None: """Initialize a Freebox device.""" self._router = router self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 99bb8d1ccf931..28b74d4e9d33c 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -57,7 +57,9 @@ async def async_setup_entry( class FreeboxSensor(Entity): """Representation of a Freebox sensor.""" - def __init__(self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any]): + def __init__( + self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any] + ) -> None: """Initialize a Freebox sensor.""" self._state = None self._router = router diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 67a442210c28a..9e1011d5d3cb2 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -25,7 +25,7 @@ async def async_setup_entry( class FreeboxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, router: FreeboxRouter): + def __init__(self, router: FreeboxRouter) -> None: """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None @@ -38,12 +38,12 @@ def unique_id(self) -> str: return self._unique_id @property - def name(self): + def name(self) -> str: """Return the name of the switch.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state From 0b2e7a4740f91b322e8a065dcf3edd5b15826db3 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 27 Feb 2020 12:38:35 +0100 Subject: [PATCH 39/45] typing --- homeassistant/components/freebox/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 236edf42caf0d..a4226fede4b90 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -71,7 +71,7 @@ async def setup(self) -> None: await self.update_all() async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) - async def update_all(self, now=None) -> None: + async def update_all(self, now: datetime = None) -> None: """Update all Freebox platforms.""" await self.update_sensors() await self.update_devices() From a1396c128154d6fe078476cf9772518c702ce2f4 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Thu, 27 Feb 2020 19:43:57 +0100 Subject: [PATCH 40/45] typing --- homeassistant/components/freebox/router.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index a4226fede4b90..6c43cdc9c8b71 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 +from typing import Dict, Optional from aiofreepybox import Freepybox from aiofreepybox.api.system import System @@ -71,7 +71,7 @@ async def setup(self) -> None: await self.update_all() async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) - async def update_all(self, now: datetime = None) -> None: + async def update_all(self, now: Optional[datetime] = None) -> None: """Update all Freebox platforms.""" await self.update_sensors() await self.update_devices() From a89d3b527ecb8db70542641a30bd3adf78d0583c Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Sat, 29 Feb 2020 12:29:03 +0100 Subject: [PATCH 41/45] Raise ConfigEntryNotReady when HttpRequestError at setup --- homeassistant/components/freebox/__init__.py | 8 +------- homeassistant/components/freebox/router.py | 8 +++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index dba435341b736..b849d03e32dd6 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -2,7 +2,6 @@ import asyncio import logging -from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX @@ -61,12 +60,7 @@ async def discovery_dispatch(service, discovery_info): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Freebox component.""" router = FreeboxRouter(hass, entry) - - try: - await router.setup() - except HttpRequestError: - _LOGGER.exception("Failed to connect to Freebox") - return False + await router.setup() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 6c43cdc9c8b71..5a18d6a28170e 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -7,9 +7,11 @@ from aiofreepybox import Freepybox from aiofreepybox.api.system import System from aiofreepybox.api.wifi import Wifi +from aiofreepybox.exceptions import HttpRequestError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -59,7 +61,11 @@ async def setup(self) -> None: """Set up a Freebox router.""" self._api = await get_api(self.hass, self._host) - await self._api.open(self._host, self._port) + try: + await self._api.open(self._host, self._port) + except HttpRequestError: + _LOGGER.exception("Failed to connect to Freebox") + return ConfigEntryNotReady # System fbx_config = await self._api.system.get_config() From b22c2e31eb049276c76a2e964ef2da5127cc25c5 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 9 Mar 2020 19:02:35 +0100 Subject: [PATCH 42/45] Review --- homeassistant/components/freebox/__init__.py | 3 +- homeassistant/components/freebox/router.py | 68 ++++++-------------- homeassistant/components/freebox/sensor.py | 11 ++-- 3 files changed, 24 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index b849d03e32dd6..c3cf8f29ab64c 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -97,8 +97,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): ) ) if unload_ok: - router = hass.data[DOMAIN][entry.unique_id] + router = hass.data[DOMAIN].pop(entry.unique_id) await router.close() - hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 5a18d6a28170e..c5c47bce16efa 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -44,16 +44,15 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: self._api: Freepybox = None self._name = None - self._mac = None + self.mac = None self._sw_v = None self._attrs = {} # Devices - self._devices: Dict[str, any] = {} + self.devices: Dict[str, any] = {} # Sensors - self._temperature_sensors: Dict[str, any] = {} - self._connection_sensors: Dict[str, any] = {} + self.sensors: Dict[str, any] = {} self.listeners = [] @@ -69,7 +68,7 @@ async def setup(self) -> None: # System fbx_config = await self._api.system.get_config() - self._mac = fbx_config["mac"] + self.mac = fbx_config["mac"] self._name = fbx_config["model_info"]["pretty_name"] self._sw_v = fbx_config["firmware_version"] @@ -90,9 +89,9 @@ async def update_devices(self) -> None: # Adds the Freebox itself fbx_devices.append( { - "primary_name": self.name, + "primary_name": self._name, "l2ident": {"id": self.mac}, - "vendor_name": self.manufacturer, + "vendor_name": "Freebox SAS", "host_type": "router", "active": True, "attrs": self._attrs, @@ -102,10 +101,10 @@ async def update_devices(self) -> None: for fbx_device in fbx_devices: device_mac = fbx_device["l2ident"]["id"] - if self._devices.get(device_mac) is None: + if self.devices.get(device_mac) is None: new_device = True - self._devices[device_mac] = fbx_device + self.devices[device_mac] = fbx_device async_dispatcher_send(self.hass, self.signal_device_update) @@ -116,17 +115,16 @@ async def update_sensors(self) -> None: """Update Freebox sensors.""" # System sensors syst_datas: Dict[str, any] = await self._api.system.get_config() - temperature_datas = {item["id"]: item for item in syst_datas["sensors"]} - # According to the doc it is only temperature sensors in celsius degree. - # Name and id of the sensors may vary under Freebox devices. - for sensor_key, sensor_attrs in temperature_datas.items(): - self._temperature_sensors[sensor_key] = sensor_attrs["value"] + # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. + # Name and id of sensors may vary under Freebox devices. + for sensor in syst_datas["sensors"]: + self.sensors[sensor["id"]] = sensor["value"] # Connection sensors connection_datas: Dict[str, any] = await self._api.connection.get_status() - for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): - self._connection_sensors[sensor_key] = connection_datas[sensor_key] + for sensor_key in CONNECTION_SENSORS: + self.sensors[sensor_key] = connection_datas[sensor_key] self._attrs = { "IPv4": connection_datas.get("ipv4"), @@ -135,7 +133,7 @@ async def update_sensors(self) -> None: "uptime": datetime.fromtimestamp( round(datetime.now().timestamp()) - syst_datas["uptime_val"] ), - "firmware_version": self.firmware_version, + "firmware_version": self._sw_v, "serial": syst_datas["serial"], } @@ -151,47 +149,17 @@ async def close(self) -> None: await self._api.close() self._api = None - @property - def name(self) -> str: - """Return the router name.""" - return self._name - - @property - def manufacturer(self) -> str: - """Return the router manufacturer.""" - return "Freebox SAS" - - @property - def mac(self) -> str: - """Return the router MAC address.""" - return self._mac - - @property - def firmware_version(self) -> str: - """Return the router software version.""" - return self._sw_v - @property def device_info(self) -> Dict[str, any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, "identifiers": {(DOMAIN, self.mac)}, - "name": self.name, - "manufacturer": self.manufacturer, - "sw_version": self.firmware_version, + "name": self._name, + "manufacturer": "Freebox SAS", + "sw_version": self._sw_v, } - @property - def devices(self) -> Dict[str, any]: - """Return all devices.""" - return self._devices - - @property - def sensors(self) -> Dict[str, any]: - """Return all sensors.""" - return {**self._temperature_sensors, **self._connection_sensors} - @property def signal_device_new(self) -> str: """Event specific per Freebox entry to signal new device.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 28b74d4e9d33c..fe3f28d0812da 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -31,18 +31,17 @@ async def async_setup_entry( # System sensors syst_datas: Dict[str, any] = await router.system.get_config() - temperature_datas = {item["id"]: item for item in syst_datas["sensors"]} - # According to the doc it is only temperature sensors in celsius degree. - # Name and id of the sensors may vary under Freebox devices. - for sensor_key, sensor_attrs in temperature_datas.items(): + # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. + # Name and id of sensors may vary under Freebox devices. + for sensor in syst_datas["sensors"]: entities.append( FreeboxSensor( router, - sensor_key, + sensor["id"], { **TEMPERATURE_SENSOR_TEMPLATE, - **{SENSOR_NAME: f"Freebox {sensor_attrs['name']}"}, + **{SENSOR_NAME: f"Freebox {sensor['name']}"}, }, ) ) From fa3f9397efd919a0be33cabbb643dcb3a783a8f5 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Mon, 9 Mar 2020 21:58:17 +0100 Subject: [PATCH 43/45] Multiple Freebox s --- homeassistant/components/freebox/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index c3cf8f29ab64c..9e303c75e7af3 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -15,13 +15,12 @@ _LOGGER = logging.getLogger(__name__) +FREEBOX_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} +) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} - ) - }, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [FREEBOX_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) @@ -48,11 +47,12 @@ async def discovery_dispatch(service, discovery_info): if conf is None: return True - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + for freebox_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=freebox_conf, + ) ) - ) return True From b3d67bf9432d7a3ed94a8d9671076435a38115b7 Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 11 Mar 2020 13:25:08 +0100 Subject: [PATCH 44/45] Review: store sensors in router --- homeassistant/components/freebox/const.py | 1 + homeassistant/components/freebox/router.py | 25 ++++++++++++---------- homeassistant/components/freebox/sensor.py | 25 +++------------------- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 0612e4e76f19a..faae7a4397700 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -30,6 +30,7 @@ SENSOR_UNIT = "unit" SENSOR_ICON = "icon" SENSOR_DEVICE_CLASS = "device_class" +SENSOR_VALUE = "value" CONNECTION_SENSORS = { "rate_down": { diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index c5c47bce16efa..6115ebb7e8901 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -5,7 +5,6 @@ from typing import Dict, Optional from aiofreepybox import Freepybox -from aiofreepybox.api.system import System from aiofreepybox.api.wifi import Wifi from aiofreepybox.exceptions import HttpRequestError @@ -23,8 +22,11 @@ APP_DESC, CONNECTION_SENSORS, DOMAIN, + SENSOR_NAME, + SENSOR_VALUE, STORAGE_KEY, STORAGE_VERSION, + TEMPERATURE_SENSOR_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) @@ -48,10 +50,7 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: self._sw_v = None self._attrs = {} - # Devices self.devices: Dict[str, any] = {} - - # Sensors self.sensors: Dict[str, any] = {} self.listeners = [] @@ -119,12 +118,21 @@ async def update_sensors(self) -> None: # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. for sensor in syst_datas["sensors"]: - self.sensors[sensor["id"]] = sensor["value"] + self.sensors[sensor["id"]] = { + **TEMPERATURE_SENSOR_TEMPLATE, + **{ + SENSOR_NAME: f"Freebox {sensor['name']}", + SENSOR_VALUE: sensor["value"], + }, + } # Connection sensors connection_datas: Dict[str, any] = await self._api.connection.get_status() for sensor_key in CONNECTION_SENSORS: - self.sensors[sensor_key] = connection_datas[sensor_key] + self.sensors[sensor_key] = { + **CONNECTION_SENSORS[sensor_key], + **{SENSOR_VALUE: connection_datas[sensor_key]}, + } self._attrs = { "IPv4": connection_datas.get("ipv4"), @@ -175,11 +183,6 @@ def signal_sensor_update(self) -> str: """Event specific per Freebox entry to signal updates in sensors.""" return f"{DOMAIN}-{self._host}-sensor-update" - @property - def system(self) -> System: - """Return the system.""" - return self._api.system - @property def wifi(self) -> Wifi: """Return the wifi.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index fe3f28d0812da..26699b8fd309a 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -9,13 +9,12 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( - CONNECTION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, SENSOR_ICON, SENSOR_NAME, SENSOR_UNIT, - TEMPERATURE_SENSOR_TEMPLATE, + SENSOR_VALUE, ) from .router import FreeboxRouter @@ -29,25 +28,7 @@ async def async_setup_entry( router = hass.data[DOMAIN][entry.unique_id] entities = [] - # System sensors - syst_datas: Dict[str, any] = await router.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. - for sensor in syst_datas["sensors"]: - entities.append( - FreeboxSensor( - router, - sensor["id"], - { - **TEMPERATURE_SENSOR_TEMPLATE, - **{SENSOR_NAME: f"Freebox {sensor['name']}"}, - }, - ) - ) - - # Connection sensors - for sensor_key, sensor_attrs in CONNECTION_SENSORS.items(): + for sensor_key, sensor_attrs in router.sensors.items(): entities.append(FreeboxSensor(router, sensor_key, sensor_attrs)) async_add_entities(entities, True) @@ -73,7 +54,7 @@ def __init__( def update(self) -> None: """Update the Freebox sensor.""" - state = self._router.sensors[self._sensor_type] + state = self._router.sensors[self._sensor_type][SENSOR_VALUE] if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: self._state = round(state / 1000, 2) else: From ae6223ceb79bbbfe85f75c16bc9875d9a42b87af Mon Sep 17 00:00:00 2001 From: Quentin POLLET Date: Wed, 11 Mar 2020 19:12:37 +0100 Subject: [PATCH 45/45] Freebox: a sensor story --- homeassistant/components/freebox/const.py | 1 - homeassistant/components/freebox/router.py | 24 ++++++++-------------- homeassistant/components/freebox/sensor.py | 20 ++++++++++++++---- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index faae7a4397700..0612e4e76f19a 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -30,7 +30,6 @@ SENSOR_UNIT = "unit" SENSOR_ICON = "icon" SENSOR_DEVICE_CLASS = "device_class" -SENSOR_VALUE = "value" CONNECTION_SENSORS = { "rate_down": { diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 6115ebb7e8901..7b4784c6ca47b 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -22,11 +22,8 @@ APP_DESC, CONNECTION_SENSORS, DOMAIN, - SENSOR_NAME, - SENSOR_VALUE, STORAGE_KEY, STORAGE_VERSION, - TEMPERATURE_SENSOR_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) @@ -51,7 +48,8 @@ def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: self._attrs = {} self.devices: Dict[str, any] = {} - self.sensors: Dict[str, any] = {} + self.sensors_temperature: Dict[str, int] = {} + self.sensors_connection: Dict[str, float] = {} self.listeners = [] @@ -118,21 +116,12 @@ async def update_sensors(self) -> None: # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. for sensor in syst_datas["sensors"]: - self.sensors[sensor["id"]] = { - **TEMPERATURE_SENSOR_TEMPLATE, - **{ - SENSOR_NAME: f"Freebox {sensor['name']}", - SENSOR_VALUE: sensor["value"], - }, - } + self.sensors_temperature[sensor["name"]] = sensor["value"] # Connection sensors connection_datas: Dict[str, any] = await self._api.connection.get_status() for sensor_key in CONNECTION_SENSORS: - self.sensors[sensor_key] = { - **CONNECTION_SENSORS[sensor_key], - **{SENSOR_VALUE: connection_datas[sensor_key]}, - } + self.sensors_connection[sensor_key] = connection_datas[sensor_key] self._attrs = { "IPv4": connection_datas.get("ipv4"), @@ -183,6 +172,11 @@ def signal_sensor_update(self) -> str: """Event specific per Freebox entry to signal updates in sensors.""" return f"{DOMAIN}-{self._host}-sensor-update" + @property + def sensors(self) -> Wifi: + """Return the wifi.""" + return {**self.sensors_temperature, **self.sensors_connection} + @property def wifi(self) -> Wifi: """Return the wifi.""" diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 26699b8fd309a..a3c5c32901caa 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -9,12 +9,13 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( + CONNECTION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, SENSOR_ICON, SENSOR_NAME, SENSOR_UNIT, - SENSOR_VALUE, + TEMPERATURE_SENSOR_TEMPLATE, ) from .router import FreeboxRouter @@ -28,8 +29,19 @@ async def async_setup_entry( router = hass.data[DOMAIN][entry.unique_id] entities = [] - for sensor_key, sensor_attrs in router.sensors.items(): - entities.append(FreeboxSensor(router, sensor_key, sensor_attrs)) + for sensor_name in router.sensors_temperature: + entities.append( + FreeboxSensor( + router, + sensor_name, + {**TEMPERATURE_SENSOR_TEMPLATE, SENSOR_NAME: f"Freebox {sensor_name}"}, + ) + ) + + for sensor_key in CONNECTION_SENSORS: + entities.append( + FreeboxSensor(router, sensor_key, CONNECTION_SENSORS[sensor_key]) + ) async_add_entities(entities, True) @@ -54,7 +66,7 @@ def __init__( def update(self) -> None: """Update the Freebox sensor.""" - state = self._router.sensors[self._sensor_type][SENSOR_VALUE] + state = self._router.sensors[self._sensor_type] if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: self._state = round(state / 1000, 2) else: