diff --git a/.coveragerc b/.coveragerc index 3e6b71c00e7..390db0f0157 100644 --- a/.coveragerc +++ b/.coveragerc @@ -245,6 +245,7 @@ omit = homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/linksys_ap.py + homeassistant/components/device_tracker/linksys_smart.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/mikrotik.py homeassistant/components/device_tracker/netgear.py diff --git a/homeassistant/components/device_tracker/linksys_smart.py b/homeassistant/components/device_tracker/linksys_smart.py new file mode 100644 index 00000000000..2d7fbfea33c --- /dev/null +++ b/homeassistant/components/device_tracker/linksys_smart.py @@ -0,0 +1,110 @@ +"""Support for Linksys Smart Wifi routers.""" +import logging +import threading +from datetime import timedelta + +import requests +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 +from homeassistant.util import Throttle + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +DEFAULT_TIMEOUT = 10 + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, +}) + + +def get_scanner(hass, config): + """Validate the configuration and return a Linksys AP scanner.""" + try: + return LinksysSmartWifiDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None + + +class LinksysSmartWifiDeviceScanner(DeviceScanner): + """This class queries a Linksys Access Point.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + + self.lock = threading.Lock() + self.last_results = {} + + # Check if the access point is accessible + response = self._make_request() + if not response.status_code == 200: + raise ConnectionError("Cannot connect to Linksys Access Point") + + def scan_devices(self): + """Scan for new devices and return a list with device IDs (MACs).""" + self._update_info() + + return self.last_results.keys() + + def get_device_name(self, mac): + """Return the name (if known) of the device.""" + return self.last_results.get(mac) + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Check for connected devices.""" + with self.lock: + _LOGGER.info("Checking Linksys Smart Wifi") + + self.last_results = {} + response = self._make_request() + if response.status_code != 200: + _LOGGER.error( + "Got HTTP status code %d when getting device list", + response.status_code) + return False + try: + data = response.json() + result = data["responses"][0] + devices = result["output"]["devices"] + for device in devices: + macs = device["knownMACAddresses"] + if not macs: + _LOGGER.warning( + "Skipping device without known MAC address") + continue + mac = macs[-1] + connections = device["connections"] + if not connections: + _LOGGER.debug("Device %s is not connected", mac) + continue + name = device["friendlyName"] + properties = device["properties"] + for prop in properties: + if prop["name"] == "userDeviceName": + name = prop["value"] + _LOGGER.debug("Device %s is connected", mac) + self.last_results[mac] = name + except (KeyError, IndexError): + _LOGGER.exception("Router returned unexpected response") + return False + return True + + def _make_request(self): + # Weirdly enough, this doesn't seem to require authentication + data = [{ + "request": { + "sinceRevision": 0 + }, + "action": "http://linksys.com/jnap/devicelist/GetDevices" + }] + headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"} + return requests.post('http://{}/JNAP/'.format(self.host), + timeout=DEFAULT_TIMEOUT, + headers=headers, + json=data)