From 4d5f3da08babb5351be232872e176bb515e3ec03 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 18 Aug 2015 16:12:01 -0400 Subject: [PATCH 01/10] Initial commit for device_tracker component for Actiontec MI424WR Verizon FiOS Wireless Router --- .../components/device_tracker/actiontec.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 homeassistant/components/device_tracker/actiontec.py diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py new file mode 100644 index 00000000000..57e86b57b99 --- /dev/null +++ b/homeassistant/components/device_tracker/actiontec.py @@ -0,0 +1,144 @@ +""" +homeassistant.components.device_tracker.actiontec +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a Actiontec MI424WR (Verizon FIOS) router for device +presence. + +This device tracker needs telnet to be enabled on the router. + +Configuration: + +To use the Actiontec tracker you will need to add something like the following +to your config/configuration.yaml + +device_tracker: + platform: actiontec + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile(r'(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') + + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """ Validates config and returns a DD-WRT scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = ActiontecDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class ActiontecDeviceScanner(object): + """ This class queries a an actiontec router + for connected devices. Adapted from DD-WRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_actiontec_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return self.last_results + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client == device: + return client + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the ASUSWRT router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + # _LOGGER.info("Checking ARP") + data = self.get_actiontec_data() + if not data: + return False + + self.last_results = data + return True + + def get_actiontec_data(self): + """ Retrieve data from ASUSWRT and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt_string = telnet.read_until(b'Wireless Broadband Router> ').split(b'\n')[-1] + telnet.write('firewall mac_cache_dump\n'.encode('ascii')) + telnet.write('\n'.encode('ascii')) + telnet.read_until(prompt_string).split(b'\n')[1:-1] + leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return + + devices = [] + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + if match is not None: devices.append(match.group('mac')) + + return devices \ No newline at end of file From d2f01174e77b69e59bb8bb86253dce9952150377 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 18 Aug 2015 16:41:03 -0400 Subject: [PATCH 02/10] fixed warnings --- homeassistant/components/device_tracker/actiontec.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 57e86b57b99..706555c6d83 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -125,7 +125,7 @@ class ActiontecDeviceScanner(object): prompt_string = telnet.read_until(b'Wireless Broadband Router> ').split(b'\n')[-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write('\n'.encode('ascii')) - telnet.read_until(prompt_string).split(b'\n')[1:-1] + skip_line = telnet.read_until(prompt_string).split(b'\n')[1:-1] leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1] telnet.write('exit\n'.encode('ascii')) except EOFError: @@ -139,6 +139,8 @@ class ActiontecDeviceScanner(object): devices = [] for lease in leases_result: match = _LEASES_REGEX.search(lease.decode('utf-8')) - if match is not None: devices.append(match.group('mac')) + if match is not None: + devices.append(match.group('mac')) + + return devices - return devices \ No newline at end of file From c471e39fa0598bb683816710fdf4df6db75e10b0 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 18 Aug 2015 16:50:40 -0400 Subject: [PATCH 03/10] trying to fix more warnings... --- .../components/device_tracker/actiontec.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 706555c6d83..d8d60be3ca8 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -1,15 +1,15 @@ """ homeassistant.components.device_tracker.actiontec ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Device tracker platform that supports scanning a Actiontec MI424WR (Verizon FIOS) router for device -presence. +Device tracker platform that supports scanning an Actiontec MI424WR +(Verizon FIOS) router for device presence. This device tracker needs telnet to be enabled on the router. Configuration: -To use the Actiontec tracker you will need to add something like the following -to your config/configuration.yaml +To use the Actiontec tracker you will need to add something like the +following to your config/configuration.yaml device_tracker: platform: actiontec @@ -49,8 +49,6 @@ _LOGGER = logging.getLogger(__name__) _LEASES_REGEX = re.compile(r'(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') - - # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DD-WRT scanner. """ @@ -63,7 +61,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None - class ActiontecDeviceScanner(object): """ This class queries a an actiontec router for connected devices. Adapted from DD-WRT scanner. @@ -122,11 +119,11 @@ class ActiontecDeviceScanner(object): telnet.write((self.username + '\n').encode('ascii')) telnet.read_until(b'Password: ') telnet.write((self.password + '\n').encode('ascii')) - prompt_string = telnet.read_until(b'Wireless Broadband Router> ').split(b'\n')[-1] + prompt = telnet.read_until(b'Wireless Broadband Router> ').split(b'\n')[-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write('\n'.encode('ascii')) - skip_line = telnet.read_until(prompt_string).split(b'\n')[1:-1] - leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1] + _=telnet.read_until(prompt).split(b'\n')[1:-1] + leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] telnet.write('exit\n'.encode('ascii')) except EOFError: _LOGGER.exception("Unexpected response from router") @@ -143,4 +140,3 @@ class ActiontecDeviceScanner(object): devices.append(match.group('mac')) return devices - From b84d5760ebb68395fb9e1d60864530077ffc3407 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 18 Aug 2015 17:03:13 -0400 Subject: [PATCH 04/10] add to .coveragerc and try again to fix warnings. --- .coveragerc | 1 + homeassistant/components/device_tracker/actiontec.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 7c0421c384a..680cd996d28 100644 --- a/.coveragerc +++ b/.coveragerc @@ -30,6 +30,7 @@ omit = homeassistant/components/browser.py homeassistant/components/camera/* + homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index d8d60be3ca8..ecdb0870770 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -49,6 +49,7 @@ _LOGGER = logging.getLogger(__name__) _LEASES_REGEX = re.compile(r'(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') + # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DD-WRT scanner. """ @@ -61,6 +62,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None + class ActiontecDeviceScanner(object): """ This class queries a an actiontec router for connected devices. Adapted from DD-WRT scanner. @@ -119,10 +121,11 @@ class ActiontecDeviceScanner(object): telnet.write((self.username + '\n').encode('ascii')) telnet.read_until(b'Password: ') telnet.write((self.password + '\n').encode('ascii')) - prompt = telnet.read_until(b'Wireless Broadband Router> ').split(b'\n')[-1] + prompt = telnet.read_until(b'Wireless Broadband Router> ', + '').split(b'\n')[-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write('\n'.encode('ascii')) - _=telnet.read_until(prompt).split(b'\n')[1:-1] + telnet.read_until(prompt) leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] telnet.write('exit\n'.encode('ascii')) except EOFError: From 6a830e3b908a73fbef8a2080c9d4c630d8d13c30 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 18 Aug 2015 17:14:26 -0400 Subject: [PATCH 05/10] fix for flake8 --- homeassistant/components/device_tracker/actiontec.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index ecdb0870770..a7f83e2acb5 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -8,7 +8,7 @@ This device tracker needs telnet to be enabled on the router. Configuration: -To use the Actiontec tracker you will need to add something like the +To use the Actiontec tracker you will need to add something like the following to your config/configuration.yaml device_tracker: @@ -122,7 +122,7 @@ class ActiontecDeviceScanner(object): telnet.read_until(b'Password: ') telnet.write((self.password + '\n').encode('ascii')) prompt = telnet.read_until(b'Wireless Broadband Router> ', - '').split(b'\n')[-1] + '').split(b'\n')[-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write('\n'.encode('ascii')) telnet.read_until(prompt) @@ -139,7 +139,7 @@ class ActiontecDeviceScanner(object): devices = [] for lease in leases_result: match = _LEASES_REGEX.search(lease.decode('utf-8')) - if match is not None: + if match is not None: devices.append(match.group('mac')) return devices From ca515615b923dcbf5bfcce8e61d755783921c381 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Wed, 19 Aug 2015 09:52:47 -0400 Subject: [PATCH 06/10] add support for recording decive name as ip address --- .../components/device_tracker/actiontec.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index a7f83e2acb5..35c7868760e 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -47,7 +47,9 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -_LEASES_REGEX = re.compile(r'(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') +_LEASES_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))') # pylint: disable=unused-argument @@ -99,8 +101,8 @@ class ActiontecDeviceScanner(object): @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): - """ Ensures the information from the ASUSWRT router is up to date. - Returns boolean if scanning successful. """ + """ Ensures the information from the Actiontec MI424WR router is up + to date. Returns boolean if scanning successful. """ if not self.success_init: return False @@ -109,12 +111,12 @@ class ActiontecDeviceScanner(object): data = self.get_actiontec_data() if not data: return False - - self.last_results = data + active_clients = [client for client in data.values()] + self.last_results = active_clients return True def get_actiontec_data(self): - """ Retrieve data from ASUSWRT and return parsed result. """ + """ Retrieve data from Actiontec MI424WR and return parsed result. """ try: telnet = telnetlib.Telnet(self.host) telnet.read_until(b'Username: ') @@ -136,10 +138,12 @@ class ActiontecDeviceScanner(object): " is telnet enabled?") return - devices = [] + devices = {} for lease in leases_result: match = _LEASES_REGEX.search(lease.decode('utf-8')) if match is not None: - devices.append(match.group('mac')) - + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper() + } return devices From 63e441c73fa06e3b7be13c6143ba2cba262fb46c Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Thu, 20 Aug 2015 10:35:01 -0400 Subject: [PATCH 07/10] fix scan_devices --- homeassistant/components/device_tracker/actiontec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 35c7868760e..41e209ac506 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -88,7 +88,7 @@ class ActiontecDeviceScanner(object): list containing found device ids. """ self._update_info() - return self.last_results + return [client['mac'] for client in self.last_results] def get_device_name(self, device): """ Returns the name of the given device or None if we don't know. """ From 93bd238be5c1754e431360435d09aa7b845e7542 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Mon, 24 Aug 2015 00:27:26 -0400 Subject: [PATCH 08/10] add return None for get_actiontec_data --- homeassistant/components/device_tracker/actiontec.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 41e209ac506..d9e89ab7157 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -130,6 +130,7 @@ class ActiontecDeviceScanner(object): telnet.read_until(prompt) leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] telnet.write('exit\n'.encode('ascii')) + return None except EOFError: _LOGGER.exception("Unexpected response from router") return From 047b4abd822e8ad65b45d2fa66d531bcc254d221 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 25 Aug 2015 09:39:00 -0400 Subject: [PATCH 09/10] Fix get_device_name and get_actiontec_data --- homeassistant/components/device_tracker/actiontec.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index d9e89ab7157..bbf20b09232 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -95,8 +95,8 @@ class ActiontecDeviceScanner(object): if not self.last_results: return None for client in self.last_results: - if client == device: - return client + if client['mac'] == device: + return client['ip'] return None @Throttle(MIN_TIME_BETWEEN_SCANS) @@ -130,14 +130,13 @@ class ActiontecDeviceScanner(object): telnet.read_until(prompt) leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] telnet.write('exit\n'.encode('ascii')) - return None except EOFError: _LOGGER.exception("Unexpected response from router") return except ConnectionRefusedError: _LOGGER.exception("Connection refused by router," + " is telnet enabled?") - return + return None devices = {} for lease in leases_result: From bc5a7564b16d09beb0a5cd7b8bb3afddf8662a0e Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Tue, 25 Aug 2015 10:09:47 -0400 Subject: [PATCH 10/10] fix formatting --- homeassistant/components/device_tracker/actiontec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index bbf20b09232..06956475ba0 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -123,8 +123,8 @@ class ActiontecDeviceScanner(object): telnet.write((self.username + '\n').encode('ascii')) telnet.read_until(b'Password: ') telnet.write((self.password + '\n').encode('ascii')) - prompt = telnet.read_until(b'Wireless Broadband Router> ', - '').split(b'\n')[-1] + prompt = telnet.read_until( + b'Wireless Broadband Router> ').split(b'\n')[-1] telnet.write('firewall mac_cache_dump\n'.encode('ascii')) telnet.write('\n'.encode('ascii')) telnet.read_until(prompt)