diff --git a/home-assistant.conf.default b/home-assistant.conf.default index 5227d34ef99..fa02152cb83 100644 --- a/home-assistant.conf.default +++ b/home-assistant.conf.default @@ -1,20 +1,15 @@ -[common] +[homeassistant] latitude=32.87336 longitude=-117.22743 [http] api_password=mypass -[light.hue] -host=192.168.1.2 +[light] +type=hue -[device_tracker.tomato] -host=192.168.1.1 -username=admin -password=PASSWORD -http_id=aaaaaaaaaaaaaaa - -[device_tracker.netgear] +[device_tracker] +type=netgear host=192.168.1.1 username=admin password=PASSWORD @@ -38,9 +33,9 @@ download_dir=downloads # A comma seperated list of states that have to be tracked as a single group # Grouped states should share the same states (ON/OFF or HOME/NOT_HOME) -[group] -living_room=light.Bowl,light.Ceiling,light.TV_back_light -bedroom=light.Bed_light +# [group] +# living_room=light.Bowl,light.Ceiling,light.TV_back_light +# bedroom=light.Bed_light [process] # items are which processes to look for: = diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index b7962442501..3509b64c169 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -30,6 +30,14 @@ ATTR_NOW = "now" ATTR_DOMAIN = "domain" ATTR_SERVICE = "service" +CONF_LATITUDE = "latitude" +CONF_LONGITUDE = "longitude" +CONF_TYPE = "type" +CONF_HOST = "host" +CONF_HOSTS = "hosts" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" + # How often time_changed event should fire TIMER_INTERVAL = 10 # seconds @@ -334,7 +342,7 @@ class EventBus(object): for func in listeners: self._pool.add_job(JobPriority.from_event_type(event_type), - (func, event)) + (func, event)) def listen(self, event_type, listener): """ Listen for all events or events of a specific type. @@ -553,8 +561,8 @@ class ServiceRegistry(object): service_call = ServiceCall(domain, service, service_data) self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._services[domain][service], - service_call)) + (self._services[domain][service], + service_call)) class Timer(threading.Thread): diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5d089aa31c6..3a54b6f26ab 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,20 +7,146 @@ After bootstrapping you can add your own components or start by calling homeassistant.start_home_assistant(bus) """ -import importlib import configparser import logging +from collections import defaultdict +from itertools import chain import homeassistant -import homeassistant.components as components +import homeassistant.components as core_components +import homeassistant.components.group as group -# pylint: disable=too-many-branches,too-many-locals,too-many-statements -def from_config_file(config_path, enable_logging=True): - """ Starts home assistant with all possible functionality - based on a config file. - Will return a tuple (bus, statemachine). """ +# pylint: disable=too-many-branches +def from_config_dict(config, hass=None): + """ + Tries to configure Home Assistant from a config dict. + Dynamically loads required components and its dependencies. + """ + if hass is None: + hass = homeassistant.HomeAssistant() + + logger = logging.getLogger(__name__) + + # Make a copy because we are mutating it. + # Convert it to defaultdict so components can always have config dict + config = defaultdict(dict, config) + + # List of loaded components + components = {} + + # List of components to validate + to_validate = [] + + # List of validated components + validated = [] + + # List of components we are going to load + to_load = [key for key in config.keys() if key != homeassistant.DOMAIN] + + # Load required components + while to_load: + domain = to_load.pop() + + component = core_components.get_component(domain, logger) + + # if None it does not exist, error already thrown by get_component + if component is not None: + components[domain] = component + + # Special treatment for GROUP, we want to load it as late as + # possible. We do this by loading it if all other to be loaded + # modules depend on it. + if component.DOMAIN == group.DOMAIN: + pass + + # Components with no dependencies are valid + elif not component.DEPENDENCIES: + validated.append(domain) + + # If dependencies we'll validate it later + else: + to_validate.append(domain) + + # Make sure to load all dependencies that are not being loaded + for dependency in component.DEPENDENCIES: + if dependency not in chain(components.keys(), to_load): + to_load.append(dependency) + + # Validate dependencies + group_added = False + + while to_validate: + newly_validated = [] + + for domain in to_validate: + if all(domain in validated for domain + in components[domain].DEPENDENCIES): + + newly_validated.append(domain) + + # We validated new domains this iteration, add them to validated + if newly_validated: + + # Add newly validated domains to validated + validated.extend(newly_validated) + + # remove domains from to_validate + for domain in newly_validated: + to_validate.remove(domain) + + newly_validated.clear() + + # Nothing validated this iteration. Add group dependency and try again. + elif not group_added: + group_added = True + validated.append(group.DOMAIN) + + # Group has already been added and we still can't validate all. + # Report missing deps as error and skip loading of these domains + else: + for domain in to_validate: + missing_deps = [dep for dep in components[domain].DEPENDENCIES + if dep not in validated] + + logger.error( + "Could not validate all dependencies for {}: {}".format( + domain, ", ".join(missing_deps))) + + break + + # Setup the components + if core_components.setup(hass, config): + logger.info("Home Assistant core initialized") + + for domain in validated: + component = components[domain] + + try: + if component.setup(hass, config): + logger.info("component {} initialized".format(domain)) + else: + logger.error( + "component {} failed to initialize".format(domain)) + + except Exception: # pylint: disable=broad-except + logger.exception( + "Error during setup of component {}".format(domain)) + + else: + logger.error(("Home Assistant core failed to initialize. " + "Further initialization aborted.")) + + return hass + + +def from_config_file(config_path, hass=None, enable_logging=True): + """ + Reads the configuration file and tries to start all the required + functionality. Will add functionality to 'hass' parameter if given, + instantiates a new Home Assistant object if 'hass' is not given. + """ if enable_logging: # Setup the logging for home assistant. logging.basicConfig(level=logging.INFO) @@ -34,196 +160,16 @@ def from_config_file(config_path, enable_logging=True): datefmt='%H:%M %d-%m-%y')) logging.getLogger('').addHandler(err_handler) - # Start the actual bootstrapping - logger = logging.getLogger(__name__) - - statusses = [] - # Read config config = configparser.ConfigParser() config.read(config_path) - # Init core - hass = homeassistant.HomeAssistant() + config_dict = {} - has_opt = config.has_option - get_opt = config.get - has_section = config.has_section - add_status = lambda name, result: statusses.append((name, result)) - load_module = lambda module: importlib.import_module( - 'homeassistant.components.'+module) + for section in config.sections(): + config_dict[section] = {} - def get_opt_safe(section, option, default=None): - """ Failure proof option retriever. """ - try: - return config.get(section, option) - except (configparser.NoSectionError, configparser.NoOptionError): - return default + for key, val in config.items(section): + config_dict[section][key] = val - def get_hosts(section): - """ Helper method to retrieve hosts from config. """ - if has_opt(section, "hosts"): - return get_opt(section, "hosts").split(",") - else: - return None - - # Device scanner - dev_scan = None - - try: - # For the error message if not all option fields exist - opt_fields = "host, username, password" - - if has_section('device_tracker.tomato'): - device_tracker = load_module('device_tracker') - - dev_scan_name = "Tomato" - opt_fields += ", http_id" - - dev_scan = device_tracker.TomatoDeviceScanner( - get_opt('device_tracker.tomato', 'host'), - get_opt('device_tracker.tomato', 'username'), - get_opt('device_tracker.tomato', 'password'), - get_opt('device_tracker.tomato', 'http_id')) - - elif has_section('device_tracker.netgear'): - device_tracker = load_module('device_tracker') - - dev_scan_name = "Netgear" - - dev_scan = device_tracker.NetgearDeviceScanner( - get_opt('device_tracker.netgear', 'host'), - get_opt('device_tracker.netgear', 'username'), - get_opt('device_tracker.netgear', 'password')) - - elif has_section('device_tracker.luci'): - device_tracker = load_module('device_tracker') - - dev_scan_name = "Luci" - - dev_scan = device_tracker.LuciDeviceScanner( - get_opt('device_tracker.luci', 'host'), - get_opt('device_tracker.luci', 'username'), - get_opt('device_tracker.luci', 'password')) - - except configparser.NoOptionError: - # If one of the options didn't exist - logger.exception(("Error initializing {}DeviceScanner, " - "could not find one of the following config " - "options: {}".format(dev_scan_name, opt_fields))) - - add_status("Device Scanner - {}".format(dev_scan_name), False) - - if dev_scan: - add_status("Device Scanner - {}".format(dev_scan_name), - dev_scan.success_init) - - if not dev_scan.success_init: - dev_scan = None - - # Device Tracker - if dev_scan: - device_tracker.DeviceTracker(hass, dev_scan) - - add_status("Device Tracker", True) - - # Sun tracker - if has_opt("common", "latitude") and \ - has_opt("common", "longitude"): - - sun = load_module('sun') - - add_status("Sun", - sun.setup(hass, - get_opt("common", "latitude"), - get_opt("common", "longitude"))) - else: - sun = None - - # Chromecast - if has_section("chromecast"): - chromecast = load_module('chromecast') - - hosts = get_hosts("chromecast") - - add_status("Chromecast", chromecast.setup(hass, hosts)) - - # WeMo - if has_section("wemo"): - wemo = load_module('wemo') - - hosts = get_hosts("wemo") - - add_status("WeMo", wemo.setup(hass, hosts)) - - # Process tracking - if has_section("process"): - process = load_module('process') - - processes = dict(config.items('process')) - add_status("process", process.setup(hass, processes)) - - # Light control - if has_section("light.hue"): - light = load_module('light') - - light_control = light.HueLightControl(get_opt_safe("hue", "host")) - - add_status("Light - Hue", light_control.success_init) - - if light_control.success_init: - light.setup(hass, light_control) - else: - light_control = None - - else: - light_control = None - - if has_opt("downloader", "download_dir"): - downloader = load_module('downloader') - - add_status("Downloader", downloader.setup( - hass, get_opt("downloader", "download_dir"))) - - add_status("Core components", components.setup(hass)) - - if has_section('browser'): - add_status("Browser", load_module('browser').setup(hass)) - - if has_section('keyboard'): - add_status("Keyboard", load_module('keyboard').setup(hass)) - - # Init HTTP interface - if has_opt("http", "api_password"): - http = load_module('http') - - http.setup(hass, get_opt("http", "api_password")) - - add_status("HTTP", True) - - # Init groups - if has_section("group"): - group = load_module('group') - - for name, entity_ids in config.items("group"): - add_status("Group - {}".format(name), - group.setup(hass, name, entity_ids.split(","))) - - # Light trigger - if light_control and sun: - device_sun_light_trigger = load_module('device_sun_light_trigger') - - light_group = get_opt_safe("device_sun_light_trigger", "light_group") - light_profile = get_opt_safe("device_sun_light_trigger", - "light_profile") - - add_status("Device Sun Light Trigger", - device_sun_light_trigger.setup(hass, - light_group, light_profile)) - - for component, success_init in statusses: - status = "initialized" if success_init else "Failed to initialize" - - logger.info("{}: {}".format(component, status)) - - return hass + return from_config_dict(config_dict, hass) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index bb1f78a2d75..165705dc8dd 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -15,6 +15,7 @@ Each component should publish services only under its own domain. """ import itertools as it +import logging import importlib import homeassistant as ha @@ -44,23 +45,51 @@ SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_PREV_TRACK = "media_prev_track" -def _get_component(component): - """ Returns requested component. """ +def get_component(component, logger=None): + """ Tries to load specified component. + Only returns it if also found to be valid.""" try: - return importlib.import_module( + comp = importlib.import_module( 'homeassistant.components.{}'.format(component)) except ImportError: - # If we got a bogus component the input will fail + if logger: + logger.error( + "Failed to find component {}".format(component)) + return None + # Validation if component has required methods and attributes + errors = [] + + if not hasattr(comp, 'DOMAIN'): + errors.append("Missing DOMAIN attribute") + + if not hasattr(comp, 'DEPENDENCIES'): + errors.append("Missing DEPENDENCIES attribute") + + if not hasattr(comp, 'setup'): + errors.append("Missing setup method") + + if errors: + if logger: + logger.error("Found invalid component {}: {}".format( + component, ", ".join(errors))) + + return None + + else: + return comp + def is_on(hass, entity_id=None): """ Loads up the module to call the is_on method. If there is no entity id given we will check all. """ + logger = logging.getLogger(__name__) + if entity_id: - group = _get_component('group') + group = get_component('group', logger) entity_ids = group.expand_entity_ids([entity_id]) else: @@ -69,7 +98,7 @@ def is_on(hass, entity_id=None): for entity_id in entity_ids: domain = util.split_entity_id(entity_id)[0] - module = _get_component(domain) + module = get_component(domain, logger) try: if module.is_on(hass, entity_id): @@ -100,7 +129,7 @@ def extract_entity_ids(hass, service): entity_ids = [] if service.data and ATTR_ENTITY_ID in service.data: - group = _get_component('group') + group = get_component('group') # Entity ID attr can be a list or a string service_ent_id = service.data[ATTR_ENTITY_ID] @@ -117,7 +146,8 @@ def extract_entity_ids(hass, service): return entity_ids -def setup(hass): +# pylint: disable=unused-argument +def setup(hass, config): """ Setup general services related to homeassistant. """ def handle_turn_service(service): diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index d1d4272a6a2..556cbd683c9 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -6,11 +6,13 @@ Provides functionality to launch a webbrowser on the host machine. """ DOMAIN = "browser" +DEPENDENCIES = [] SERVICE_BROWSE_URL = "browse_url" -def setup(hass): +# pylint: disable=unused-argument +def setup(hass, config): """ Listen for browse_url events and open the url in the default webbrowser. """ diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index e60f0596179..b7350a5d981 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -10,6 +10,7 @@ import homeassistant.util as util import homeassistant.components as components DOMAIN = 'chromecast' +DEPENDENCIES = [] SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' @@ -100,7 +101,7 @@ def media_prev_track(hass, entity_id=None): # pylint: disable=too-many-locals, too-many-branches -def setup(hass, hosts=None): +def setup(hass, config): """ Listen for chromecast events. """ logger = logging.getLogger(__name__) @@ -113,8 +114,11 @@ def setup(hass, hosts=None): return False + if 'hosts' in config[DOMAIN]: + hosts = config[DOMAIN]['hosts'].split(",") + # If no hosts given, scan for chromecasts - if not hosts: + else: logger.info("Scanning for Chromecasts") hosts = pychromecast.discover_chromecasts() @@ -131,7 +135,7 @@ def setup(hass, hosts=None): casts[entity_id] = cast - except pychromecast.ConnectionError: + except pychromecast.ChromecastConnectionError: pass if not casts: diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index c7e3cf0172c..99ba0906be5 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -11,19 +11,26 @@ from datetime import datetime, timedelta import homeassistant.components as components from . import light, sun, device_tracker, group +DOMAIN = "device_sun_light_trigger" +DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun'] LIGHT_TRANSITION_TIME = timedelta(minutes=15) # Light profile to be used if none given LIGHT_PROFILE = 'relax' +CONF_LIGHT_PROFILE = 'light_profile' +CONF_LIGHT_GROUP = 'light_group' + # pylint: disable=too-many-branches -def setup(hass, light_group=None, light_profile=None): +def setup(hass, config): """ Triggers to turn lights on or off based on device precense. """ - light_group = light_group or light.GROUP_NAME_ALL_LIGHTS - light_profile = light_profile or LIGHT_PROFILE + light_group = config[DOMAIN].get(CONF_LIGHT_GROUP, + light.GROUP_NAME_ALL_LIGHTS) + + light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE) logger = logging.getLogger(__name__) @@ -61,8 +68,7 @@ def setup(hass, light_group=None, light_profile=None): def turn_light_on_before_sunset(light_id): """ Helper function to turn on lights slowly if there are devices home and the light is not on yet. """ - if (device_tracker.is_on(hass) and - not light.is_on(hass, light_id)): + if device_tracker.is_on(hass) and not light.is_on(hass, light_id): light.turn_on(hass, light_id, transition=LIGHT_TRANSITION_TIME.seconds, @@ -99,8 +105,8 @@ def setup(hass, light_group=None, light_profile=None): light_needed = not (lights_are_on or sun.is_on(hass)) # Specific device came home ? - if (entity != device_tracker.ENTITY_ID_ALL_DEVICES and - new_state.state == components.STATE_HOME): + if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \ + new_state.state == components.STATE_HOME: # These variables are needed for the elif check now = datetime.now() diff --git a/homeassistant/components/device_tracker.py b/homeassistant/components/device_tracker.py index 05e8a5e224b..f962c15793c 100644 --- a/homeassistant/components/device_tracker.py +++ b/homeassistant/components/device_tracker.py @@ -14,12 +14,14 @@ from datetime import datetime, timedelta import requests +import homeassistant as ha import homeassistant.util as util import homeassistant.components as components from homeassistant.components import group DOMAIN = "device_tracker" +DEPENDENCIES = [] SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" @@ -39,6 +41,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) # Filename to save known devices to KNOWN_DEVICES_FILE = "known_devices.csv" +CONF_HTTP_ID = "http_id" + def is_on(hass, entity_id=None): """ Returns if any or specified device is home. """ @@ -47,16 +51,69 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity, components.STATE_HOME) +def setup(hass, config): + """ Sets up the device tracker. """ + + logger = logging.getLogger(__name__) + + # We have flexible requirements for device tracker so + # we cannot use util.validate_config + + conf = config[DOMAIN] + + if not ha.CONF_TYPE in conf: + logger.error( + 'Missing required configuration item in {}: {}'.format( + DOMAIN, ha.CONF_TYPE)) + + return False + + fields = [ha.CONF_HOST, ha.CONF_USERNAME, ha.CONF_PASSWORD] + + router_type = conf[ha.CONF_TYPE] + + if router_type == 'tomato': + fields.append(CONF_HTTP_ID) + + scanner = TomatoDeviceScanner + + elif router_type == 'netgear': + scanner = NetgearDeviceScanner + + elif router_type == 'luci': + scanner = LuciDeviceScanner + + else: + logger.error('Found unknown router type {}'.format(router_type)) + + return False + + if not util.validate_config(config, {DOMAIN: fields}, logger): + return False + + device_scanner = scanner(conf) + + if not device_scanner.success_init: + logger.error( + "Failed to initialize device scanner for {}".format(router_type)) + + return False + + DeviceTracker(hass, device_scanner) + + return True + + # pylint: disable=too-many-instance-attributes class DeviceTracker(object): """ Class that tracks which devices are home and which are not. """ - def __init__(self, hass, device_scanner, error_scanning=None): + def __init__(self, hass, device_scanner): self.states = hass.states self.device_scanner = device_scanner - self.error_scanning = error_scanning or TIME_SPAN_FOR_ERROR_IN_SCANNING + self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING self.logger = logging.getLogger(__name__) @@ -84,7 +141,7 @@ class DeviceTracker(object): self.update_devices() - group.setup(hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids) + group.setup_group(hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids) @property def device_entity_ids(self): @@ -164,8 +221,8 @@ class DeviceTracker(object): except IOError: self.logger.exception(( "DeviceTracker:Error updating {}" - "with {} new devices").format( - KNOWN_DEVICES_FILE, len(unknown_devices))) + "with {} new devices").format(KNOWN_DEVICES_FILE, + len(unknown_devices))) self.lock.release() @@ -223,8 +280,8 @@ class DeviceTracker(object): # Remove entities that are no longer maintained new_entity_ids = set([known_devices[device]['entity_id'] - for device in known_devices - if known_devices[device]['track']]) + for device in known_devices + if known_devices[device]['track']]) for entity_id in \ self.device_entity_ids - new_entity_ids: @@ -246,8 +303,8 @@ class DeviceTracker(object): self.invalid_known_devices_file = True self.logger.warning(( "Invalid {} found. " - "We won't update it with new found devices."). - format(KNOWN_DEVICES_FILE)) + "We won't update it with new found devices." + ).format(KNOWN_DEVICES_FILE)) finally: self.lock.release() @@ -261,7 +318,10 @@ class TomatoDeviceScanner(object): http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/ """ - def __init__(self, host, username, password, http_id): + def __init__(self, config): + host, http_id = config['host'], config['http_id'] + username, password = config['username'], config['password'] + self.req = requests.Request('POST', 'http://{}/update.cgi'.format(host), data={'_http_id': http_id, @@ -309,8 +369,8 @@ class TomatoDeviceScanner(object): self.lock.acquire() # if date_updated is None or the date is too old we scan for new data - if (not self.date_updated or datetime.now() - self.date_updated > - MIN_TIME_BETWEEN_SCANS): + if not self.date_updated or \ + datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: self.logger.info("Tomato:Scanning") @@ -380,7 +440,10 @@ class TomatoDeviceScanner(object): class NetgearDeviceScanner(object): """ This class queries a Netgear wireless router using the SOAP-api. """ - def __init__(self, host, username, password): + def __init__(self, config): + host = config['host'] + username, password = config['username'], config['password'] + self.logger = logging.getLogger(__name__) self.date_updated = None self.last_results = [] @@ -442,8 +505,8 @@ class NetgearDeviceScanner(object): with self.lock: # if date_updated is None or the date is too old we scan for # new data - if (not self.date_updated or datetime.now() - self.date_updated > - MIN_TIME_BETWEEN_SCANS): + if not self.date_updated or \ + datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: self.logger.info("Netgear:Scanning") @@ -470,7 +533,10 @@ class LuciDeviceScanner(object): (Currently, we do only wifi iwscan, and no DHCP lease access.) """ - def __init__(self, host, username, password): + def __init__(self, config): + host = config['host'] + username, password = config['username'], config['password'] + self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") self.logger = logging.getLogger(__name__) @@ -554,8 +620,8 @@ class LuciDeviceScanner(object): with self.lock: # if date_updated is None or the date is too old we scan # for new data - if (not self.date_updated or datetime.now() - self.date_updated > - MIN_TIME_BETWEEN_SCANS): + if not self.date_updated or \ + datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS: self.logger.info("Checking ARP") diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 1f109ec8b7f..1f979063a4a 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -12,15 +12,18 @@ import threading import homeassistant.util as util DOMAIN = "downloader" +DEPENDENCIES = [] SERVICE_DOWNLOAD_FILE = "download_file" ATTR_URL = "url" ATTR_SUBDIR = "subdir" +CONF_DOWNLOAD_DIR = 'download_dir' + # pylint: disable=too-many-branches -def setup(hass, download_path): +def setup(hass, config): """ Listens for download events to download files. """ logger = logging.getLogger(__name__) @@ -33,6 +36,11 @@ def setup(hass, download_path): return False + if not util.validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger): + return False + + download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] + if not os.path.isdir(download_path): logger.error( @@ -106,8 +114,7 @@ def setup(hass, download_path): final_path = "{}_{}.{}".format(path, tries, ext) - logger.info("{} -> {}".format( - url, final_path)) + logger.info("{} -> {}".format(url, final_path)) with open(final_path, 'wb') as fil: for chunk in req.iter_content(1024): diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 9b8d989b78b..0bf336c8e1d 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -13,6 +13,7 @@ from homeassistant.components import (STATE_ON, STATE_OFF, ATTR_ENTITY_ID) DOMAIN = "group" +DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -89,13 +90,21 @@ def get_entity_ids(hass, entity_id, domain_filter=None): # pylint: disable=too-many-branches, too-many-locals -def setup(hass, name, entity_ids): +def setup(hass, config): + """ Sets up all groups found definded in the configuration. """ + + for name, entity_ids in config[DOMAIN].items(): + entity_ids = entity_ids.split(",") + + setup_group(hass, name, entity_ids) + + return True + + +def setup_group(hass, name, entity_ids): """ Sets up a group state that is the combined state of several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ - # Convert entity_ids to a list incase it is an iterable - entity_ids = list(entity_ids) - logger = logging.getLogger(__name__) # Loop over the given entities to: @@ -118,9 +127,11 @@ def setup(hass, name, entity_ids): else: # We did not find a matching group_type - errors.append("Found unexpected state '{}'".format( - name, state.state)) + errors.append( + "Entity {} has ungroupable state '{}'".format( + name, state.state)) + # Stop check all other entity IDs and report as error break # Check if entity exists @@ -134,43 +145,48 @@ def setup(hass, name, entity_ids): entity_id, state.state, group_off, group_on)) # Keep track of the group state to init later on - elif group_state == group_off and state.state == group_on: + elif state.state == group_on: group_state = group_on + if group_type is None and not errors: + errors.append('Unable to determine group type for {}'.format(name)) + if errors: - logger.error("Error setting up state group {}: {}".format( + logger.error("Error setting up group {}: {}".format( name, ", ".join(errors))) return False - group_entity_id = ENTITY_ID_FORMAT.format(name) - state_attr = {ATTR_ENTITY_ID: entity_ids} + else: + group_entity_id = ENTITY_ID_FORMAT.format(name) + state_attr = {ATTR_ENTITY_ID: entity_ids} - # pylint: disable=unused-argument - def update_group_state(entity_id, old_state, new_state): - """ Updates the group state based on a state change by a tracked - entity. """ + # pylint: disable=unused-argument + def update_group_state(entity_id, old_state, new_state): + """ Updates the group state based on a state change by + a tracked entity. """ - cur_group_state = hass.states.get(group_entity_id).state + cur_gr_state = hass.states.get(group_entity_id).state - # if cur_group_state = OFF and new_state = ON: set ON - # if cur_group_state = ON and new_state = OFF: research - # else: ignore + # if cur_gr_state = OFF and new_state = ON: set ON + # if cur_gr_state = ON and new_state = OFF: research + # else: ignore - if cur_group_state == group_off and new_state.state == group_on: + if cur_gr_state == group_off and new_state.state == group_on: - hass.states.set(group_entity_id, group_on, state_attr) + hass.states.set(group_entity_id, group_on, state_attr) - elif cur_group_state == group_on and new_state.state == group_off: + elif cur_gr_state == group_on and new_state.state == group_off: - # Check if any of the other states is still on - if not any([hass.states.is_state(ent_id, group_on) - for ent_id in entity_ids if entity_id != ent_id]): - hass.states.set(group_entity_id, group_off, state_attr) + # Check if any of the other states is still on + if not any([hass.states.is_state(ent_id, group_on) + for ent_id in entity_ids + if entity_id != ent_id]): + hass.states.set(group_entity_id, group_off, state_attr) - for entity_id in entity_ids: - hass.track_state_change(entity_id, update_group_state) + for entity_id in entity_ids: + hass.track_state_change(entity_id, update_group_state) - hass.states.set(group_entity_id, group_state, state_attr) + hass.states.set(group_entity_id, group_state, state_attr) - return True + return True diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index df0c38890b2..362e9cdb0d9 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -89,6 +89,8 @@ import homeassistant.remote as rem import homeassistant.util as util from homeassistant.components import (STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) +DOMAIN = "http" +DEPENDENCIES = [] HTTP_OK = 200 HTTP_CREATED = 201 @@ -120,6 +122,10 @@ DOMAIN_ICONS = { "downloader": "glyphicon-download-alt" } +CONF_API_PASSWORD = "api_password" +CONF_SERVER_HOST = "server_host" +CONF_SERVER_PORT = "server_port" + def _get_domain_icon(domain): """ Returns HTML that shows domain icon. """ @@ -127,12 +133,19 @@ def _get_domain_icon(domain): DOMAIN_ICONS.get(domain, "")) -def setup(hass, api_password, server_port=None, server_host=None): +def setup(hass, config): """ Sets up the HTTP API and debug interface. """ - server_port = server_port or rem.SERVER_PORT + + if not util.validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, + logging.getLogger(__name__)): + return False + + api_password = config[DOMAIN]['api_password'] # If no server host is given, accept all incoming requests - server_host = server_host or '0.0.0.0' + server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') + + server_port = config[DOMAIN].get(CONF_SERVER_PORT, rem.SERVER_PORT) server = HomeAssistantHTTPServer((server_host, server_port), RequestHandler, hass, api_password) @@ -147,6 +160,8 @@ def setup(hass, api_password, server_port=None, server_host=None): hass.local_api = \ rem.API(util.get_local_ip(), api_password, server_port) + return True + class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): """ Handle HTTP requests in a threaded fashion. """ @@ -213,7 +228,7 @@ class RequestHandler(BaseHTTPRequestHandler): # /event_forwarding ('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'), ('DELETE', rem.URL_API_EVENT_FORWARD, - '_handle_delete_api_event_forward'), + '_handle_delete_api_event_forward'), # Statis files ('GET', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'), @@ -407,7 +422,7 @@ class RequestHandler(BaseHTTPRequestHandler): "href='#'>").format(action, state.entity_id)) write("{}{}".format( - attributes, util.datetime_to_str(state.last_changed))) + attributes, util.datetime_to_str(state.last_changed))) # Change state form write((" - MIN_TIME_BETWEEN_SCANS): + if force_reload or \ + datetime.now() - update_lights_state.last_updated > \ + MIN_TIME_BETWEEN_SCANS: logger.info("Updating light status") update_lights_state.last_updated = datetime.now() @@ -206,7 +223,7 @@ def setup(hass, light_control): return False # Track all lights in a group - group.setup(hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) + group.setup_group(hass, GROUP_NAME_ALL_LIGHTS, light_to_ent.values()) # Load built-in profiles and custom profiles profile_paths = [os.path.dirname(__file__), os.getcwd()] @@ -336,9 +353,11 @@ def _hue_to_light_state(info): class HueLightControl(object): """ Class to interface with the Hue light system. """ - def __init__(self, host=None): + def __init__(self, config): logger = logging.getLogger(__name__) + host = config.get(ha.CONF_HOST, None) + try: import phue except ImportError: diff --git a/homeassistant/components/process.py b/homeassistant/components/process.py index 1a88bc6b699..7b57f4a626d 100644 --- a/homeassistant/components/process.py +++ b/homeassistant/components/process.py @@ -26,12 +26,13 @@ from homeassistant.components import STATE_ON, STATE_OFF import homeassistant.util as util DOMAIN = 'process' +DEPENDENCIES = [] ENTITY_ID_FORMAT = DOMAIN + '.{}' PS_STRING = 'ps awx' -def setup(hass, processes): +def setup(hass, config): """ Sets up a check if specified processes are running. processes: dict mapping entity id to substring to search for @@ -39,7 +40,7 @@ def setup(hass, processes): """ entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring - for pname, pstring in processes.items()} + for pname, pstring in config[DOMAIN].items()} # pylint: disable=unused-argument def update_process_states(time): diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index c728ce39d81..5b775695cdf 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -7,8 +7,11 @@ Provides functionality to keep track of the sun. import logging from datetime import timedelta +import homeassistant as ha import homeassistant.util as util +DEPENDENCIES = [] +DOMAIN = "sun" ENTITY_ID = "sun.sun" STATE_ABOVE_HORIZON = "above_horizon" @@ -49,10 +52,16 @@ def next_rising(hass): return None -def setup(hass, latitude, longitude): +def setup(hass, config): """ Tracks the state of the sun. """ logger = logging.getLogger(__name__) + if not util.validate_config(config, + {ha.DOMAIN: [ha.CONF_LATITUDE, + ha.CONF_LONGITUDE]}, + logger): + return False + try: import ephem except ImportError: @@ -61,12 +70,15 @@ def setup(hass, latitude, longitude): sun = ephem.Sun() # pylint: disable=no-member + latitude = config[ha.DOMAIN][ha.CONF_LATITUDE] + longitude = config[ha.DOMAIN][ha.CONF_LONGITUDE] + def update_sun_state(now): # pylint: disable=unused-argument """ Method to update the current state of the sun and set time of next setting and rising. """ observer = ephem.Observer() - observer.lat = latitude - observer.long = longitude + observer.lat = latitude # pylint: disable=assigning-non-slot + observer.long = longitude # pylint: disable=assigning-non-slot next_rising_dt = ephem.localtime(observer.next_rising(sun)) next_setting_dt = ephem.localtime(observer.next_setting(sun)) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 1892db66fdf..521c8d8559c 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -4,12 +4,14 @@ Component to interface with WeMo devices on the network. import logging from datetime import datetime, timedelta +import homeassistant as ha import homeassistant.util as util from homeassistant.components import (group, extract_entity_ids, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) DOMAIN = 'wemo' +DEPENDENCIES = [] GROUP_NAME_ALL_WEMOS = 'all_wemos' ENTITY_ID_ALL_WEMOS = group.ENTITY_ID_FORMAT.format( @@ -47,7 +49,7 @@ def turn_off(hass, entity_id=None): # pylint: disable=too-many-branches -def setup(hass, hosts=None): +def setup(hass, config): """ Track states and offer events for WeMo switches. """ logger = logging.getLogger(__name__) @@ -61,10 +63,10 @@ def setup(hass, hosts=None): return False - if hosts: + if ha.CONF_HOSTS in config[DOMAIN]: devices = [] - for host in hosts: + for host in config[DOMAIN][ha.CONF_HOSTS].split(","): device = pywemo.device_from_host(host) if device: @@ -125,9 +127,9 @@ def setup(hass, hosts=None): """ Update states of all WeMo devices. """ # First time this method gets called, force_reload should be True - if (force_reload or - datetime.now() - update_wemos_state.last_updated > - MIN_TIME_BETWEEN_SCANS): + if force_reload or \ + datetime.now() - update_wemos_state.last_updated > \ + MIN_TIME_BETWEEN_SCANS: logger.info("Updating WeMo status") update_wemos_state.last_updated = datetime.now() @@ -138,7 +140,7 @@ def setup(hass, hosts=None): update_wemos_state(None, True) # Track all lights in a group - group.setup(hass, GROUP_NAME_ALL_WEMOS, sno_to_ent.values()) + group.setup_group(hass, GROUP_NAME_ALL_WEMOS, sno_to_ent.values()) def handle_wemo_service(service): """ Handles calls to the WeMo service. """ diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 7bf53e37281..39ce4a26203 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -95,7 +95,8 @@ class HomeAssistant(ha.HomeAssistant): def __init__(self, remote_api, local_api=None): if not remote_api.validate_api(): raise ha.HomeAssistantError( - "Remote API not valid: {}".format(remote_api.status)) + "Remote API at {}:{} not valid: {}".format( + remote_api.host, remote_api.port, remote_api.status)) self.remote_api = remote_api self.local_api = local_api @@ -113,7 +114,10 @@ class HomeAssistant(ha.HomeAssistant): import homeassistant.components.http as http import random - http.setup(self, '%030x'.format(random.randrange(16**30))) + # pylint: disable=too-many-format-args + random_password = '%030x'.format(random.randrange(16**30)) + + http.setup(self, random_password) ha.Timer(self) diff --git a/homeassistant/test.py b/homeassistant/test.py index b61b17f32d2..e42fdac6759 100644 --- a/homeassistant/test.py +++ b/homeassistant/test.py @@ -40,7 +40,8 @@ def ensure_homeassistant_started(): hass.bus.listen('test_event', len) hass.states.set('test', 'a_state') - http.setup(hass, API_PASSWORD) + http.setup(hass, + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD}}) hass.start() @@ -55,12 +56,16 @@ def ensure_homeassistant_started(): def ensure_slave_started(): """ Ensure a home assistant slave is started. """ + ensure_homeassistant_started() + if not HAHelper.slave: local_api = remote.API("127.0.0.1", API_PASSWORD, 8124) remote_api = remote.API("127.0.0.1", API_PASSWORD) - slave = remote.HomeAssistant(local_api, remote_api) + slave = remote.HomeAssistant(remote_api, local_api) - http.setup(slave, API_PASSWORD, 8124) + http.setup(slave, + {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, + http.CONF_SERVER_PORT: 8124}}) slave.start() @@ -73,7 +78,7 @@ def ensure_slave_started(): # pylint: disable=too-many-public-methods -class TestHTTPInterface(unittest.TestCase): +class TestHTTP(unittest.TestCase): """ Test the HTTP debug interface and API. """ @classmethod diff --git a/homeassistant/util.py b/homeassistant/util.py index b28c20ebbb9..2de31ce12dc 100644 --- a/homeassistant/util.py +++ b/homeassistant/util.py @@ -167,6 +167,30 @@ class OrderedEnum(enum.Enum): return NotImplemented +def validate_config(config, items, logger): + """ + Validates if all items are available in the configuration. + + config is the general dictionary with all the configurations. + items is a dict with per domain which attributes we require. + logger is the logger from the caller to log the errors to. + + Returns True if all required items were found. + """ + errors_found = False + for domain in items.keys(): + errors = [item for item in items[domain] if item not in config[domain]] + + if errors: + logger.error( + "Missing required configuration items in {}: {}".format( + domain, ", ".join(errors))) + + errors_found = True + + return not errors_found + + # Reason why I decided to roll my own ThreadPool instead of using # multiprocessing.dummy.pool or even better, use multiprocessing.pool and # not be hurt by the GIL in the cpython interpreter: