diff --git a/README.md b/README.md index 10170b7b5a4..019cc6bdc06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,13 @@ It is currently able to do the following things: ![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/states.png) -It currently works with any wireless router running [Tomato firmware](http://www.polarcloud.com/tomato) in combination with [Philips Hue](http://meethue.com) and the [Google Chromecast](http://www.google.com/intl/en/chrome/devices/chromecast). The system is built modular so support for other wireless routers, other devices or actions can be implemented easily. +Current compatible devices: + * Wireless router running [Tomato firmware](http://www.polarcloud.com/tomato) + * Netgear wireless routers (tested with R6300) + * [Philips Hue](http://meethue.com) + * [Google Chromecast](http://www.google.com/intl/en/chrome/devices/chromecast) + +The system is built modular so support for other wireless routers, other devices or actions can be implemented easily. Installation instructions ------------------------- diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 57953da45a1..b3e11be84ec 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -42,12 +42,24 @@ def from_config_file(config_path): statusses.append(("Device Scanner - Tomato", device_scanner.success_init)) - if not device_scanner.success_init: - device_scanner = None + elif config.has_option('netgear', 'host') and \ + config.has_option('netgear', 'username') and \ + config.has_option('netgear', 'password'): + + device_scanner = device.NetgearDeviceScanner( + config.get('netgear', 'host'), + config.get('netgear', 'username'), + config.get('netgear', 'password')) + + statusses.append(("Device Scanner - Netgear", + device_scanner.success_init)) else: device_scanner = None + if device_scanner and not device_scanner.success_init: + device_scanner = None + # Device Tracker if device_scanner: device.DeviceTracker(bus, statemachine, device_scanner) diff --git a/homeassistant/components/device.py b/homeassistant/components/device.py index 7c1dd1f3917..93d307ddf2e 100644 --- a/homeassistant/components/device.py +++ b/homeassistant/components/device.py @@ -33,7 +33,7 @@ STATE_HOME = 'device_home' TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=1) # Return cached results if last scan was less then this time ago -TOMATO_MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) # Filename to save known devices to KNOWN_DEVICES_FILE = "known_devices.csv" @@ -324,7 +324,7 @@ class TomatoDeviceScanner(object): # if date_updated is None or the date is too old we scan for new data if (not self.date_updated or datetime.now() - self.date_updated > - TOMATO_MIN_TIME_BETWEEN_SCANS): + MIN_TIME_BETWEEN_SCANS): self.logger.info("Tomato:Scanning") @@ -389,3 +389,127 @@ class TomatoDeviceScanner(object): self.lock.release() return True + + +class NetgearDeviceScanner(object): + """ This class queries a Netgear wireless router. + + Tested with the Netgear R6300. + """ + + def __init__(self, host, username, password): + self.req = requests.Request('GET', + 'http://{}/DEV_device.htm'.format(host), + auth=requests.auth.HTTPBasicAuth( + username, password)).prepare() + + self.req_main_page = requests.Request('GET', + 'http://{}/start.htm'.format(host), + auth=requests.auth.HTTPBasicAuth( + username, password)).prepare() + + + self.parse_api_pattern = re.compile(r'ttext">(.*?)<') + + self.logger = logging.getLogger(__name__) + self.lock = threading.Lock() + + self.date_updated = None + self.last_results = [] + + self.success_init = self._update_info() + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + + return [item[2] for item in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + + # Make sure there are results + if not self.date_updated: + self._update_info() + + filter_named = [item[1] for item in self.last_results + if item[2] == device] + + if len(filter_named) == 0 or filter_named[0] == "--": + return None + else: + return filter_named[0] + + def _update_info(self): + """ Retrieves latest information from the Netgear router. + Returns boolean if scanning successful. """ + + self.lock.acquire() + + # if date_updated is None or the date is too old we scan for new data + if (not self.date_updated or datetime.now() - self.date_updated > + MIN_TIME_BETWEEN_SCANS): + + self.logger.info("Netgear:Scanning") + + try: + response = requests.Session().send(self.req, timeout=3) + + # Netgear likes us to hit the main page first + # So first 401 we get we will first hit main page + if response.status_code == 401: + response = requests.Session().send(self.req_main_page, timeout=3) + response = requests.Session().send(self.req, timeout=3) + + if response.status_code == 200: + + entries = self.parse_api_pattern.findall(response.text) + + if len(entries) % 3 != 0: + self.logger.error("Netgear:Failed to parse response") + return False + + else: + self.last_results = [entries[i:i+3] for i + in xrange(0, len(entries), 3)] + + self.date_updated = datetime.now() + + return True + + elif response.status_code == 401: + # Authentication error + self.logger.exception(( + "Netgear:Failed to authenticate, " + "please check your username and password")) + + return False + + except requests.exceptions.ConnectionError: + # We get this if we could not connect to the router or + # an invalid http_id was supplied + self.logger.exception(( + "Netgear:Failed to connect to the router" + " or invalid http_id supplied")) + + return False + + except requests.exceptions.Timeout: + # We get this if we could not connect to the router or + # an invalid http_id was supplied + self.logger.exception( + "Netgear:Connection to the router timed out") + + return False + + finally: + self.lock.release() + + else: + # We acquired the lock before the IF check, + # release it before we return True + self.lock.release() + + return True