From 635e5c8eba39611fc94f68c50ea8c08682e61ac8 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Sun, 21 Aug 2016 13:29:13 -0400 Subject: [PATCH] Add voluptuous to ecobee, speedtest.net, fast.com, actiontec, forecast.io (#2872) * add voluptuous * fixes for comments * str to cv.string --- .../components/device_tracker/actiontec.py | 255 +++++++++--------- homeassistant/components/ecobee.py | 13 +- homeassistant/components/sensor/fastdotcom.py | 27 +- homeassistant/components/sensor/forecast.py | 34 ++- homeassistant/components/sensor/speedtest.py | 27 +- 5 files changed, 201 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 5de139b9e1f..a4804848f4a 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -1,126 +1,129 @@ -""" -Support for Actiontec MI424WR (Verizon FIOS) routers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.actiontec/ -""" -import logging -import re -import telnetlib -import threading -from collections import namedtuple -from datetime import timedelta - -import homeassistant.util.dt as dt_util -from homeassistant.components.device_tracker import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config -from homeassistant.util import Throttle - -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - -_LOGGER = logging.getLogger(__name__) - -_LEASES_REGEX = re.compile( - r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + - r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' + - r'\svalid\sfor:\s(?P(-?\d+))' + - r'\ssec') - - -# pylint: disable=unused-argument -def get_scanner(hass, config): - """Validate the configuration and return an Actiontec scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = ActiontecDeviceScanner(config[DOMAIN]) - return scanner if scanner.success_init else None - -Device = namedtuple("Device", ["mac", "ip", "last_update"]) - - -class ActiontecDeviceScanner(object): - """This class queries a an actiontec router for connected devices.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() - self.last_results = [] - data = self.get_actiontec_data() - self.success_init = data is not None - _LOGGER.info("actiontec scanner initialized") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return [client.mac for client in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - if not self.last_results: - return None - for client in self.last_results: - if client.mac == device: - return client.ip - return None - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def _update_info(self): - """Ensure the information from the router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Scanning") - if not self.success_init: - return False - - with self.lock: - now = dt_util.now() - actiontec_data = self.get_actiontec_data() - if not actiontec_data: - return False - self.last_results = [Device(data['mac'], name, now) - for name, data in actiontec_data.items() - if data['timevalid'] > -60] - _LOGGER.info("actiontec scan successful") - return True - - def get_actiontec_data(self): - """Retrieve data from Actiontec MI424WR and return parsed result.""" - try: - telnet = telnetlib.Telnet(self.host) - telnet.read_until(b'Username: ') - telnet.write((self.username + '\n').encode('ascii')) - telnet.read_until(b'Password: ') - telnet.write((self.password + '\n').encode('ascii')) - prompt = telnet.read_until( - b'Wireless Broadband Router> ').split(b'\n')[-1] - telnet.write('firewall mac_cache_dump\n'.encode('ascii')) - telnet.write('\n'.encode('ascii')) - telnet.read_until(prompt) - leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] - telnet.write('exit\n'.encode('ascii')) - except EOFError: - _LOGGER.exception("Unexpected response from router") - return - except ConnectionRefusedError: - _LOGGER.exception("Connection refused by router," + - " is telnet enabled?") - return None - - devices = {} - for lease in leases_result: - match = _LEASES_REGEX.search(lease.decode('utf-8')) - if match is not None: - devices[match.group('ip')] = { - 'ip': match.group('ip'), - 'mac': match.group('mac').upper(), - 'timevalid': int(match.group('timevalid')) - } - return devices +""" +Support for Actiontec MI424WR (Verizon FIOS) routers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.actiontec/ +""" +import logging +import re +import telnetlib +import threading +from collections import namedtuple +from datetime import timedelta +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.util import Throttle + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'\smac:\s(?P([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' + + r'\svalid\sfor:\s(?P(-?\d+))' + + r'\ssec') + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + + +# pylint: disable=unused-argument +def get_scanner(hass, config): + """Validate the configuration and return an Actiontec scanner.""" + scanner = ActiontecDeviceScanner(config[DOMAIN]) + return scanner if scanner.success_init else None + +Device = namedtuple("Device", ["mac", "ip", "last_update"]) + + +class ActiontecDeviceScanner(object): + """This class queries a an actiontec router for connected devices.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + self.lock = threading.Lock() + self.last_results = [] + data = self.get_actiontec_data() + self.success_init = data is not None + _LOGGER.info("actiontec scanner initialized") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client.mac for client in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if not self.last_results: + return None + for client in self.last_results: + if client.mac == device: + return client.ip + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Ensure the information from the router is up to date. + + Return boolean if scanning successful. + """ + _LOGGER.info("Scanning") + if not self.success_init: + return False + + with self.lock: + now = dt_util.now() + actiontec_data = self.get_actiontec_data() + if not actiontec_data: + return False + self.last_results = [Device(data['mac'], name, now) + for name, data in actiontec_data.items() + if data['timevalid'] > -60] + _LOGGER.info("actiontec scan successful") + return True + + def get_actiontec_data(self): + """Retrieve data from Actiontec MI424WR and return parsed result.""" + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'Username: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt = telnet.read_until( + b'Wireless Broadband Router> ').split(b'\n')[-1] + telnet.write('firewall mac_cache_dump\n'.encode('ascii')) + telnet.write('\n'.encode('ascii')) + telnet.read_until(prompt) + leases_result = telnet.read_until(prompt).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") + return None + + devices = {} + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + if match is not None: + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper(), + 'timevalid': int(match.group('timevalid')) + } + return devices diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index d684b903d0c..702c7fd6304 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -7,7 +7,9 @@ https://home-assistant.io/components/ecobee/ import logging import os from datetime import timedelta +import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.const import CONF_API_KEY from homeassistant.loader import get_component @@ -15,12 +17,19 @@ from homeassistant.util import Throttle DOMAIN = "ecobee" NETWORK = None -HOLD_TEMP = 'hold_temp' +CONF_HOLD_TEMP = 'hold_temp' REQUIREMENTS = [ 'https://github.com/nkgilley/python-ecobee-api/archive/' '4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6'] +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean + }) +}, extra=vol.ALLOW_EXTRA) + _LOGGER = logging.getLogger(__name__) ECOBEE_CONFIG_FILE = 'ecobee.conf' @@ -67,7 +76,7 @@ def setup_ecobee(hass, network, config): configurator = get_component('configurator') configurator.request_done(_CONFIGURING.pop('ecobee')) - hold_temp = config[DOMAIN].get(HOLD_TEMP, False) + hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) discovery.load_platform(hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index db68d505b14..95d91d42efc 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -5,10 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fastdotcom/ """ import logging +import voluptuous as vol import homeassistant.util.dt as dt_util +import homeassistant.helpers.config_validation as cv from homeassistant.components import recorder -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change @@ -22,6 +24,17 @@ CONF_MINUTE = 'minute' CONF_HOUR = 'hour' CONF_DAY = 'day' +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_SECOND, default=[0]): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), + vol.Optional(CONF_MINUTE, default=[0]): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), + vol.Optional(CONF_HOUR): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), + vol.Optional(CONF_DAY): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Fast.com sensor.""" @@ -43,10 +56,10 @@ class SpeedtestSensor(Entity): def __init__(self, speedtest_data): """Initialize the sensor.""" - self._name = 'Fast.com Speedtest' + self._name = 'Fast.com Download' self.speedtest_client = speedtest_data self._state = None - self._unit_of_measurement = 'Mbps' + self._unit_of_measurement = 'Mbit/s' @property def name(self): @@ -94,10 +107,10 @@ class SpeedtestData(object): """Initialize the data object.""" self.data = None track_time_change(hass, self.update, - second=config.get(CONF_SECOND, 0), - minute=config.get(CONF_MINUTE, 0), - hour=config.get(CONF_HOUR, None), - day=config.get(CONF_DAY, None)) + second=config.get(CONF_SECOND), + minute=config.get(CONF_MINUTE), + hour=config.get(CONF_HOUR), + day=config.get(CONF_DAY)) def update(self, now): """Get the latest data from fast.com.""" diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 0ab2301dbe2..4f3b2cd17c7 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -6,12 +6,14 @@ https://home-assistant.io/components/sensor.forecast/ """ import logging from datetime import timedelta +import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, \ HTTPError, Timeout -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_API_KEY, CONF_NAME, + CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -56,6 +58,16 @@ SENSOR_TYPES = { 'mm', 'in', 'mm', 'mm', 'mm'], } DEFAULT_NAME = "Forecast.io" +CONF_UNITS = 'units' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']) +}) + # Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) @@ -67,12 +79,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - elif not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY]}, _LOGGER): - return False - if 'units' in config: - units = config['units'] + if CONF_UNITS in config: + units = config[CONF_UNITS] elif hass.config.units.is_metric: units = 'si' else: @@ -89,15 +98,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(error) return False - name = config.get('name', DEFAULT_NAME) + name = config.get(CONF_NAME) # Initialize and add all of the sensors. sensors = [] - for variable in config['monitored_conditions']: - if variable in SENSOR_TYPES: - sensors.append(ForeCastSensor(forecast_data, variable, name)) - else: - _LOGGER.error('Sensor type: "%s" does not exist', variable) + for variable in config[CONF_MONITORED_CONDITIONS]: + sensors.append(ForeCastSensor(forecast_data, variable, name)) add_devices(sensors) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 1ce653f7ae6..86fd48e4d03 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -8,10 +8,13 @@ import logging import re import sys from subprocess import check_output, CalledProcessError +import voluptuous as vol import homeassistant.util.dt as dt_util +import homeassistant.helpers.config_validation as cv from homeassistant.components import recorder -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change @@ -22,7 +25,6 @@ _SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+' r'Download:\s(\d+\.\d+)\sMbit/s[\r\n]+' r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+') -CONF_MONITORED_CONDITIONS = 'monitored_conditions' CONF_SECOND = 'second' CONF_MINUTE = 'minute' CONF_HOUR = 'hour' @@ -33,6 +35,19 @@ SENSOR_TYPES = { 'upload': ['Upload', 'Mbit/s'], } +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]), + vol.Optional(CONF_SECOND, default=[0]): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), + vol.Optional(CONF_MINUTE, default=[0]): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]), + vol.Optional(CONF_HOUR): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]), + vol.Optional(CONF_DAY): + vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]), +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Speedtest sensor.""" @@ -117,10 +132,10 @@ class SpeedtestData(object): """Initialize the data object.""" self.data = None track_time_change(hass, self.update, - second=config.get(CONF_SECOND, 0), - minute=config.get(CONF_MINUTE, 0), - hour=config.get(CONF_HOUR, None), - day=config.get(CONF_DAY, None)) + second=config.get(CONF_SECOND), + minute=config.get(CONF_MINUTE), + hour=config.get(CONF_HOUR), + day=config.get(CONF_DAY)) def update(self, now): """Get the latest data from speedtest.net."""