From c8c38e498a54d96c7c63f219afbe33332fbf9db7 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 03:51:33 +1100 Subject: [PATCH 01/10] Added a device tracker for dd-wrt routers --- .../components/device_tracker/ddwrt.py | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 homeassistant/components/device_tracker/ddwrt.py diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py new file mode 100644 index 00000000000..a055d2d496f --- /dev/null +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -0,0 +1,159 @@ +""" Supports scanning a DD-WRT router. """ +import logging +import json +from datetime import timedelta +import re +import threading +import requests + +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__) + + +def get_scanner(hass, config): + """ Validates config and returns a DdWrt scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = DdWrtDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +# pylint: disable=too-many-instance-attributes +class DdWrtDeviceScanner(object): + """ This class queries a wireless router running DD-WRT firmware + for connected devices. Adapted from Tomato 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 = {} + + self.mac2name = None + + #test the router is accessible + url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + 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. """ + + with self.lock: + # if not initialised and not already scanned and not found + if self.mac2name is None or device not in self.mac2name: + url = 'http://{}/Status_Lan.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + + if not data: + return + + dhcp_leases = data.get('dhcp_leases', None) + if dhcp_leases: + + # remove leading and trailing single quotes + cleaned_str = dhcp_leases.strip().strip('"') + elements = cleaned_str.split('","') + num_clients = int(len(elements)/5) + self.mac2name = {} + for x in range(0, num_clients): + # this is stupid but the data is a single array + # every 5 elements represents one hosts, the MAC + # is the third element and the name is the first + mac_index = (x * 5) + 2 + if mac_index < len(elements): + self.mac2name[elements[mac_index]] = elements[x * 5] + + return self.mac2name.get(device, None) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the DdWrt router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + + url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) + + if not data: + return False + + if data: + self.last_results = [] + active_clients = data.get('active_wireless', None) + if active_clients: + # This is really lame, instead of using JSON the ddwrt UI uses + # it's own data format for some reason and then regex's out + # values so I guess I have to do the same, LAME!!! + + # remove leading and trailing single quotes + clean_str = active_clients.strip().strip("'") + elements = clean_str.split("','") + + num_clients = int(len(elements)/9) + for x in range(0, num_clients): + # get every 9th element which is the MAC address + index = x * 9 + if index < len(elements): + self.last_results.append(elements[index]) + + return True + + return False + + def get_ddwrt_data(self, url): + """ Retrieve data from DD-WRT and return parsed result """ + try: + response = requests.get(url, auth=(self.username, + self.password), timeout=4) + except requests.exceptions.Timeout: + _LOGGER.exception("Connection to the router timed out") + return + if response.status_code == 200: + return _parse_ddwrt_response(response.text) + elif res.status_code == 401: + # Authentication error + _LOGGER.exception( + "Failed to authenticate, " + "please check your username and password") + return + else: + _LOGGER.error("Invalid response from ddwrt: %s", res) + + +def _parse_ddwrt_response(data_str): + """ Parse the awful DD-WRT data format, why didn't they use JSON????. + This code is a python version of how they are parsing in the JS """ + data = {} + pattern = re.compile(r'\{(\w+)::([^\}]*)\}') + for (key, val) in re.findall(pattern, data_str): + data[key] = val + + return data \ No newline at end of file From 7e6af57186cb702a32cf5fa87557c26fa6ef0c60 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:29:45 +1100 Subject: [PATCH 02/10] FIxed some linting issues --- .../components/device_tracker/ddwrt.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index a055d2d496f..baebe484a27 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -1,6 +1,5 @@ """ Supports scanning a DD-WRT router. """ import logging -import json from datetime import timedelta import re import threading @@ -38,7 +37,7 @@ class DdWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.password = config[CONF_PASSWORD] self.lock = threading.Lock() @@ -73,20 +72,20 @@ class DdWrtDeviceScanner(object): dhcp_leases = data.get('dhcp_leases', None) if dhcp_leases: - # remove leading and trailing single quotes cleaned_str = dhcp_leases.strip().strip('"') - elements = cleaned_str.split('","') - num_clients = int(len(elements)/5) - self.mac2name = {} - for x in range(0, num_clients): + elements = cleaned_str.split('","') + num_clients = int(len(elements)/5) + self.mac2name = {} + for idx in range(0, num_clients): # this is stupid but the data is a single array # every 5 elements represents one hosts, the MAC # is the third element and the name is the first - mac_index = (x * 5) + 2 - if mac_index < len(elements): - self.mac2name[elements[mac_index]] = elements[x * 5] - + mac_index = (idx * 5) + 2 + if mac_index < len(elements): + mac = elements[mac_index] + self.mac2name[mac] = elements[idx * 5] + return self.mac2name.get(device, None) @Throttle(MIN_TIME_BETWEEN_SCANS) @@ -109,21 +108,22 @@ class DdWrtDeviceScanner(object): self.last_results = [] active_clients = data.get('active_wireless', None) if active_clients: - # This is really lame, instead of using JSON the ddwrt UI uses - # it's own data format for some reason and then regex's out - # values so I guess I have to do the same, LAME!!! - + # This is really lame, instead of using JSON the ddwrt UI + # uses it's own data format for some reason and then + # regex's out values so I guess I have to do the same, + # LAME!!! + # remove leading and trailing single quotes clean_str = active_clients.strip().strip("'") elements = clean_str.split("','") - - num_clients = int(len(elements)/9) - for x in range(0, num_clients): + + num_clients = int(len(elements)/9) + for idx in range(0, num_clients): # get every 9th element which is the MAC address - index = x * 9 - if index < len(elements): + index = idx * 9 + if index < len(elements): self.last_results.append(elements[index]) - + return True return False @@ -131,29 +131,29 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=(self.username, + response = requests.get(url, auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return if response.status_code == 200: return _parse_ddwrt_response(response.text) - elif res.status_code == 401: + elif response.status_code == 401: # Authentication error _LOGGER.exception( "Failed to authenticate, " "please check your username and password") return else: - _LOGGER.error("Invalid response from ddwrt: %s", res) + _LOGGER.error("Invalid response from ddwrt: %s", response) def _parse_ddwrt_response(data_str): - """ Parse the awful DD-WRT data format, why didn't they use JSON????. + """ Parse the awful DD-WRT data format, why didn't they use JSON????. This code is a python version of how they are parsing in the JS """ data = {} pattern = re.compile(r'\{(\w+)::([^\}]*)\}') for (key, val) in re.findall(pattern, data_str): data[key] = val - return data \ No newline at end of file + return data From fc07032d35bcccd217d02149a96d85a3338a0e31 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:43:41 +1100 Subject: [PATCH 03/10] Fixed some code formatting and added dd-wrt to the readme --- homeassistant/components/device_tracker/ddwrt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index baebe484a27..20db671fbc7 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -16,6 +16,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DdWrt scanner. """ if not validate_config(config, @@ -131,8 +132,10 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=(self.username, - self.password), timeout=4) + response = requests.get(url, auth=( + self.username, + self.password), + timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return From 05239c26f9ecfaa6a671e822ed24c2a230e53f18 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:50:24 +1100 Subject: [PATCH 04/10] Forgot to add README.md file changes to last commit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f848d551adb..43c4d58a33d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: - * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com)) + * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index)) * Track and control [Philips Hue](http://meethue.com) lights * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) From a9ce12be34cb04299cd63bbdeb9075c3307592e5 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 18:59:12 +1100 Subject: [PATCH 05/10] Fixed travis CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 20db671fbc7..fa7d332feb8 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -132,9 +132,9 @@ class DdWrtDeviceScanner(object): def get_ddwrt_data(self, url): """ Retrieve data from DD-WRT and return parsed result """ try: - response = requests.get(url, auth=( - self.username, - self.password), + response = requests.get( + url, + auth=(self.username, self.password), timeout=4) except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") From a959c48708caaf7f22a8cd6a4e112aa37dd5fc82 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 28 Mar 2015 19:17:51 +1100 Subject: [PATCH 06/10] Fixed travis another CI indenting error --- homeassistant/components/device_tracker/ddwrt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index fa7d332feb8..3d6af407ded 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -46,7 +46,7 @@ class DdWrtDeviceScanner(object): self.mac2name = None - #test the router is accessible + # Test the router is accessible url = 'http://{}/Status_Wireless.live.asp'.format(self.host) data = self.get_ddwrt_data(url) self.success_init = data is not None @@ -142,7 +142,7 @@ class DdWrtDeviceScanner(object): if response.status_code == 200: return _parse_ddwrt_response(response.text) elif response.status_code == 401: - # Authentication error + # Authentication error _LOGGER.exception( "Failed to authenticate, " "please check your username and password") From 242c143c85041c6f7253165c670e073ff6383ebf Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 29 Mar 2015 11:30:04 +1100 Subject: [PATCH 07/10] refactored ddwrt data format parsong code --- homeassistant/components/device_tracker/ddwrt.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 3d6af407ded..1dd0eb6a436 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -154,9 +154,6 @@ class DdWrtDeviceScanner(object): def _parse_ddwrt_response(data_str): """ Parse the awful DD-WRT data format, why didn't they use JSON????. This code is a python version of how they are parsing in the JS """ - data = {} - pattern = re.compile(r'\{(\w+)::([^\}]*)\}') - for (key, val) in re.findall(pattern, data_str): - data[key] = val - - return data + return { + key: val for key, val in re.compile(r'\{(\w+)::([^\}]*)\}') + .findall(data_str)} From fda44cdbf76cc3d062be6f5a8757d68ba32af209 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 29 Mar 2015 11:40:21 +1100 Subject: [PATCH 08/10] Moved compiled regex to a constant for efficiency --- homeassistant/components/device_tracker/ddwrt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 1dd0eb6a436..c17aca201d8 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -15,6 +15,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +_DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') # pylint: disable=unused-argument def get_scanner(hass, config): @@ -155,5 +156,5 @@ def _parse_ddwrt_response(data_str): """ Parse the awful DD-WRT data format, why didn't they use JSON????. This code is a python version of how they are parsing in the JS """ return { - key: val for key, val in re.compile(r'\{(\w+)::([^\}]*)\}') + key: val for key, val in _DDWRT_DATA_REGEX .findall(data_str)} From 007d0d9ce960fbe32fbf4c914ad797a82fc46748 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 29 Mar 2015 11:44:02 +1100 Subject: [PATCH 09/10] Added ddwrt component to coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index cccd815df2d..f41886aaa0f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -27,6 +27,7 @@ omit = homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py + homeassistant/components/device_tracker/ddwrt.py [report] From 0b6d260fa6190271bcbc51dd27a19b8b6b61bde2 Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sun, 29 Mar 2015 11:49:07 +1100 Subject: [PATCH 10/10] fixed flake8 blank lines error --- homeassistant/components/device_tracker/ddwrt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index c17aca201d8..3d63c49209c 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -17,6 +17,7 @@ _LOGGER = logging.getLogger(__name__) _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}') + # pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DdWrt scanner. """