diff --git a/.coveragerc b/.coveragerc index 370d229d87d..2f6e7a9adf8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -72,6 +72,7 @@ omit = homeassistant/components/camera/foscam.py homeassistant/components/camera/generic.py homeassistant/components/camera/mjpeg.py + homeassistant/components/camera/rpi_camera.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py @@ -107,6 +108,8 @@ omit = homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/onkyo.py + homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/yamaha.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/googlevoice.py @@ -136,7 +139,10 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py + homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/netatmo.py + homeassistant/components/sensor/nzbget.py + homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openweathermap.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7b5a0f1980f..b366e24b037 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,5 @@ +Feature requests should go in the forum: https://community.home-assistant.io/c/feature-requests + **Home Assistant release (`hass --version`):** diff --git a/.gitignore b/.gitignore index 1e79f07b663..e7d64517e17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ config/* !config/home-assistant.conf.default -homeassistant/components/frontend/www_static/polymer/bower_components/* # There is not a better solution afaik.. !config/custom_components diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index fae945b05e4..899134b71a7 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -1,9 +1,9 @@ homeassistant: - # Omitted values in this section will be auto detected using freegeoip.net + # Omitted values in this section will be auto detected using freegeoip.io # Location required to calculate the time the sun rises and sets. - # Cooridinates are also used for location for weather related components. - # Google Maps can be used to determine more precise GPS cooridinates. + # Coordinates are also used for location for weather related components. + # Google Maps can be used to determine more precise GPS coordinates. latitude: 32.87336 longitude: 117.22743 diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 02ccc239f2b..6dbb609ec05 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -9,8 +9,6 @@ import threading import time from multiprocessing import Process -import homeassistant.config as config_util -from homeassistant import bootstrap from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, @@ -32,6 +30,7 @@ def validate_python(): def ensure_config_path(config_dir): """Validate the configuration directory.""" + import homeassistant.config as config_util lib_dir = os.path.join(config_dir, 'lib') # Test if configuration directory exists @@ -60,6 +59,7 @@ def ensure_config_path(config_dir): def ensure_config_file(config_dir): """Ensure configuration file exists.""" + import homeassistant.config as config_util config_path = config_util.ensure_config_exists(config_dir) if config_path is None: @@ -71,6 +71,7 @@ def ensure_config_file(config_dir): def get_arguments(): """Get parsed passed in arguments.""" + import homeassistant.config as config_util parser = argparse.ArgumentParser( description="Home Assistant: Observe, Control, Automate.") parser.add_argument('--version', action='version', version=__version__) @@ -225,6 +226,8 @@ def setup_and_run_hass(config_dir, args, top_process=False): Block until stopped. Will assume it is running in a subprocess unless top_process is set to true. """ + from homeassistant import bootstrap + if args.demo_mode: config = { 'frontend': {}, @@ -241,6 +244,9 @@ def setup_and_run_hass(config_dir, args, top_process=False): config_file, daemon=args.daemon, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days) + if hass is None: + return + if args.open_ui: def open_browser(event): """Open the webinterface in a browser.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c6f1a75b8a6..a7476162265 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -8,6 +8,8 @@ import sys from collections import defaultdict from threading import RLock +import voluptuous as vol + import homeassistant.components as core_components import homeassistant.components.group as group import homeassistant.config as config_util @@ -19,8 +21,9 @@ import homeassistant.util.package as pkg_util from homeassistant.const import ( CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, - TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__) -from homeassistant.helpers import event_decorators, service + TEMP_CELCIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__) +from homeassistant.helpers import ( + event_decorators, service, config_per_platform, extract_domain_configs) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -29,7 +32,6 @@ _CURRENT_SETUP = [] ATTR_COMPONENT = 'component' -PLATFORM_FORMAT = '{}.{}' ERROR_LOG_FILENAME = 'home-assistant.log' @@ -72,7 +74,7 @@ def _handle_requirements(hass, component, name): def _setup_component(hass, domain, config): """Setup a component for Home Assistant.""" - # pylint: disable=too-many-return-statements + # pylint: disable=too-many-return-statements,too-many-branches if domain in hass.config.components: return True @@ -96,6 +98,56 @@ def _setup_component(hass, domain, config): domain, ", ".join(missing_deps)) return False + if hasattr(component, 'CONFIG_SCHEMA'): + try: + config = component.CONFIG_SCHEMA(config) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid config for [%s]: %s', domain, ex) + return False + + elif hasattr(component, 'PLATFORM_SCHEMA'): + platforms = [] + for p_name, p_config in config_per_platform(config, domain): + # Validate component specific platform schema + try: + p_validated = component.PLATFORM_SCHEMA(p_config) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid platform config for [%s]: %s. %s', + domain, ex, p_config) + return False + + # Not all platform components follow same pattern for platforms + # Sof if p_name is None we are not going to validate platform + # (the automation component is one of them) + if p_name is None: + platforms.append(p_validated) + continue + + platform = prepare_setup_platform(hass, config, domain, + p_name) + + if platform is None: + return False + + # Validate platform specific schema + if hasattr(platform, 'PLATFORM_SCHEMA'): + try: + p_validated = platform.PLATFORM_SCHEMA(p_validated) + except vol.MultipleInvalid as ex: + _LOGGER.error( + 'Invalid platform config for [%s.%s]: %s. %s', + domain, p_name, ex, p_config) + return False + + platforms.append(p_validated) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + filter_keys = extract_domain_configs(config, domain) + config = {key: value for key, value in config.items() + if key not in filter_keys} + config[domain] = platforms + if not _handle_requirements(hass, component, domain): return False @@ -130,7 +182,7 @@ def prepare_setup_platform(hass, config, domain, platform_name): platform_path = PLATFORM_FORMAT.format(domain, platform_name) - platform = loader.get_component(platform_path) + platform = loader.get_platform(domain, platform_name) # Not found if platform is None: @@ -176,8 +228,14 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, hass.config.config_dir = config_dir mount_local_lib_path(config_dir) + try: + process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( + config.get(core.DOMAIN, {}))) + except vol.MultipleInvalid as ex: + _LOGGER.error('Invalid config for [homeassistant]: %s', ex) + return None + process_ha_config_upgrade(hass) - process_ha_core_config(hass, config.get(core.DOMAIN, {})) if enable_log: enable_logging(hass, verbose, daemon, log_rotate_days) @@ -262,8 +320,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): } )) except ImportError: - _LOGGER.warning( - "Colorlog package not found, console coloring disabled") + pass # Log errors to a file if we have write access to file or config dir err_log_path = hass.config.path(ERROR_LOG_FILENAME) @@ -336,40 +393,28 @@ def process_ha_core_config(hass, config): else: _LOGGER.error('Received invalid time zone %s', time_zone_str) - for key, attr, typ in ((CONF_LATITUDE, 'latitude', float), - (CONF_LONGITUDE, 'longitude', float), - (CONF_NAME, 'location_name', str)): + for key, attr in ((CONF_LATITUDE, 'latitude'), + (CONF_LONGITUDE, 'longitude'), + (CONF_NAME, 'location_name')): if key in config: - try: - setattr(hac, attr, typ(config[key])) - except ValueError: - _LOGGER.error('Received invalid %s value for %s: %s', - typ.__name__, key, attr) + setattr(hac, attr, config[key]) - set_time_zone(config.get(CONF_TIME_ZONE)) + if CONF_TIME_ZONE in config: + set_time_zone(config.get(CONF_TIME_ZONE)) - customize = config.get(CONF_CUSTOMIZE) - - if isinstance(customize, dict): - for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): - if not isinstance(attrs, dict): - continue - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) + for entity_id, attrs in config.get(CONF_CUSTOMIZE).items(): + Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) if CONF_TEMPERATURE_UNIT in config: - unit = config[CONF_TEMPERATURE_UNIT] - - if unit == 'C': - hac.temperature_unit = TEMP_CELCIUS - elif unit == 'F': - hac.temperature_unit = TEMP_FAHRENHEIT + hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] # If we miss some of the needed values, auto detect them if None not in ( hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): return - _LOGGER.info('Auto detecting location and temperature unit') + _LOGGER.warning('Incomplete core config. Auto detecting location and ' + 'temperature unit') info = loc_util.detect_location_info() diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index f70da3d54ec..e3bde441211 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 0e86e0df875..3bc7b860869 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -6,43 +6,55 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.mqtt as mqtt from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, + CONF_NAME) +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "MQTT Alarm" -DEFAULT_QOS = 0 -DEFAULT_PAYLOAD_DISARM = "DISARM" -DEFAULT_PAYLOAD_ARM_HOME = "ARM_HOME" -DEFAULT_PAYLOAD_ARM_AWAY = "ARM_AWAY" - DEPENDENCIES = ['mqtt'] +CONF_PAYLOAD_DISARM = 'payload_disarm' +CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' +CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' +CONF_CODE = 'code' + +DEFAULT_NAME = "MQTT Alarm" +DEFAULT_DISARM = "DISARM" +DEFAULT_ARM_HOME = "ARM_HOME" +DEFAULT_ARM_AWAY = "ARM_AWAY" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, + vol.Optional(CONF_CODE): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT platform.""" - if config.get('state_topic') is None: - _LOGGER.error("Missing required variable: state_topic") - return False - - if config.get('command_topic') is None: - _LOGGER.error("Missing required variable: command_topic") - return False - add_devices([MqttAlarm( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic'), - config.get('command_topic'), - config.get('qos', DEFAULT_QOS), - config.get('payload_disarm', DEFAULT_PAYLOAD_DISARM), - config.get('payload_arm_home', DEFAULT_PAYLOAD_ARM_HOME), - config.get('payload_arm_away', DEFAULT_PAYLOAD_ARM_AWAY), - config.get('code'))]) + config[CONF_NAME], + config[CONF_STATE_TOPIC], + config[CONF_COMMAND_TOPIC], + config[CONF_QOS], + config[CONF_PAYLOAD_DISARM], + config[CONF_PAYLOAD_ARM_HOME], + config[CONF_PAYLOAD_ARM_AWAY], + config.get(CONF_CODE))]) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -62,7 +74,7 @@ class MqttAlarm(alarm.AlarmControlPanel): self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away - self._code = str(code) if code else None + self._code = code def message_received(topic, payload, qos): """A new MQTT message has been received.""" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7a3f635bc49..ce5dde3609d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,13 +6,15 @@ https://home-assistant.io/components/automation/ """ import logging +import voluptuous as vol + from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook from homeassistant.helpers import extract_domain_configs -from homeassistant.helpers.service import (call_from_config, - validate_service_call) - +from homeassistant.helpers.service import call_from_config +from homeassistant.loader import get_platform +import homeassistant.helpers.config_validation as cv DOMAIN = 'automation' @@ -31,17 +33,72 @@ CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND +METHOD_TRIGGER = 'trigger' +METHOD_IF_ACTION = 'if_action' + _LOGGER = logging.getLogger(__name__) +def _platform_validator(method, schema): + """Generate platform validator for different steps.""" + def validator(config): + """Validate it is a valid platform.""" + platform = get_platform(DOMAIN, config[CONF_PLATFORM]) + + if not hasattr(platform, method): + raise vol.Invalid('invalid method platform') + + if not hasattr(platform, schema): + return config + + print('validating config', method, config) + + return getattr(platform, schema)(config) + + return validator + +_TRIGGER_SCHEMA = vol.All( + cv.ensure_list, + [ + vol.All( + vol.Schema({ + vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + }, extra=vol.ALLOW_EXTRA), + _platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA') + ), + ] +) + +_CONDITION_SCHEMA = vol.Any( + CONDITION_USE_TRIGGER_VALUES, + vol.All( + cv.ensure_list, + [ + vol.All( + vol.Schema({ + vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN), + }, extra=vol.ALLOW_EXTRA), + _platform_validator(METHOD_IF_ACTION, 'IF_ACTION_SCHEMA'), + ) + ] + ) +) + +PLATFORM_SCHEMA = vol.Schema({ + CONF_ALIAS: cv.string, + vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, + vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE): + vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)), + CONF_CONDITION: _CONDITION_SCHEMA, + vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA, +}) + + def setup(hass, config): """Setup the automation.""" for config_key in extract_domain_configs(config, DOMAIN): conf = config[config_key] - if not isinstance(conf, list): - conf = [conf] - for list_no, config_block in enumerate(conf): name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, list_no)) @@ -54,10 +111,7 @@ def _setup_automation(hass, config_block, name, config): """Setup one instance of automation.""" action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) - if action is None: - return False - - if CONF_CONDITION in config_block or CONF_CONDITION_TYPE in config_block: + if CONF_CONDITION in config_block: action = _process_if(hass, config, config_block, action) if action is None: @@ -70,11 +124,6 @@ def _setup_automation(hass, config_block, name, config): def _get_action(hass, config, name): """Return an action based on a configuration.""" - validation_error = validate_service_call(config) - if validation_error: - _LOGGER.error(validation_error) - return None - def action(): """Action to be executed.""" _LOGGER.info('Executing %s', name) @@ -96,12 +145,9 @@ def _process_if(hass, config, p_config, action): if use_trigger: if_configs = p_config[CONF_TRIGGER] - if isinstance(if_configs, dict): - if_configs = [if_configs] - checks = [] for if_config in if_configs: - platform = _resolve_platform('if_action', hass, config, + platform = _resolve_platform(METHOD_IF_ACTION, hass, config, if_config.get(CONF_PLATFORM)) if platform is None: continue @@ -134,7 +180,7 @@ def _process_trigger(hass, config, trigger_configs, name, action): trigger_configs = [trigger_configs] for conf in trigger_configs: - platform = _resolve_platform('trigger', hass, config, + platform = _resolve_platform(METHOD_TRIGGER, hass, config, conf.get(CONF_PLATFORM)) if platform is None: continue diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index db63d81e54b..db0c1be7c2a 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -4,26 +4,29 @@ Offer MQTT listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#mqtt-trigger """ -import logging +import voluptuous as vol import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] CONF_TOPIC = 'topic' CONF_PAYLOAD = 'payload' +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): mqtt.DOMAIN, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD): cv.string, +}) + def trigger(hass, config, action): """Listen for state changes based on configuration.""" - topic = config.get(CONF_TOPIC) + topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) - if topic is None: - logging.getLogger(__name__).error( - "Missing configuration key %s", CONF_TOPIC) - return False - def mqtt_automation_listener(msg_topic, msg_payload, qos): """Listen for MQTT messages.""" if payload is None or payload == msg_payload: diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index d87d7e3fff6..742e6195949 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -4,16 +4,17 @@ Offer state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#state-trigger """ -import logging from datetime import timedelta -import homeassistant.util.dt as dt_util +import voluptuous as vol +import homeassistant.util.dt as dt_util from homeassistant.const import ( - EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) + EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM) from homeassistant.components.automation.time import ( CONF_HOURS, CONF_MINUTES, CONF_SECONDS) from homeassistant.helpers.event import track_state_change, track_point_in_time +import homeassistant.helpers.config_validation as cv CONF_ENTITY_ID = "entity_id" CONF_FROM = "from" @@ -21,6 +22,33 @@ CONF_TO = "to" CONF_STATE = "state" CONF_FOR = "for" +BASE_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'state', + vol.Required(CONF_ENTITY_ID): cv.entity_id, + # These are str on purpose. Want to catch YAML conversions + CONF_STATE: str, + CONF_FOR: vol.All(vol.Schema({ + CONF_HOURS: vol.Coerce(int), + CONF_MINUTES: vol.Coerce(int), + CONF_SECONDS: vol.Coerce(int), + }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)), +}) + +TRIGGER_SCHEMA = vol.Schema(vol.All( + BASE_SCHEMA.extend({ + # These are str on purpose. Want to catch YAML conversions + CONF_FROM: str, + CONF_TO: str, + }), + vol.Any(cv.key_dependency(CONF_FOR, CONF_TO), + cv.key_dependency(CONF_FOR, CONF_STATE)) +)) + +IF_ACTION_SCHEMA = vol.Schema(vol.All( + BASE_SCHEMA, + cv.key_dependency(CONF_FOR, CONF_STATE) +)) + def get_time_config(config): """Helper function to extract the time specified in the configuration.""" @@ -31,18 +59,6 @@ def get_time_config(config): minutes = config[CONF_FOR].get(CONF_MINUTES) seconds = config[CONF_FOR].get(CONF_SECONDS) - if hours is None and minutes is None and seconds is None: - logging.getLogger(__name__).error( - "Received invalid value for '%s': %s", - config[CONF_FOR], CONF_FOR) - return None - - if config.get(CONF_TO) is None and config.get(CONF_STATE) is None: - logging.getLogger(__name__).error( - "For: requires a to: value'%s': %s", - config[CONF_FOR], CONF_FOR) - return None - return timedelta(hours=(hours or 0.0), minutes=(minutes or 0.0), seconds=(seconds or 0.0)) @@ -51,24 +67,10 @@ def get_time_config(config): def trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) - - if entity_id is None: - logging.getLogger(__name__).error( - "Missing trigger configuration key %s", CONF_ENTITY_ID) - return None - from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL time_delta = get_time_config(config) - if isinstance(from_state, bool) or isinstance(to_state, bool): - logging.getLogger(__name__).error( - 'Config error. Surround to/from values with quotes.') - return None - - if CONF_FOR in config and time_delta is None: - return None - def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" def state_for_listener(now): @@ -105,18 +107,7 @@ def if_action(hass, config): """Wrap action method with state based condition.""" entity_id = config.get(CONF_ENTITY_ID) state = config.get(CONF_STATE) - - if entity_id is None or state is None: - logging.getLogger(__name__).error( - "Missing if-condition configuration key %s or %s", CONF_ENTITY_ID, - CONF_STATE) - return None - time_delta = get_time_config(config) - if CONF_FOR in config and time_delta is None: - return None - - state = str(state) def if_state(): """Test if condition.""" diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 63d7715ef7d..2a564a3b588 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,12 +4,16 @@ Offer sun based automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#sun-trigger """ -import logging from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.const import CONF_PLATFORM import homeassistant.util.dt as dt_util from homeassistant.components import sun from homeassistant.helpers.event import track_sunrise, track_sunset +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['sun'] @@ -26,22 +30,30 @@ EVENT_SUNRISE = 'sunrise' _LOGGER = logging.getLogger(__name__) +_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET)) + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'sun', + vol.Required(CONF_EVENT): _SUN_EVENT, + vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset, +}) + +IF_ACTION_SCHEMA = vol.All( + vol.Schema({ + vol.Required(CONF_PLATFORM): 'sun', + CONF_BEFORE: _SUN_EVENT, + CONF_AFTER: _SUN_EVENT, + vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset, + vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset, + }), + cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER), +) + + def trigger(hass, config, action): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) - - if event is None: - _LOGGER.error("Missing configuration key %s", CONF_EVENT) - return False - - event = event.lower() - if event not in (EVENT_SUNRISE, EVENT_SUNSET): - _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) - return False - - offset = _parse_offset(config.get(CONF_OFFSET)) - if offset is False: - return False + offset = config.get(CONF_OFFSET) # Do something to call action if event == EVENT_SUNRISE: @@ -56,26 +68,8 @@ def if_action(hass, config): """Wrap action method with sun based condition.""" before = config.get(CONF_BEFORE) after = config.get(CONF_AFTER) - - # Make sure required configuration keys are present - if before is None and after is None: - logging.getLogger(__name__).error( - "Missing if-condition configuration key %s or %s", - CONF_BEFORE, CONF_AFTER) - return None - - # Make sure configuration keys have the right value - if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \ - after not in (None, EVENT_SUNRISE, EVENT_SUNSET): - logging.getLogger(__name__).error( - "%s and %s can only be set to %s or %s", - CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) - return None - - before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET)) - after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET)) - if before_offset is False or after_offset is False: - return None + before_offset = config.get(CONF_BEFORE_OFFSET) + after_offset = config.get(CONF_AFTER_OFFSET) if before is None: def before_func(): @@ -120,27 +114,3 @@ def if_action(hass, config): return True return time_if - - -def _parse_offset(raw_offset): - """Parse the offset.""" - if raw_offset is None: - return timedelta(0) - - negative_offset = False - if raw_offset.startswith('-'): - negative_offset = True - raw_offset = raw_offset[1:] - - try: - (hour, minute, second) = [int(x) for x in raw_offset.split(':')] - except ValueError: - _LOGGER.error('Could not parse offset %s', raw_offset) - return False - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if negative_offset: - offset *= -1 - - return offset diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 1d17246c012..02e8f30d209 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -6,21 +6,27 @@ at https://home-assistant.io/components/automation/#template-trigger """ import logging -from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED +import voluptuous as vol + +from homeassistant.const import ( + CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv + _LOGGER = logging.getLogger(__name__) +TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'template', + vol.Required(CONF_VALUE_TEMPLATE): cv.template, +}) + def trigger(hass, config, action): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is None: - _LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE) - return False - # Local variable to keep track of if the action has already been triggered already_triggered = False @@ -44,10 +50,6 @@ def if_action(hass, config): """Wrap action method with state based condition.""" value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is None: - _LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE) - return False - return lambda: _check_template(hass, value_template) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index a5f7259b112..66ea3c2d7c7 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -4,12 +4,13 @@ Offer zone automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#zone-trigger """ -import logging +import voluptuous as vol from homeassistant.components import zone from homeassistant.const import ( - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) + ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, CONF_PLATFORM) from homeassistant.helpers.event import track_state_change +import homeassistant.helpers.config_validation as cv CONF_ENTITY_ID = "entity_id" CONF_ZONE = "zone" @@ -18,19 +19,26 @@ EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'zone', + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ZONE): cv.entity_id, + vol.Required(CONF_EVENT, default=DEFAULT_EVENT): + vol.Any(EVENT_ENTER, EVENT_LEAVE), +}) + +IF_ACTION_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'zone', + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ZONE): cv.entity_id, +}) + def trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) - - if entity_id is None or zone_entity_id is None: - logging.getLogger(__name__).error( - "Missing trigger configuration key %s or %s", CONF_ENTITY_ID, - CONF_ZONE) - return False - - event = config.get(CONF_EVENT, DEFAULT_EVENT) + event = config.get(CONF_EVENT) def zone_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" @@ -59,12 +67,6 @@ def if_action(hass, config): entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) - if entity_id is None or zone_entity_id is None: - logging.getLogger(__name__).error( - "Missing condition configuration key %s or %s", CONF_ENTITY_ID, - CONF_ZONE) - return False - def if_in_zone(): """Test if condition.""" return _in_zone(hass, zone_entity_id, hass.states.get(entity_id)) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 777974905b8..8d941166fdd 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -11,26 +11,28 @@ from homeassistant.helpers.entity import Entity from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.components import ( bloomsky, mysensors, zwave, vera, wemo, wink) +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa DOMAIN = 'binary_sensor' SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' SENSOR_CLASSES = [ - None, # Generic on/off - 'opening', # Door, window, etc - 'motion', # Motion sensor - 'gas', # CO, CO2, etc - 'smoke', # Smoke detector - 'moisture', # Specifically a wetness sensor - 'light', # Lightness threshold - 'power', # Power, over-current, etc - 'safety', # Generic on=unsafe, off=safe - 'heat', # On means hot (or too hot) - 'cold', # On means cold (or too cold) - 'moving', # On means moving, Off means stopped - 'sound', # On means sound detected, Off means no sound - 'vibration', # On means vibration detected, Off means no vibration + None, # Generic on/off + 'cold', # On means cold (or too cold) + 'connectivity', # On means connection present, Off = no connection + 'gas', # CO, CO2, etc. + 'heat', # On means hot (or too hot) + 'light', # Lightness threshold + 'moisture', # Specifically a wetness sensor + 'motion', # Motion sensor + 'moving', # On means moving, Off means stopped + 'opening', # Door, window, etc. + 'power', # Power, over-current, etc + 'safety', # Generic on=unsafe, off=safe + 'smoke', # Smoke detector + 'sound', # On means sound detected, Off means no sound + 'vibration', # On means vibration detected, Off means no vibration ] # Maps discovered services to their platforms diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 61e0202fcdb..899dd44a42b 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -9,7 +9,8 @@ from datetime import timedelta import requests -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import (BinarySensorDevice, + SENSOR_CLASSES) from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -26,6 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): resource = config.get(CONF_RESOURCE) pin = config.get(CONF_PIN) + sensor_class = config.get('sensor_class') + if sensor_class not in SENSOR_CLASSES: + _LOGGER.warning('Unknown sensor class: %s', sensor_class) + sensor_class = None + if None in (resource, pin): _LOGGER.error('Not all required config keys present: %s', ', '.join((CONF_RESOURCE, CONF_PIN))) @@ -45,21 +51,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): arest = ArestData(resource, pin) - add_devices([ArestBinarySensor(arest, - resource, - config.get('name', response['name']), - pin)]) + add_devices([ArestBinarySensor( + arest, + resource, + config.get('name', response['name']), + sensor_class, + pin)]) # pylint: disable=too-many-instance-attributes, too-many-arguments class ArestBinarySensor(BinarySensorDevice): """Implement an aREST binary sensor for a pin.""" - def __init__(self, arest, resource, name, pin): + def __init__(self, arest, resource, name, sensor_class, pin): """Initialize the aREST device.""" self.arest = arest self._resource = resource self._name = name + self._sensor_class = sensor_class self._pin = pin self.update() @@ -79,6 +88,11 @@ class ArestBinarySensor(BinarySensorDevice): """Return true if the binary sensor is on.""" return bool(self.arest.data.get('state')) + @property + def sensor_class(self): + """Return the class of this sensor.""" + return self._sensor_class + def update(self): """Get the latest data from aREST API.""" self.arest.update() diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 88cbceccc45..a381305691a 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -6,43 +6,50 @@ https://home-assistant.io/components/binary_sensor.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import (BinarySensorDevice, SENSOR_CLASSES) -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE +from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mqtt'] + +CONF_SENSOR_CLASS = 'sensor_class' +CONF_PAYLOAD_ON = 'payload_on' +CONF_PAYLOAD_OFF = 'payload_off' + DEFAULT_NAME = 'MQTT Binary sensor' -DEFAULT_QOS = 0 DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' -DEPENDENCIES = ['mqtt'] +PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SENSOR_CLASS, default=None): + vol.Any(vol.In(SENSOR_CLASSES), vol.SetTo(None)), + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, +}) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Add MQTT binary sensor.""" - if config.get('state_topic') is None: - _LOGGER.error('Missing required variable: state_topic') - return False - - sensor_class = config.get('sensor_class') - if sensor_class not in SENSOR_CLASSES: - _LOGGER.warning('Unknown sensor class: %s', sensor_class) - sensor_class = None - add_devices([MqttBinarySensor( hass, - config.get('name', DEFAULT_NAME), - config.get('state_topic', None), - sensor_class, - config.get('qos', DEFAULT_QOS), - config.get('payload_on', DEFAULT_PAYLOAD_ON), - config.get('payload_off', DEFAULT_PAYLOAD_OFF), - config.get(CONF_VALUE_TEMPLATE))]) + config[CONF_NAME], + config[CONF_STATE_TOPIC], + config[CONF_SENSOR_CLASS], + config[CONF_QOS], + config[CONF_PAYLOAD_ON], + config[CONF_PAYLOAD_OFF], + config.get(CONF_VALUE_TEMPLATE) + )]) # pylint: disable=too-many-arguments, too-many-instance-attributes diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py index 4048c884df6..dcf4c3dff7e 100644 --- a/homeassistant/components/binary_sensor/tcp.py +++ b/homeassistant/components/binary_sensor/tcp.py @@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.tcp/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor.tcp import Sensor, DOMAIN, CONF_VALUE_ON +from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON + -DEPENDENCIES = [DOMAIN] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 41860a62cb7..c473f159f65 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,10 +14,8 @@ import requests from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import bloomsky -from homeassistant.const import ( - HTTP_NOT_FOUND, - ATTR_ENTITY_ID, - ) +from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa DOMAIN = 'camera' @@ -36,7 +34,7 @@ STATE_IDLE = 'idle' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}' -MULTIPART_BOUNDARY = '--jpegboundary' +MULTIPART_BOUNDARY = '--jpgboundary' MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' @@ -49,17 +47,6 @@ def setup(hass, config): component.setup(config) - # ------------------------------------------------------------------------- - # CAMERA COMPONENT ENDPOINTS - # ------------------------------------------------------------------------- - # The following defines the endpoints for serving images from the camera - # via the HA http server. This is means that you can access images from - # your camera outside of your LAN without the need for port forwards etc. - - # Because the authentication header can't be added in image requests these - # endpoints are secured with session based security. - - # pylint: disable=unused-argument def _proxy_camera_image(handler, path_match, data): """Serve the camera image via the HA server.""" entity_id = path_match.group(ATTR_ENTITY_ID) @@ -77,22 +64,16 @@ def setup(hass, config): handler.end_headers() return - handler.wfile.write(response) + handler.send_response(HTTP_OK) + handler.write_content(response) hass.http.register_path( 'GET', re.compile(r'/api/camera_proxy/(?P[a-zA-Z\._0-9]+)'), _proxy_camera_image) - # pylint: disable=unused-argument def _proxy_camera_mjpeg_stream(handler, path_match, data): - """ - Proxy the camera image as an mjpeg stream via the HA server. - - This function takes still images from the IP camera and turns them - into an MJPEG stream. This means that HA can return a live video - stream even with only a still image URL available. - """ + """Proxy the camera image as an mjpeg stream via the HA server.""" entity_id = path_match.group(ATTR_ENTITY_ID) camera = component.entities.get(entity_id) @@ -112,8 +93,7 @@ def setup(hass, config): hass.http.register_path( 'GET', - re.compile( - r'/api/camera_proxy_stream/(?P[a-zA-Z\._0-9]+)'), + re.compile(r'/api/camera_proxy_stream/(?P[a-zA-Z\._0-9]+)'), _proxy_camera_mjpeg_stream) return True @@ -137,19 +117,16 @@ class Camera(Entity): return ENTITY_IMAGE_URL.format(self.entity_id) @property - # pylint: disable=no-self-use def is_recording(self): """Return true if the device is recording.""" return False @property - # pylint: disable=no-self-use def brand(self): """Camera brand.""" return None @property - # pylint: disable=no-self-use def model(self): """Camera model.""" return None @@ -160,29 +137,28 @@ class Camera(Entity): def mjpeg_stream(self, handler): """Generate an HTTP MJPEG stream from camera images.""" - handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8')) - handler.request.sendall(bytes( - 'Content-type: multipart/x-mixed-replace; \ - boundary=--jpgboundary\r\n\r\n', 'utf-8')) - handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8')) + def write_string(text): + """Helper method to write a string to the stream.""" + handler.request.sendall(bytes(text + '\r\n', 'utf-8')) + + write_string('HTTP/1.1 200 OK') + write_string('Content-type: multipart/x-mixed-replace; ' + 'boundary={}'.format(MULTIPART_BOUNDARY)) + write_string('') + write_string(MULTIPART_BOUNDARY) - # MJPEG_START_HEADER.format() while True: img_bytes = self.camera_image() + if img_bytes is None: continue - headers_str = '\r\n'.join(( - 'Content-length: {}'.format(len(img_bytes)), - 'Content-type: image/jpeg', - )) + '\r\n\r\n' - handler.request.sendall( - bytes(headers_str, 'utf-8') + - img_bytes + - bytes('\r\n', 'utf-8')) - - handler.request.sendall( - bytes('--jpgboundary\r\n', 'utf-8')) + write_string('Content-length: {}'.format(len(img_bytes))) + write_string('Content-type: image/jpeg') + write_string('') + handler.request.sendall(img_bytes) + write_string('') + write_string(MULTIPART_BOUNDARY) time.sleep(0.5) diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py new file mode 100644 index 00000000000..cda48d1ddfa --- /dev/null +++ b/homeassistant/components/camera/rpi_camera.py @@ -0,0 +1,86 @@ +"""Camera platform that has a Raspberry Pi camera.""" + +import os +import subprocess +import logging +import shutil + +from homeassistant.components.camera import Camera + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Raspberry Camera.""" + if shutil.which("raspistill") is None: + _LOGGER.error("Error: raspistill not found") + return False + + setup_config = ( + { + "name": config.get("name", "Raspberry Pi Camera"), + "image_width": int(config.get("image_width", "640")), + "image_height": int(config.get("image_height", "480")), + "image_quality": int(config.get("image_quality", "7")), + "image_rotation": int(config.get("image_rotation", "0")), + "timelapse": int(config.get("timelapse", "2000")), + "horizontal_flip": int(config.get("horizontal_flip", "0")), + "vertical_flip": int(config.get("vertical_flip", "0")), + "file_path": config.get("file_path", + os.path.join(os.path.dirname(__file__), + 'image.jpg')) + } + ) + + # check filepath given is writable + if not os.access(setup_config["file_path"], os.W_OK): + _LOGGER.error("Error: file path is not writable") + return False + + add_devices([ + RaspberryCamera(setup_config) + ]) + + +class RaspberryCamera(Camera): + """Raspberry Pi camera.""" + + def __init__(self, device_info): + """Initialize Raspberry Pi camera component.""" + super().__init__() + + self._name = device_info["name"] + self._config = device_info + + # kill if there's raspistill instance + subprocess.Popen(['killall', 'raspistill'], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT) + + cmd_args = [ + 'raspistill', '--nopreview', '-o', str(device_info["file_path"]), + '-t', '0', '-w', str(device_info["image_width"]), + '-h', str(device_info["image_height"]), + '-tl', str(device_info["timelapse"]), + '-q', str(device_info["image_quality"]), + '-rot', str(device_info["image_rotation"]) + ] + if device_info["horizontal_flip"]: + cmd_args.append("-hf") + + if device_info["vertical_flip"]: + cmd_args.append("-vf") + + subprocess.Popen(cmd_args, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT) + + def camera_image(self): + """Return raspstill image response.""" + with open(self._config["file_path"], 'rb') as file: + return file.read() + + @property + def name(self): + """Return the name of this camera.""" + return self._name diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 74ba0648c74..11e018846c2 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/conversation/ """ import logging import re +import warnings from homeassistant import core from homeassistant.const import ( @@ -24,6 +25,7 @@ REQUIREMENTS = ['fuzzywuzzy==0.8.0'] def setup(hass, config): """Register the process service.""" + warnings.filterwarnings('ignore', module='fuzzywuzzy') from fuzzywuzzy import process as fuzzyExtract logger = logging.getLogger(__name__) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 3a70be00b38..0570d20c262 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -95,18 +95,18 @@ def setup(hass, config): 'demo': { 'alias': 'Toggle {}'.format(lights[0].split('.')[1]), 'sequence': [{ - 'execute_service': 'light.turn_off', - 'service_data': {ATTR_ENTITY_ID: lights[0]} + 'service': 'light.turn_off', + 'data': {ATTR_ENTITY_ID: lights[0]} }, { 'delay': {'seconds': 5} }, { - 'execute_service': 'light.turn_on', - 'service_data': {ATTR_ENTITY_ID: lights[0]} + 'service': 'light.turn_on', + 'data': {ATTR_ENTITY_ID: lights[0]} }, { 'delay': {'seconds': 5} }, { - 'execute_service': 'light.turn_off', - 'service_data': {ATTR_ENTITY_ID: lights[0]} + 'service': 'light.turn_off', + 'data': {ATTR_ENTITY_ID: lights[0]} }] }}}) @@ -136,7 +136,7 @@ def setup(hass, config): 'Home Alone']}, 'who_cooks': {'icon': 'mdi:panda', 'initial': 'Anne Therese', - 'name': 'Who cooks today', + 'name': 'Cook today', 'options': ['Paulus', 'Anne Therese']}}}) # Set up input boolean bootstrap.setup_component( @@ -144,6 +144,11 @@ def setup(hass, config): {'input_boolean': {'notify': {'icon': 'mdi:car', 'initial': False, 'name': 'Notify Anne Therese is home'}}}) + # Set up weblink + bootstrap.setup_component( + hass, 'weblink', + {'weblink': {'entities': [{'name': 'Router', + 'url': 'http://192.168.1.1'}]}}) # Setup configurator configurator_ids = [] diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 4278d3db655..240ac532e5c 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,6 +17,7 @@ from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity import Entity +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -129,8 +130,7 @@ def setup(hass, config): except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up platform %s', p_type) - for p_type, p_config in \ - config_per_platform(config, DOMAIN, _LOGGER): + for p_type, p_config in config_per_platform(config, DOMAIN): setup_platform(p_type, p_config) def device_tracker_discovered(service, info): diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index d754156f217..0998e227857 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -6,28 +6,27 @@ https://home-assistant.io/components/device_tracker.mqtt/ """ import logging +import voluptuous as vol + import homeassistant.components.mqtt as mqtt -from homeassistant import util +from homeassistant.components.mqtt import CONF_QOS +import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['mqtt'] -CONF_QOS = 'qos' CONF_DEVICES = 'devices' -DEFAULT_QOS = 0 - _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, +}) + def setup_scanner(hass, config, see): """Setup the MQTT tracker.""" - devices = config.get(CONF_DEVICES) - qos = util.convert(config.get(CONF_QOS), int, DEFAULT_QOS) - - if not isinstance(devices, dict): - _LOGGER.error('Expected %s to be a dict, found %s', CONF_DEVICES, - devices) - return False + devices = config[CONF_DEVICES] + qos = config[CONF_QOS] dev_id_lookup = {} diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 709312acc0b..eb41aec1ebc 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -11,6 +11,7 @@ from collections import defaultdict import homeassistant.components.mqtt as mqtt from homeassistant.const import STATE_HOME +from homeassistant.util import convert DEPENDENCIES = ['mqtt'] @@ -46,8 +47,8 @@ def setup_scanner(hass, config, see): return if (not isinstance(data, dict) or data.get('_type') != 'location') or ( - 'acc' in data and max_gps_accuracy is not None and data[ - 'acc'] > max_gps_accuracy): + max_gps_accuracy is not None and + convert(data.get('acc'), float, 0.0) > max_gps_accuracy): return dev_id, kwargs = _parse_see_args(topic, data) @@ -79,6 +80,11 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return + if data.get('desc') is None: + _LOGGER.error( + "Location missing from `enter/exit` message - " + "please turn `Share` on in OwnTracks app") + return # OwnTracks uses - at the start of a beacon zone # to switch on 'hold mode' - ignore this location = data['desc'].lstrip("-") diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 2d58c54ac0d..7daf513316e 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -15,7 +15,7 @@ from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED) DOMAIN = "discovery" -REQUIREMENTS = ['netdisco==0.5.5'] +REQUIREMENTS = ['netdisco==0.6.1'] SCAN_INTERVAL = 300 # seconds @@ -26,6 +26,7 @@ SERVICE_NETGEAR = 'netgear_router' SERVICE_SONOS = 'sonos' SERVICE_PLEX = 'plex_mediaserver' SERVICE_SQUEEZEBOX = 'logitech_mediaserver' +SERVICE_PANASONIC_VIERA = 'panasonic_viera' SERVICE_HANDLERS = { SERVICE_WEMO: "wemo", @@ -35,6 +36,7 @@ SERVICE_HANDLERS = { SERVICE_SONOS: 'media_player', SERVICE_PLEX: 'media_player', SERVICE_SQUEEZEBOX: 'media_player', + SERVICE_PANASONIC_VIERA: 'media_player', } diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ba2a06130b..c1025fd1657 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -66,10 +66,6 @@ def _handle_get_api_bootstrap(handler, path_match, data): def _handle_get_root(handler, path_match, data): """Render the frontend.""" - handler.send_response(HTTP_OK) - handler.send_header('Content-type', 'text/html; charset=utf-8') - handler.end_headers() - if handler.server.development: app_url = "home-assistant-polymer/src/home-assistant.html" else: @@ -86,7 +82,9 @@ def _handle_get_root(handler, path_match, data): template_html = template_html.replace('{{ auth }}', auth) template_html = template_html.replace('{{ icons }}', mdi_version.VERSION) - handler.wfile.write(template_html.encode("UTF-8")) + handler.send_response(HTTP_OK) + handler.write_content(template_html.encode("UTF-8"), + 'text/html; charset=utf-8') def _handle_get_service_worker(handler, path_match, data): diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 3103cf36027..5f27f606cb8 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """DO NOT MODIFY. Auto-generated by build_frontend script.""" -VERSION = "49974cb3bb443751f7548e4e3b353304" +VERSION = "4062ab87999fd5e382074ba9d7a880d7" diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/COPYRIGHT.txt b/homeassistant/components/frontend/www_static/fonts/roboto/COPYRIGHT.txt new file mode 100644 index 00000000000..a7ef69930cb --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/roboto/COPYRIGHT.txt @@ -0,0 +1 @@ +Copyright 2011 Google Inc. All Rights Reserved. \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/DESCRIPTION.en_us.html b/homeassistant/components/frontend/www_static/fonts/roboto/DESCRIPTION.en_us.html new file mode 100644 index 00000000000..3a6834fd4c4 --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/roboto/DESCRIPTION.en_us.html @@ -0,0 +1,17 @@ +

Roboto has a dual nature. It has a mechanical skeleton and the forms are +largely geometric. At the same time, the font features friendly and open +curves. While some grotesks distort their letterforms to force a rigid rhythm, +Roboto doesn’t compromise, allowing letters to be settled into their natural +width. This makes for a more natural reading rhythm more commonly found in +humanist and serif types.

+ +

This is the normal family, which can be used alongside the +Roboto Condensed family and the +Roboto Slab family.

+ +

+Updated January 14 2015: +Christian Robertson and the Material Design team unveiled the latest version of Roboto at Google I/O last year, and it is now available from Google Fonts. +Existing websites using Roboto via Google Fonts will start using the latest version automatically. +If you have installed the fonts on your computer, please download them again and re-install. +

\ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/LICENSE.txt b/homeassistant/components/frontend/www_static/fonts/roboto/LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/roboto/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/METADATA.json b/homeassistant/components/frontend/www_static/fonts/roboto/METADATA.json new file mode 100644 index 00000000000..061bc67688b --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/roboto/METADATA.json @@ -0,0 +1,129 @@ +{ + "name": "Roboto", + "designer": "Christian Robertson", + "license": "Apache2", + "visibility": "External", + "category": "Sans Serif", + "size": 86523, + "fonts": [ + { + "name": "Roboto", + "style": "normal", + "weight": 100, + "filename": "Roboto-Thin.ttf", + "postScriptName": "Roboto-Thin", + "fullName": "Roboto Thin", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 100, + "filename": "Roboto-ThinItalic.ttf", + "postScriptName": "Roboto-ThinItalic", + "fullName": "Roboto Thin Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "normal", + "weight": 300, + "filename": "Roboto-Light.ttf", + "postScriptName": "Roboto-Light", + "fullName": "Roboto Light", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 300, + "filename": "Roboto-LightItalic.ttf", + "postScriptName": "Roboto-LightItalic", + "fullName": "Roboto Light Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "normal", + "weight": 400, + "filename": "Roboto-Regular.ttf", + "postScriptName": "Roboto-Regular", + "fullName": "Roboto", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 400, + "filename": "Roboto-Italic.ttf", + "postScriptName": "Roboto-Italic", + "fullName": "Roboto Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "normal", + "weight": 500, + "filename": "Roboto-Medium.ttf", + "postScriptName": "Roboto-Medium", + "fullName": "Roboto Medium", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 500, + "filename": "Roboto-MediumItalic.ttf", + "postScriptName": "Roboto-MediumItalic", + "fullName": "Roboto Medium Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "normal", + "weight": 700, + "filename": "Roboto-Bold.ttf", + "postScriptName": "Roboto-Bold", + "fullName": "Roboto Bold", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 700, + "filename": "Roboto-BoldItalic.ttf", + "postScriptName": "Roboto-BoldItalic", + "fullName": "Roboto Bold Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "normal", + "weight": 900, + "filename": "Roboto-Black.ttf", + "postScriptName": "Roboto-Black", + "fullName": "Roboto Black", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto", + "style": "italic", + "weight": 900, + "filename": "Roboto-BlackItalic.ttf", + "postScriptName": "Roboto-BlackItalic", + "fullName": "Roboto Black Italic", + "copyright": "Copyright 2011 Google Inc. All Rights Reserved." + } + ], + "subsets": [ + "cyrillic", + "cyrillic-ext", + "greek", + "greek-ext", + "latin", + "latin-ext", + "menu", + "vietnamese" + ], + "dateAdded": "2013-01-09" +} diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Black.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Black.ttf new file mode 100644 index 00000000000..fbde625d403 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Black.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BlackItalic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BlackItalic.ttf new file mode 100644 index 00000000000..60f7782a2e4 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BlackItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Bold.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Bold.ttf new file mode 100644 index 00000000000..a355c27cde0 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Bold.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BoldItalic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BoldItalic.ttf new file mode 100644 index 00000000000..3c9a7a37361 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-BoldItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Italic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Italic.ttf new file mode 100644 index 00000000000..ff6046d5bfa Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Italic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Light.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Light.ttf new file mode 100644 index 00000000000..94c6bcc67e0 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Light.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-LightItalic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-LightItalic.ttf new file mode 100644 index 00000000000..04cc0023020 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-LightItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Medium.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Medium.ttf new file mode 100644 index 00000000000..39c63d74617 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Medium.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-MediumItalic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-MediumItalic.ttf new file mode 100644 index 00000000000..dc743f0a66c Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-MediumItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Regular.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Regular.ttf new file mode 100644 index 00000000000..8c082c8de09 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Regular.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Thin.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Thin.ttf new file mode 100644 index 00000000000..d69555029c3 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-Thin.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-ThinItalic.ttf b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-ThinItalic.ttf new file mode 100644 index 00000000000..07172ff666a Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/roboto/Roboto-ThinItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/DESCRIPTION.en_us.html b/homeassistant/components/frontend/www_static/fonts/robotomono/DESCRIPTION.en_us.html new file mode 100644 index 00000000000..eb6ba3a2e3c --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/robotomono/DESCRIPTION.en_us.html @@ -0,0 +1,17 @@ +

+Roboto Mono is a monospaced addition to the Roboto type family. +Like the other members of the Roboto family, the fonts are optimized for readability on screens across a wide variety of devices and reading environments. +While the monospaced version is related to its variable width cousin, it doesn’t hesitate to change forms to better fit the constraints of a monospaced environment. +For example, narrow glyphs like ‘I’, ‘l’ and ‘i’ have added serifs for more even texture while wider glyphs are adjusted for weight. +Curved caps like ‘C’ and ‘O’ take on the straighter sides from Roboto Condensed. +

+ +

+Special consideration is given to glyphs important for reading and writing software source code. +Letters with similar shapes are easy to tell apart. +Digit ‘1’, lowercase ‘l’ and capital ‘I’ are easily differentiated as are zero and the letter ‘O’. +Punctuation important for code has also been considered. +For example, the curly braces ‘{ }’ have exaggerated points to clearly differentiate them from parenthesis ‘( )’ and braces ‘[ ]’. +Periods and commas are also exaggerated to identify them more quickly. +The scale and weight of symbols commonly used as operators have also been optimized. +

diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/LICENSE.txt b/homeassistant/components/frontend/www_static/fonts/robotomono/LICENSE.txt new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/robotomono/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/METADATA.json b/homeassistant/components/frontend/www_static/fonts/robotomono/METADATA.json new file mode 100644 index 00000000000..a2a212bfa8f --- /dev/null +++ b/homeassistant/components/frontend/www_static/fonts/robotomono/METADATA.json @@ -0,0 +1,111 @@ +{ + "name": "Roboto Mono", + "designer": "Christian Robertson", + "license": "Apache2", + "visibility": "External", + "category": "Monospace", + "size": 51290, + "fonts": [ + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Thin", + "fullName": "Roboto Mono Thin", + "style": "normal", + "weight": 100, + "filename": "RobotoMono-Thin.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-ThinItalic", + "fullName": "Roboto Mono Thin Italic", + "style": "italic", + "weight": 100, + "filename": "RobotoMono-ThinItalic.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Light", + "fullName": "Roboto Mono Light", + "style": "normal", + "weight": 300, + "filename": "RobotoMono-Light.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-LightItalic", + "fullName": "Roboto Mono Light Italic", + "style": "italic", + "weight": 300, + "filename": "RobotoMono-LightItalic.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Regular", + "fullName": "Roboto Mono", + "style": "normal", + "weight": 400, + "filename": "RobotoMono-Regular.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Italic", + "fullName": "Roboto Mono Italic", + "style": "italic", + "weight": 400, + "filename": "RobotoMono-Italic.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Medium", + "fullName": "Roboto Mono Medium", + "style": "normal", + "weight": 500, + "filename": "RobotoMono-Medium.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-MediumItalic", + "fullName": "Roboto Mono Medium Italic", + "style": "italic", + "weight": 500, + "filename": "RobotoMono-MediumItalic.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-Bold", + "fullName": "Roboto Mono Bold", + "style": "normal", + "weight": 700, + "filename": "RobotoMono-Bold.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + }, + { + "name": "Roboto Mono", + "postScriptName": "RobotoMono-BoldItalic", + "fullName": "Roboto Mono Bold Italic", + "style": "italic", + "weight": 700, + "filename": "RobotoMono-BoldItalic.ttf", + "copyright": "Copyright 2015 Google Inc. All Rights Reserved." + } + ], + "subsets": [ + "cyrillic", + "cyrillic-ext", + "greek", + "greek-ext", + "latin", + "latin-ext", + "menu", + "vietnamese" + ], + "dateAdded": "2015-05-13" +} diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Bold.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Bold.ttf new file mode 100644 index 00000000000..c6a81a570c2 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Bold.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-BoldItalic.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-BoldItalic.ttf new file mode 100644 index 00000000000..b2261d6649a Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-BoldItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Italic.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Italic.ttf new file mode 100644 index 00000000000..6e4001e1967 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Italic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Light.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Light.ttf new file mode 100644 index 00000000000..5ca4889ebac Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Light.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-LightItalic.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-LightItalic.ttf new file mode 100644 index 00000000000..db7c368471c Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-LightItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Medium.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Medium.ttf new file mode 100644 index 00000000000..0bcdc740c66 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Medium.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-MediumItalic.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-MediumItalic.ttf new file mode 100644 index 00000000000..b4f5e20e3d9 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-MediumItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Regular.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Regular.ttf new file mode 100644 index 00000000000..495a82ce92e Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Regular.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Thin.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Thin.ttf new file mode 100644 index 00000000000..1b5085eed8c Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-Thin.ttf differ diff --git a/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-ThinItalic.ttf b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-ThinItalic.ttf new file mode 100644 index 00000000000..dfa1d139ba8 Binary files /dev/null and b/homeassistant/components/frontend/www_static/fonts/robotomono/RobotoMono-ThinItalic.ttf differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 76db8ee085d..8a6c2285480 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,585 +1,493 @@ -