diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 48933f8c87c..3bd62ba535e 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -83,6 +83,16 @@ def from_config_file(config_path):
get_opt('device_tracker.netgear', 'username'),
get_opt('device_tracker.netgear', 'password'))
+ elif has_section('device_tracker.luci'):
+ device_tracker = load_module('device_tracker')
+
+ dev_scan_name = "Luci"
+
+ dev_scan = device_tracker.LuciDeviceScanner(
+ get_opt('device_tracker.luci', 'host'),
+ get_opt('device_tracker.luci', 'username'),
+ get_opt('device_tracker.luci', 'password'))
+
except configparser.NoOptionError:
# If one of the options didn't exist
logger.exception(("Error initializing {}DeviceScanner, "
diff --git a/homeassistant/components/device_tracker.py b/homeassistant/components/device_tracker.py
index 3c57b85f2c1..445c74b245f 100644
--- a/homeassistant/components/device_tracker.py
+++ b/homeassistant/components/device_tracker.py
@@ -456,3 +456,151 @@ class NetgearDeviceScanner(object):
else:
return
+
+class LuciDeviceScanner(object):
+ """ This class queries a wireless router running OpenWrt firmware
+ for connected devices. Adapted from Tomato scanner.
+
+ # opkg install luci-mod-rpc
+ for this to work on the router.
+
+ The API is described here:
+ http://luci.subsignal.org/trac/wiki/Documentation/JsonRpcHowTo
+
+ (Currently, we do only wifi iwscan, and no DHCP lease access.)
+ """
+
+ def __init__(self, host, username, password):
+ self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);")
+
+ self.logger = logging.getLogger(__name__)
+ self.lock = threading.Lock()
+
+ self.date_updated = None
+ self.last_results = {}
+
+ self.token = self.get_token(host, username, password)
+ self.host = host
+
+ self.mac2name = None
+ self.success_init = self.token
+
+ def get_token(self, host, username, password):
+ data = json.dumps({'method': 'login',
+ 'params': [username, password]})
+ try:
+ r = requests.post('http://{}/cgi-bin/luci/rpc/auth'.format(host), data=data, timeout=3)
+ if r.status_code == 200:
+ token = r.json()['result']
+ self.logger.info('Authenticated')
+ return token
+ elif r.status_code == 401:
+ # Authentication error
+ self.logger.exception(
+ "Failed to authenticate, "
+ "please check your username and password")
+ return
+ else:
+ self.logger.error("Invalid response: %s" % r)
+ except requests.exceptions.Timeout:
+ self.logger.exception("Connection to the router timed out")
+ except ValueError:
+ # If json decoder could not parse the response
+ self.logger.exception("Failed to parse response from router")
+
+ 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 self.mac2name is None:
+ try:
+ data = json.dumps({'method': 'get_all',
+ 'params': ['dhcp']})
+
+ r = requests.post('http://{}/cgi-bin/luci/rpc/uci'.format(self.host), params={'auth': self.token}, data=data, timeout=3)
+
+ # Calling and parsing the Luci api here. We only need the
+ # wldev and dhcpd_lease values. For API description see:
+ # http://paulusschoutsen.nl/
+ # blog/2013/10/tomato-api-documentation/
+ if r.status_code == 200:
+ self.mac2name = dict(map(lambda x:(x['mac'], x['name']),
+ filter(lambda x:x['.type'] == 'host' and 'mac' in x and 'name' in x,
+ r.json()['result'].values())))
+ # Passthrough
+ else:
+ self.logger.error("Invalid response: %s" % r)
+ return
+
+ 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(
+ "Connection to the router timed out")
+ return
+
+ except ValueError:
+ # If json decoder could not parse the response
+ self.logger.exception(
+ "Failed to parse response from router")
+
+ return
+ return self.mac2name.get(device, None)
+
+ def _update_info(self):
+ """ Ensures the information from the Luci router is up to date.
+ Returns boolean if scanning successful. """
+ if not self.success_init:
+ return False
+ with self.lock:
+
+ # 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("Checking ARP")
+
+ try:
+ data = json.dumps({'method': 'net.arptable',
+ 'params': []})
+
+ r = requests.post('http://{}/cgi-bin/luci/rpc/sys'.format(self.host), params={'auth': self.token}, data=data, timeout=3)
+
+ # Calling and parsing the Luci api here. We only need the
+ # wldev and dhcpd_lease values. For API description see:
+ # http://paulusschoutsen.nl/
+ # blog/2013/10/tomato-api-documentation/
+ if r.status_code == 200:
+ self.last_results = list(map(lambda x:x['HW address'], r.json()['result']))
+ self.date_updated = datetime.now()
+ return True
+
+ else:
+ self.logger.error("Invalid response: %s" % r)
+ 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(
+ "Connection to the router timed out")
+
+ return False
+
+ except ValueError:
+ # If json decoder could not parse the response
+ self.logger.exception(
+ "Failed to parse response from router")
+
+ return False
+
+ return True
+