From 566cb11fe83f6908e0b3525cd1d0ae90c9e0660f Mon Sep 17 00:00:00 2001 From: stilllman Date: Sun, 18 Feb 2018 00:31:36 +0100 Subject: [PATCH 1/3] Add a device tracker for Freebox routers --- .coveragerc | 1 + .../components/device_tracker/freebox.py | 145 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 149 insertions(+) create mode 100644 homeassistant/components/device_tracker/freebox.py diff --git a/.coveragerc b/.coveragerc index eb73cb66c3055..d48bb82eefa59 100644 --- a/.coveragerc +++ b/.coveragerc @@ -393,6 +393,7 @@ omit = homeassistant/components/device_tracker/bt_home_hub_5.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/gpslogger.py diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py new file mode 100644 index 0000000000000..b42b4a84037dc --- /dev/null +++ b/homeassistant/components/device_tracker/freebox.py @@ -0,0 +1,145 @@ +""" +Support for device tracking through Freebox routers. + +This tracker keeps track of the devices connected to the configured Freebox. + +Example configuration.yaml entry: + +device_tracker: + - platform: freebox + host: foobar.fbox.fr + port: 1234 + +You can find out your Freebox host and port by opening this address in your +browser: http://mafreebox.freebox.fr/api_version. The returned json should +contain an api_domain (host) and a https_port (port). + +The first time you add your Freebox, you will need to authorize Home Assistant +by pressing the right button on the facade of the Freebox when prompted to do +so. + +Note that the Freebox waits for some time before marking a device as +inactive, meaning that there will be a small delay (1 or 2 minutes) +between the time you disconnect a device and the time it will appear +as "away" in Hass. You should take this into account when specifying +consider_home. +On the contrary, the Freebox immediately reports devices newly connected, so +they should appear as "home" almost instantly, as soon as Hass refreshes the +devices states. + +""" +import logging +from collections import namedtuple +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import ( + CONF_HOST, CONF_PORT) +from homeassistant.util import Throttle + +REQUIREMENTS = ['freepybox==0.0.3'] + +_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) + + +def get_scanner(hass, config): + """Validate the configuration and return a Bbox scanner.""" + scanner = FreeboxDeviceScanner(hass, config[DOMAIN]) + + 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): + """This class scans for devices connected to the Freebox.""" + + def __init__(self, hass, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.port = config[CONF_PORT] + self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) + + self.last_results = [] # type: List[Device] + + self.success_init = self._update_info() + _LOGGER.info("Scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + _LOGGER.info("Devices detected: " + str([device.name for device in self.last_results])) + return [device.id for device in self.last_results] + + def get_device_name(self, id): + """Return the name of the given device or None if we don't know.""" + for device in self.last_results: + if device.id == id: + return device.name + + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Check the Freebox for devices. + + Returns boolean if scanning successful. + """ + _LOGGER.info('Scanning devices') + + from freepybox import Freepybox + import socket + + # 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. + fbx = Freepybox( + app_desc=app_desc, + token_file=self.token_file, + api_version=api_version) + fbx.open(self.host, self.port) + try: + hosts = fbx.lan.get_hosts_list() + finally: + fbx.close() + + last_results = [_build_device(device) + for device in hosts + if device['active']] + + self.last_results = last_results + + _LOGGER.info('Scan successful') + return True diff --git a/requirements_all.txt b/requirements_all.txt index 0879c539ea83e..654245fdf2596 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -323,6 +323,9 @@ flux_led==0.21 # homeassistant.components.sensor.foobot foobot_async==0.3.1 +# homeassistant.components.device_tracker.freebox +freepybox==0.0.3 + # homeassistant.components.notify.free_mobile freesms==0.1.2 From a0b48030eb8c9261e97c1b2323ed690692f270ee Mon Sep 17 00:00:00 2001 From: stilllman Date: Tue, 20 Feb 2018 22:01:44 +0100 Subject: [PATCH 2/3] Automatic setup of Freebox device tracker based on discovery --- .../components/device_tracker/freebox.py | 53 ++++++++----------- homeassistant/components/discovery.py | 1 + requirements_all.txt | 1 + 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index b42b4a84037dc..1cbee0bde45c1 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -28,6 +28,7 @@ devices states. """ +import copy import logging from collections import namedtuple from datetime import timedelta @@ -35,8 +36,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval from homeassistant.components.device_tracker import ( - DOMAIN, PLATFORM_SCHEMA, DeviceScanner) + PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) from homeassistant.const import ( CONF_HOST, CONF_PORT) from homeassistant.util import Throttle @@ -56,11 +58,16 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -def get_scanner(hass, config): - """Validate the configuration and return a Bbox scanner.""" - scanner = FreeboxDeviceScanner(hass, config[DOMAIN]) +def setup_scanner(hass, config, see, discovery_info=None): + 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]) - return scanner if scanner.success_init else None + FreeboxDeviceScanner(hass, freebox_config, see) + return True Device = namedtuple('Device', ['id', 'name', 'ip']) @@ -73,41 +80,25 @@ def _build_device(device_dict): device_dict['l3connectivities'][0]['addr']) -class FreeboxDeviceScanner(DeviceScanner): +class FreeboxDeviceScanner(object): """This class scans for devices connected to the Freebox.""" - def __init__(self, hass, config): + def __init__(self, hass, config, see): """Initialize the scanner.""" self.host = config[CONF_HOST] self.port = config[CONF_PORT] self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) - self.last_results = [] # type: List[Device] + self.see = see - self.success_init = self._update_info() - _LOGGER.info("Scanner initialized") + self.update_info() - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - _LOGGER.info("Devices detected: " + str([device.name for device in self.last_results])) - return [device.id for device in self.last_results] - - def get_device_name(self, id): - """Return the name of the given device or None if we don't know.""" - for device in self.last_results: - if device.id == id: - return device.name - - return None + interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + track_time_interval(hass, self.update_info, interval) @Throttle(MIN_TIME_BETWEEN_SCANS) - def _update_info(self): - """Check the Freebox for devices. - - Returns boolean if scanning successful. - """ + def update_info(self, now=None): + """Check the Freebox for devices.""" _LOGGER.info('Scanning devices') from freepybox import Freepybox @@ -139,7 +130,7 @@ def _update_info(self): for device in hosts if device['active']] - self.last_results = last_results + for d in last_results: + self.see(mac=d.id, host_name=d.name) - _LOGGER.info('Scan successful') return True diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 69447b81cd427..00d4291539b93 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -84,6 +84,7 @@ 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), + 'freebox': ('device_tracker', 'freebox'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/requirements_all.txt b/requirements_all.txt index 654245fdf2596..4f24471012337 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -618,6 +618,7 @@ pdunehd==1.3 # homeassistant.components.media_player.pandora pexpect==4.0.1 +# homeassistant.components.freebox # homeassistant.components.rpi_pfio pifacecommon==4.1.2 From c8de2aa214057e4a748c46de0e2a1728b652a4a4 Mon Sep 17 00:00:00 2001 From: stilllman Date: Sat, 28 Apr 2018 01:07:04 +0200 Subject: [PATCH 3/3] Make the Freebox device tracker asynchronous --- .../components/device_tracker/freebox.py | 104 ++++++++---------- requirements_all.txt | 7 +- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index 1cbee0bde45c1..67957ca99b9f6 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -3,47 +3,26 @@ This tracker keeps track of the devices connected to the configured Freebox. -Example configuration.yaml entry: - -device_tracker: - - platform: freebox - host: foobar.fbox.fr - port: 1234 - -You can find out your Freebox host and port by opening this address in your -browser: http://mafreebox.freebox.fr/api_version. The returned json should -contain an api_domain (host) and a https_port (port). - -The first time you add your Freebox, you will need to authorize Home Assistant -by pressing the right button on the facade of the Freebox when prompted to do -so. - -Note that the Freebox waits for some time before marking a device as -inactive, meaning that there will be a small delay (1 or 2 minutes) -between the time you disconnect a device and the time it will appear -as "away" in Hass. You should take this into account when specifying -consider_home. -On the contrary, the Freebox immediately reports devices newly connected, so -they should appear as "home" almost instantly, as soon as Hass refreshes the -devices states. - +For more details about this platform, 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 voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval +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) -from homeassistant.util import Throttle -REQUIREMENTS = ['freepybox==0.0.3'] +REQUIREMENTS = ['aiofreepybox==0.0.3'] _LOGGER = logging.getLogger(__name__) @@ -58,7 +37,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -def setup_scanner(hass, config, see, discovery_info=None): +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'] @@ -66,7 +46,9 @@ def setup_scanner(hass, config, see, discovery_info=None): _LOGGER.info("Discovered Freebox server: %s:%s", freebox_config[CONF_HOST], freebox_config[CONF_PORT]) - FreeboxDeviceScanner(hass, freebox_config, see) + 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 @@ -75,34 +57,22 @@ def setup_scanner(hass, config, see, discovery_info=None): def _build_device(device_dict): return Device( - device_dict['l2ident']['id'], - device_dict['primary_name'], - device_dict['l3connectivities'][0]['addr']) + device_dict['l2ident']['id'], + device_dict['primary_name'], + device_dict['l3connectivities'][0]['addr']) class FreeboxDeviceScanner(object): """This class scans for devices connected to the Freebox.""" - def __init__(self, hass, config, see): + def __init__(self, hass, config, async_see): """Initialize the scanner.""" + from aiofreepybox import Freepybox + self.host = config[CONF_HOST] self.port = config[CONF_PORT] self.token_file = hass.config.path(FREEBOX_CONFIG_FILE) - - self.see = see - - self.update_info() - - interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - track_time_interval(hass, self.update_info, interval) - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def update_info(self, now=None): - """Check the Freebox for devices.""" - _LOGGER.info('Scanning devices') - - from freepybox import Freepybox - import socket + self.async_see = async_see # Hardcode the app description to avoid invalidating the authentication # file at each new version. @@ -116,21 +86,35 @@ def update_info(self, now=None): } api_version = 'v1' # Use the lowest working version. - fbx = Freepybox( + self.fbx = Freepybox( app_desc=app_desc, token_file=self.token_file, api_version=api_version) - fbx.open(self.host, self.port) - try: - hosts = fbx.lan.get_hosts_list() - finally: - fbx.close() - last_results = [_build_device(device) - for device in hosts - if device['active']] + async def async_start(self, hass, interval): + """Perform a first update and start polling at the given interval.""" + await self.async_update_info() + interval = max(interval, MIN_TIME_BETWEEN_SCANS) + async_track_time_interval(hass, self.async_update_info, interval) - for d in last_results: - self.see(mac=d.id, host_name=d.name) + async def async_update_info(self, now=None): + """Check the Freebox for devices.""" + from aiofreepybox.exceptions import HttpRequestError - return True + _LOGGER.info('Scanning 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']] + + if active_devices: + await asyncio.wait([self.async_see(mac=d.id, host_name=d.name) + for d in active_devices]) + + await self.fbx.close() diff --git a/requirements_all.txt b/requirements_all.txt index 4f24471012337..ce14637af347e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -81,6 +81,9 @@ aioautomatic==0.6.5 # homeassistant.components.sensor.dnsip aiodns==1.1.1 +# homeassistant.components.device_tracker.freebox +aiofreepybox==0.0.3 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 @@ -323,9 +326,6 @@ flux_led==0.21 # homeassistant.components.sensor.foobot foobot_async==0.3.1 -# homeassistant.components.device_tracker.freebox -freepybox==0.0.3 - # homeassistant.components.notify.free_mobile freesms==0.1.2 @@ -618,7 +618,6 @@ pdunehd==1.3 # homeassistant.components.media_player.pandora pexpect==4.0.1 -# homeassistant.components.freebox # homeassistant.components.rpi_pfio pifacecommon==4.1.2