""" Support for Tomato routers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.tomato/ """ import json import logging import re 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, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, CONF_PASSWORD, CONF_USERNAME) CONF_HTTP_ID = 'http_id' _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): vol.Any( cv.boolean, cv.isfile), vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_HTTP_ID): cv.string }) def get_scanner(hass, config): """Validate the configuration and returns a Tomato scanner.""" return TomatoDeviceScanner(config[DOMAIN]) class TomatoDeviceScanner(DeviceScanner): """This class queries a wireless router running Tomato firmware.""" def __init__(self, config): """Initialize the scanner.""" host, http_id = config[CONF_HOST], config[CONF_HTTP_ID] port = config.get(CONF_PORT) username, password = config[CONF_USERNAME], config[CONF_PASSWORD] self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL] if port is None: port = 443 if self.ssl else 80 self.req = requests.Request( 'POST', 'http{}://{}:{}/update.cgi'.format( "s" if self.ssl else "", host, port ), data={'_http_id': http_id, 'exec': 'devlist'}, auth=requests.auth.HTTPBasicAuth(username, password)).prepare() self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") self.last_results = {"wldev": [], "dhcpd_lease": []} self.success_init = self._update_tomato_info() def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_tomato_info() return [item[1] for item in self.last_results['wldev']] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" filter_named = [item[0] for item in self.last_results['dhcpd_lease'] if item[2] == device] if not filter_named or not filter_named[0]: return None return filter_named[0] def _update_tomato_info(self): """Ensure the information from the Tomato router is up to date. Return boolean if scanning successful. """ _LOGGER.info("Scanning") try: if self.ssl: response = requests.Session().send(self.req, timeout=3, verify=self.verify_ssl) else: response = requests.Session().send(self.req, timeout=3) # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. if response.status_code == 200: for param, value in \ self.parse_api_pattern.findall(response.text): if param == 'wldev' or param == 'dhcpd_lease': self.last_results[param] = \ json.loads(value.replace("'", '"')) return True elif response.status_code == 401: # Authentication error _LOGGER.exception(( "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. _LOGGER.exception("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. _LOGGER.exception("Connection to the router timed out") return False except ValueError: # If JSON decoder could not parse the response. _LOGGER.exception("Failed to parse response from router") return False