From 699a38de52bfd303279db1720c779884360c6c2f Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 28 Dec 2018 00:26:09 +0100 Subject: [PATCH] Add Freebox component with sensors and device tracker (#18472) * Add freebox component with sensor and device tracker * script/gen_requirements_all passed and pylint fixes * Fix docstring in wrong place * Fix indentation * Lint fixes * More lint fixes * Lint fixes again * Pylint fixes * Bump aiopyfreebox version * Close freebox connection on HA Stop * Fixed docstring * Fixed ident * Lint fixes * Fix cloing session when HA stop * Fix URL * Fix URL * Fix double look up in discovery datas * Fix logging level * Fix get_device_name Thx for the hint Martin * Fix async_update_info * Update requirements_all.txt --- .coveragerc | 4 +- .../components/device_tracker/freebox.py | 127 ++++++------------ homeassistant/components/discovery.py | 3 +- homeassistant/components/freebox.py | 91 +++++++++++++ homeassistant/components/sensor/freebox.py | 86 ++++++++++++ requirements_all.txt | 4 +- 6 files changed, 223 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/freebox.py create mode 100644 homeassistant/components/sensor/freebox.py diff --git a/.coveragerc b/.coveragerc index d1125c59146..9b78f0696a8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -112,6 +112,9 @@ omit = homeassistant/components/evohome.py homeassistant/components/*/evohome.py + homeassistant/components/freebox.py + homeassistant/components/*/freebox.py + homeassistant/components/fritzbox.py homeassistant/components/*/fritzbox.py @@ -508,7 +511,6 @@ omit = homeassistant/components/device_tracker/bt_smarthub.py homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/ddwrt.py - homeassistant/components/device_tracker/freebox.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/googlehome.py diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index b96ee710044..f4e1ce5bd8a 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -1,56 +1,25 @@ """ -Support for device tracking through Freebox routers. +Support for Freebox devices (Freebox v6 and Freebox mini 4K). -This tracker keeps track of the devices connected to the configured Freebox. - -For more details about this platform, please refer to the documentation at +For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_tracker.freebox/ """ -import asyncio -import copy -import logging -import socket from collections import namedtuple -from datetime import timedelta +import logging -import voluptuous as vol +from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.freebox import DATA_FREEBOX -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) -from homeassistant.const import ( - CONF_HOST, CONF_PORT) - -REQUIREMENTS = ['aiofreepybox==0.0.5'] +DEPENDENCIES = ['freebox'] _LOGGER = logging.getLogger(__name__) -FREEBOX_CONFIG_FILE = 'freebox.conf' - -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port - })) - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Freebox device tracker and start the polling.""" - freebox_config = copy.deepcopy(config) - if discovery_info is not None: - freebox_config[CONF_HOST] = discovery_info['properties']['api_domain'] - freebox_config[CONF_PORT] = discovery_info['properties']['https_port'] - _LOGGER.info("Discovered Freebox server: %s:%s", - freebox_config[CONF_HOST], freebox_config[CONF_PORT]) - - scanner = FreeboxDeviceScanner(hass, freebox_config, async_see) - interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - await scanner.async_start(hass, interval) - return True +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']) @@ -62,59 +31,41 @@ def _build_device(device_dict): device_dict['l3connectivities'][0]['addr']) -class FreeboxDeviceScanner: - """This class scans for devices connected to the Freebox.""" +class FreeboxDeviceScanner(DeviceScanner): + """Queries the Freebox device.""" - def __init__(self, hass, config, async_see): + def __init__(self, fbx): """Initialize the scanner.""" - from aiofreepybox import Freepybox + self.last_results = {} + self.success_init = False + self.connection = fbx - self.host = config[CONF_HOST] - self.port = config[CONF_PORT] - self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) - self.async_see = async_see + 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 - # Hardcode the app description to avoid invalidating the authentication - # file at each new version. - # The version can be changed if we want the user to re-authorize HASS - # on her Freebox. - app_desc = { - 'app_id': 'hass', - 'app_name': 'Home Assistant', - 'app_version': '0.65', - 'device_name': socket.gethostname() - } - - api_version = 'v1' # Use the lowest working version. - self.fbx = Freepybox( - app_desc=app_desc, - token_file=self.token_file, - api_version=api_version) - - async def async_start(self, hass, interval): - """Perform a first update and start polling at the given interval.""" + async def async_scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" await self.async_update_info() - interval = max(interval, MIN_TIME_BETWEEN_SCANS) - async_track_time_interval(hass, self.async_update_info, interval) + return [device.id for device in self.last_results] - async def async_update_info(self, now=None): - """Check the Freebox for devices.""" - from aiofreepybox.exceptions import HttpRequestError + 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) + return name - _LOGGER.info('Scanning devices') + async def async_update_info(self): + """Ensure the information from the Freebox router is up to date.""" + _LOGGER.debug('Checking Devices') - await self.fbx.open(self.host, self.port) - try: - hosts = await self.fbx.lan.get_hosts_list() - except HttpRequestError: - _LOGGER.exception('Failed to scan devices') - else: - active_devices = [_build_device(device) - for device in hosts - if device['active']] + hosts = await self.connection.lan.get_hosts_list() - if active_devices: - await asyncio.wait([self.async_see(mac=d.id, host_name=d.name) - for d in active_devices]) + last_results = [_build_device(device) + for device in hosts + if device['active']] - await self.fbx.close() + self.last_results = last_results diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 17425309943..dd61fd01fdc 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -44,6 +44,7 @@ SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' SERVICE_OCTOPRINT = 'octoprint' +SERVICE_FREEBOX = 'freebox' SERVICE_IGD = 'igd' SERVICE_DLNA_DMR = 'dlna_dmr' @@ -71,6 +72,7 @@ SERVICE_HANDLERS = { SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_KONNECTED: ('konnected', None), SERVICE_OCTOPRINT: ('octoprint', None), + SERVICE_FREEBOX: ('freebox', None), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), 'roku': ('media_player', 'roku'), @@ -90,7 +92,6 @@ SERVICE_HANDLERS = { 'volumio': ('media_player', 'volumio'), 'lg_smart_device': ('media_player', 'lg_soundbar'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), - 'freebox': ('device_tracker', 'freebox'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/freebox.py b/homeassistant/components/freebox.py new file mode 100644 index 00000000000..99efbae0984 --- /dev/null +++ b/homeassistant/components/freebox.py @@ -0,0 +1,91 @@ +""" +Support for Freebox devices (Freebox v6 and Freebox mini 4K). + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/freebox/ +""" +import logging +import socket + +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_FREEBOX +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 + +REQUIREMENTS = ['aiofreepybox==0.0.6'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "freebox" +DATA_FREEBOX = DOMAIN + +FREEBOX_CONFIG_FILE = 'freebox.conf' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Freebox component.""" + conf = config.get(DOMAIN) + + async def discovery_dispatch(service, discovery_info): + if conf is None: + 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) + + 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 + + +async def async_setup_freebox(hass, config, host, port): + """Start up the Freebox component platforms.""" + from aiofreepybox import Freepybox + from aiofreepybox.exceptions import HttpRequestError + + 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 = 'v1' + + fbx = Freepybox( + app_desc=app_desc, + token_file=token_file, + api_version=api_version) + + try: + await fbx.open(host, port) + except HttpRequestError: + _LOGGER.exception('Failed to connect to Freebox') + else: + hass.data[DATA_FREEBOX] = fbx + + hass.async_create_task(async_load_platform( + hass, 'sensor', DOMAIN, {}, config)) + hass.async_create_task(async_load_platform( + hass, 'device_tracker', DOMAIN, {}, config)) + + async def close_fbx(event): + """Close Freebox connection on HA Stop.""" + await fbx.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) diff --git a/homeassistant/components/sensor/freebox.py b/homeassistant/components/sensor/freebox.py new file mode 100644 index 00000000000..cc737d2d398 --- /dev/null +++ b/homeassistant/components/sensor/freebox.py @@ -0,0 +1,86 @@ +""" +Support for Freebox devices (Freebox v6 and Freebox mini 4K). + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.freebox/ +""" +import logging + +from homeassistant.components.freebox import DATA_FREEBOX +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['freebox'] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass, config, add_entities, discovery_info=None): + """Set up the sensors.""" + fbx = hass.data[DATA_FREEBOX] + add_entities([ + FbxRXSensor(fbx), + FbxTXSensor(fbx) + ]) + + +class FbxSensor(Entity): + """Representation of a freebox sensor.""" + + _name = 'generic' + + def __init__(self, fbx): + """Initialize the sensor.""" + self._fbx = fbx + self._state = None + self._datas = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + 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 = 'KB/s' + + @property + def unit_of_measurement(self): + """Define the unit.""" + return self._unit + + 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.""" + + _name = 'Freebox upload speed' + _unit = 'KB/s' + + @property + def unit_of_measurement(self): + """Define the unit.""" + return self._unit + + 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) diff --git a/requirements_all.txt b/requirements_all.txt index 8fc5c08e737..8d91334a9bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -98,8 +98,8 @@ aiodns==1.1.1 # homeassistant.components.esphome aioesphomeapi==1.3.0 -# homeassistant.components.device_tracker.freebox -aiofreepybox==0.0.5 +# homeassistant.components.freebox +aiofreepybox==0.0.6 # homeassistant.components.camera.yi aioftp==0.12.0