Components+configuration now loaded dynamically

A major change to the bootstrapping of Home Assistant decoupling the
knowledge in bootstrap for a more dynamic approach. This refactoring
also prepares the code for different configuration backends and the
loading components from different places.
This commit is contained in:
Paulus Schoutsen 2014-08-13 14:28:45 +02:00
parent cb33b3bf24
commit 997c2e8ef6
19 changed files with 473 additions and 309 deletions

View file

@ -1,20 +1,15 @@
[common] [homeassistant]
latitude=32.87336 latitude=32.87336
longitude=-117.22743 longitude=-117.22743
[http] [http]
api_password=mypass api_password=mypass
[light.hue] [light]
host=192.168.1.2 type=hue
[device_tracker.tomato] [device_tracker]
host=192.168.1.1 type=netgear
username=admin
password=PASSWORD
http_id=aaaaaaaaaaaaaaa
[device_tracker.netgear]
host=192.168.1.1 host=192.168.1.1
username=admin username=admin
password=PASSWORD password=PASSWORD
@ -38,9 +33,9 @@ download_dir=downloads
# A comma seperated list of states that have to be tracked as a single group # 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) # Grouped states should share the same states (ON/OFF or HOME/NOT_HOME)
[group] # [group]
living_room=light.Bowl,light.Ceiling,light.TV_back_light # living_room=light.Bowl,light.Ceiling,light.TV_back_light
bedroom=light.Bed_light # bedroom=light.Bed_light
[process] [process]
# items are which processes to look for: <entity_id>=<search string within ps> # items are which processes to look for: <entity_id>=<search string within ps>

View file

@ -30,6 +30,14 @@ ATTR_NOW = "now"
ATTR_DOMAIN = "domain" ATTR_DOMAIN = "domain"
ATTR_SERVICE = "service" 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 # How often time_changed event should fire
TIMER_INTERVAL = 10 # seconds TIMER_INTERVAL = 10 # seconds
@ -334,7 +342,7 @@ class EventBus(object):
for func in listeners: for func in listeners:
self._pool.add_job(JobPriority.from_event_type(event_type), self._pool.add_job(JobPriority.from_event_type(event_type),
(func, event)) (func, event))
def listen(self, event_type, listener): def listen(self, event_type, listener):
""" Listen for all events or events of a specific type. """ Listen for all events or events of a specific type.
@ -553,8 +561,8 @@ class ServiceRegistry(object):
service_call = ServiceCall(domain, service, service_data) service_call = ServiceCall(domain, service, service_data)
self._pool.add_job(JobPriority.EVENT_SERVICE, self._pool.add_job(JobPriority.EVENT_SERVICE,
(self._services[domain][service], (self._services[domain][service],
service_call)) service_call))
class Timer(threading.Thread): class Timer(threading.Thread):

View file

@ -7,20 +7,146 @@ After bootstrapping you can add your own components or
start by calling homeassistant.start_home_assistant(bus) start by calling homeassistant.start_home_assistant(bus)
""" """
import importlib
import configparser import configparser
import logging import logging
from collections import defaultdict
from itertools import chain
import homeassistant 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 # pylint: disable=too-many-branches
def from_config_file(config_path, enable_logging=True): def from_config_dict(config, hass=None):
""" Starts home assistant with all possible functionality """
based on a config file. Tries to configure Home Assistant from a config dict.
Will return a tuple (bus, statemachine). """
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: if enable_logging:
# Setup the logging for home assistant. # Setup the logging for home assistant.
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -34,196 +160,16 @@ def from_config_file(config_path, enable_logging=True):
datefmt='%H:%M %d-%m-%y')) datefmt='%H:%M %d-%m-%y'))
logging.getLogger('').addHandler(err_handler) logging.getLogger('').addHandler(err_handler)
# Start the actual bootstrapping
logger = logging.getLogger(__name__)
statusses = []
# Read config # Read config
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(config_path) config.read(config_path)
# Init core config_dict = {}
hass = homeassistant.HomeAssistant()
has_opt = config.has_option for section in config.sections():
get_opt = config.get config_dict[section] = {}
has_section = config.has_section
add_status = lambda name, result: statusses.append((name, result))
load_module = lambda module: importlib.import_module(
'homeassistant.components.'+module)
def get_opt_safe(section, option, default=None): for key, val in config.items(section):
""" Failure proof option retriever. """ config_dict[section][key] = val
try:
return config.get(section, option)
except (configparser.NoSectionError, configparser.NoOptionError):
return default
def get_hosts(section): return from_config_dict(config_dict, hass)
""" 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

View file

@ -15,6 +15,7 @@ Each component should publish services only under its own domain.
""" """
import itertools as it import itertools as it
import logging
import importlib import importlib
import homeassistant as ha import homeassistant as ha
@ -44,23 +45,51 @@ SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track" SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
def _get_component(component): def get_component(component, logger=None):
""" Returns requested component. """ """ Tries to load specified component.
Only returns it if also found to be valid."""
try: try:
return importlib.import_module( comp = importlib.import_module(
'homeassistant.components.{}'.format(component)) 'homeassistant.components.{}'.format(component))
except ImportError: except ImportError:
# If we got a bogus component the input will fail if logger:
logger.error(
"Failed to find component {}".format(component))
return None 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): def is_on(hass, entity_id=None):
""" Loads up the module to call the is_on method. """ Loads up the module to call the is_on method.
If there is no entity id given we will check all. """ If there is no entity id given we will check all. """
logger = logging.getLogger(__name__)
if entity_id: if entity_id:
group = _get_component('group') group = get_component('group', logger)
entity_ids = group.expand_entity_ids([entity_id]) entity_ids = group.expand_entity_ids([entity_id])
else: else:
@ -69,7 +98,7 @@ def is_on(hass, entity_id=None):
for entity_id in entity_ids: for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0] domain = util.split_entity_id(entity_id)[0]
module = _get_component(domain) module = get_component(domain, logger)
try: try:
if module.is_on(hass, entity_id): if module.is_on(hass, entity_id):
@ -100,7 +129,7 @@ def extract_entity_ids(hass, service):
entity_ids = [] entity_ids = []
if service.data and ATTR_ENTITY_ID in service.data: 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 # Entity ID attr can be a list or a string
service_ent_id = service.data[ATTR_ENTITY_ID] service_ent_id = service.data[ATTR_ENTITY_ID]
@ -117,7 +146,8 @@ def extract_entity_ids(hass, service):
return entity_ids return entity_ids
def setup(hass): # pylint: disable=unused-argument
def setup(hass, config):
""" Setup general services related to homeassistant. """ """ Setup general services related to homeassistant. """
def handle_turn_service(service): def handle_turn_service(service):

View file

@ -6,11 +6,13 @@ Provides functionality to launch a webbrowser on the host machine.
""" """
DOMAIN = "browser" DOMAIN = "browser"
DEPENDENCIES = []
SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL = "browse_url"
def setup(hass): # pylint: disable=unused-argument
def setup(hass, config):
""" Listen for browse_url events and open """ Listen for browse_url events and open
the url in the default webbrowser. """ the url in the default webbrowser. """

View file

@ -10,6 +10,7 @@ import homeassistant.util as util
import homeassistant.components as components import homeassistant.components as components
DOMAIN = 'chromecast' DOMAIN = 'chromecast'
DEPENDENCIES = []
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' 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 # pylint: disable=too-many-locals, too-many-branches
def setup(hass, hosts=None): def setup(hass, config):
""" Listen for chromecast events. """ """ Listen for chromecast events. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -113,8 +114,11 @@ def setup(hass, hosts=None):
return False return False
if 'hosts' in config[DOMAIN]:
hosts = config[DOMAIN]['hosts'].split(",")
# If no hosts given, scan for chromecasts # If no hosts given, scan for chromecasts
if not hosts: else:
logger.info("Scanning for Chromecasts") logger.info("Scanning for Chromecasts")
hosts = pychromecast.discover_chromecasts() hosts = pychromecast.discover_chromecasts()
@ -131,7 +135,7 @@ def setup(hass, hosts=None):
casts[entity_id] = cast casts[entity_id] = cast
except pychromecast.ConnectionError: except pychromecast.ChromecastConnectionError:
pass pass
if not casts: if not casts:

View file

@ -11,19 +11,26 @@ from datetime import datetime, timedelta
import homeassistant.components as components import homeassistant.components as components
from . import light, sun, device_tracker, group 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_TRANSITION_TIME = timedelta(minutes=15)
# Light profile to be used if none given # Light profile to be used if none given
LIGHT_PROFILE = 'relax' LIGHT_PROFILE = 'relax'
CONF_LIGHT_PROFILE = 'light_profile'
CONF_LIGHT_GROUP = 'light_group'
# pylint: disable=too-many-branches # 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. """ """ Triggers to turn lights on or off based on device precense. """
light_group = light_group or light.GROUP_NAME_ALL_LIGHTS light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
light_profile = light_profile or LIGHT_PROFILE light.GROUP_NAME_ALL_LIGHTS)
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
logger = logging.getLogger(__name__) 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): def turn_light_on_before_sunset(light_id):
""" Helper function to turn on lights slowly if there """ Helper function to turn on lights slowly if there
are devices home and the light is not on yet. """ are devices home and the light is not on yet. """
if (device_tracker.is_on(hass) and if device_tracker.is_on(hass) and not light.is_on(hass, light_id):
not light.is_on(hass, light_id)):
light.turn_on(hass, light_id, light.turn_on(hass, light_id,
transition=LIGHT_TRANSITION_TIME.seconds, 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)) light_needed = not (lights_are_on or sun.is_on(hass))
# Specific device came home ? # Specific device came home ?
if (entity != device_tracker.ENTITY_ID_ALL_DEVICES and if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
new_state.state == components.STATE_HOME): new_state.state == components.STATE_HOME:
# These variables are needed for the elif check # These variables are needed for the elif check
now = datetime.now() now = datetime.now()

View file

@ -14,12 +14,14 @@ from datetime import datetime, timedelta
import requests import requests
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
import homeassistant.components as components import homeassistant.components as components
from homeassistant.components import group from homeassistant.components import group
DOMAIN = "device_tracker" DOMAIN = "device_tracker"
DEPENDENCIES = []
SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv"
@ -39,6 +41,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
# Filename to save known devices to # Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv" KNOWN_DEVICES_FILE = "known_devices.csv"
CONF_HTTP_ID = "http_id"
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """ """ 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) 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 # pylint: disable=too-many-instance-attributes
class DeviceTracker(object): class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """ """ 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.states = hass.states
self.device_scanner = device_scanner 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__) self.logger = logging.getLogger(__name__)
@ -84,7 +141,7 @@ class DeviceTracker(object):
self.update_devices() 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 @property
def device_entity_ids(self): def device_entity_ids(self):
@ -164,8 +221,8 @@ class DeviceTracker(object):
except IOError: except IOError:
self.logger.exception(( self.logger.exception((
"DeviceTracker:Error updating {}" "DeviceTracker:Error updating {}"
"with {} new devices").format( "with {} new devices").format(KNOWN_DEVICES_FILE,
KNOWN_DEVICES_FILE, len(unknown_devices))) len(unknown_devices)))
self.lock.release() self.lock.release()
@ -223,8 +280,8 @@ class DeviceTracker(object):
# Remove entities that are no longer maintained # Remove entities that are no longer maintained
new_entity_ids = set([known_devices[device]['entity_id'] new_entity_ids = set([known_devices[device]['entity_id']
for device in known_devices for device in known_devices
if known_devices[device]['track']]) if known_devices[device]['track']])
for entity_id in \ for entity_id in \
self.device_entity_ids - new_entity_ids: self.device_entity_ids - new_entity_ids:
@ -246,8 +303,8 @@ class DeviceTracker(object):
self.invalid_known_devices_file = True self.invalid_known_devices_file = True
self.logger.warning(( self.logger.warning((
"Invalid {} found. " "Invalid {} found. "
"We won't update it with new found devices."). "We won't update it with new found devices."
format(KNOWN_DEVICES_FILE)) ).format(KNOWN_DEVICES_FILE))
finally: finally:
self.lock.release() self.lock.release()
@ -261,7 +318,10 @@ class TomatoDeviceScanner(object):
http://paulusschoutsen.nl/blog/2013/10/tomato-api-documentation/ 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', self.req = requests.Request('POST',
'http://{}/update.cgi'.format(host), 'http://{}/update.cgi'.format(host),
data={'_http_id': http_id, data={'_http_id': http_id,
@ -309,8 +369,8 @@ class TomatoDeviceScanner(object):
self.lock.acquire() self.lock.acquire()
# if date_updated is None or the date is too old we scan for new data # 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 > if not self.date_updated or \
MIN_TIME_BETWEEN_SCANS): datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
self.logger.info("Tomato:Scanning") self.logger.info("Tomato:Scanning")
@ -380,7 +440,10 @@ class TomatoDeviceScanner(object):
class NetgearDeviceScanner(object): class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """ """ 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.logger = logging.getLogger(__name__)
self.date_updated = None self.date_updated = None
self.last_results = [] self.last_results = []
@ -442,8 +505,8 @@ class NetgearDeviceScanner(object):
with self.lock: with self.lock:
# if date_updated is None or the date is too old we scan for # if date_updated is None or the date is too old we scan for
# new data # new data
if (not self.date_updated or datetime.now() - self.date_updated > if not self.date_updated or \
MIN_TIME_BETWEEN_SCANS): datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
self.logger.info("Netgear:Scanning") self.logger.info("Netgear:Scanning")
@ -470,7 +533,10 @@ class LuciDeviceScanner(object):
(Currently, we do only wifi iwscan, and no DHCP lease access.) (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<param>\w*) = (?P<value>.*);") self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@ -554,8 +620,8 @@ class LuciDeviceScanner(object):
with self.lock: with self.lock:
# if date_updated is None or the date is too old we scan # if date_updated is None or the date is too old we scan
# for new data # for new data
if (not self.date_updated or datetime.now() - self.date_updated > if not self.date_updated or \
MIN_TIME_BETWEEN_SCANS): datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
self.logger.info("Checking ARP") self.logger.info("Checking ARP")

View file

@ -12,15 +12,18 @@ import threading
import homeassistant.util as util import homeassistant.util as util
DOMAIN = "downloader" DOMAIN = "downloader"
DEPENDENCIES = []
SERVICE_DOWNLOAD_FILE = "download_file" SERVICE_DOWNLOAD_FILE = "download_file"
ATTR_URL = "url" ATTR_URL = "url"
ATTR_SUBDIR = "subdir" ATTR_SUBDIR = "subdir"
CONF_DOWNLOAD_DIR = 'download_dir'
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(hass, download_path): def setup(hass, config):
""" Listens for download events to download files. """ """ Listens for download events to download files. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,6 +36,11 @@ def setup(hass, download_path):
return False 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): if not os.path.isdir(download_path):
logger.error( logger.error(
@ -106,8 +114,7 @@ def setup(hass, download_path):
final_path = "{}_{}.{}".format(path, tries, ext) final_path = "{}_{}.{}".format(path, tries, ext)
logger.info("{} -> {}".format( logger.info("{} -> {}".format(url, final_path))
url, final_path))
with open(final_path, 'wb') as fil: with open(final_path, 'wb') as fil:
for chunk in req.iter_content(1024): for chunk in req.iter_content(1024):

View file

@ -13,6 +13,7 @@ from homeassistant.components import (STATE_ON, STATE_OFF,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
DOMAIN = "group" DOMAIN = "group"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}" 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 # 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 """ Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ 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__) logger = logging.getLogger(__name__)
# Loop over the given entities to: # Loop over the given entities to:
@ -118,9 +127,11 @@ def setup(hass, name, entity_ids):
else: else:
# We did not find a matching group_type # We did not find a matching group_type
errors.append("Found unexpected state '{}'".format( errors.append(
name, state.state)) "Entity {} has ungroupable state '{}'".format(
name, state.state))
# Stop check all other entity IDs and report as error
break break
# Check if entity exists # Check if entity exists
@ -134,43 +145,48 @@ def setup(hass, name, entity_ids):
entity_id, state.state, group_off, group_on)) entity_id, state.state, group_off, group_on))
# Keep track of the group state to init later 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 group_state = group_on
if group_type is None and not errors:
errors.append('Unable to determine group type for {}'.format(name))
if errors: if errors:
logger.error("Error setting up state group {}: {}".format( logger.error("Error setting up group {}: {}".format(
name, ", ".join(errors))) name, ", ".join(errors)))
return False return False
group_entity_id = ENTITY_ID_FORMAT.format(name) else:
state_attr = {ATTR_ENTITY_ID: entity_ids} group_entity_id = ENTITY_ID_FORMAT.format(name)
state_attr = {ATTR_ENTITY_ID: entity_ids}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state): def update_group_state(entity_id, old_state, new_state):
""" Updates the group state based on a state change by a tracked """ Updates the group state based on a state change by
entity. """ 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_gr_state = OFF and new_state = ON: set ON
# if cur_group_state = ON and new_state = OFF: research # if cur_gr_state = ON and new_state = OFF: research
# else: ignore # 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 # Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on) if not any([hass.states.is_state(ent_id, group_on)
for ent_id in entity_ids if entity_id != ent_id]): for ent_id in entity_ids
hass.states.set(group_entity_id, group_off, state_attr) if entity_id != ent_id]):
hass.states.set(group_entity_id, group_off, state_attr)
for entity_id in entity_ids: for entity_id in entity_ids:
hass.track_state_change(entity_id, update_group_state) 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

View file

@ -89,6 +89,8 @@ import homeassistant.remote as rem
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (STATE_ON, STATE_OFF, from homeassistant.components import (STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF) SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = "http"
DEPENDENCIES = []
HTTP_OK = 200 HTTP_OK = 200
HTTP_CREATED = 201 HTTP_CREATED = 201
@ -120,6 +122,10 @@ DOMAIN_ICONS = {
"downloader": "glyphicon-download-alt" "downloader": "glyphicon-download-alt"
} }
CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
def _get_domain_icon(domain): def _get_domain_icon(domain):
""" Returns HTML that shows domain icon. """ """ Returns HTML that shows domain icon. """
@ -127,12 +133,19 @@ def _get_domain_icon(domain):
DOMAIN_ICONS.get(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. """ """ 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 # 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), server = HomeAssistantHTTPServer((server_host, server_port),
RequestHandler, hass, api_password) RequestHandler, hass, api_password)
@ -147,6 +160,8 @@ def setup(hass, api_password, server_port=None, server_host=None):
hass.local_api = \ hass.local_api = \
rem.API(util.get_local_ip(), api_password, server_port) rem.API(util.get_local_ip(), api_password, server_port)
return True
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
""" Handle HTTP requests in a threaded fashion. """ """ Handle HTTP requests in a threaded fashion. """
@ -213,7 +228,7 @@ class RequestHandler(BaseHTTPRequestHandler):
# /event_forwarding # /event_forwarding
('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'), ('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
('DELETE', rem.URL_API_EVENT_FORWARD, ('DELETE', rem.URL_API_EVENT_FORWARD,
'_handle_delete_api_event_forward'), '_handle_delete_api_event_forward'),
# Statis files # Statis files
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'), ('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
@ -407,7 +422,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"href='#'></a>").format(action, state.entity_id)) "href='#'></a>").format(action, state.entity_id))
write("</td><td>{}</td><td>{}</td></tr>".format( write("</td><td>{}</td><td>{}</td></tr>".format(
attributes, util.datetime_to_str(state.last_changed))) attributes, util.datetime_to_str(state.last_changed)))
# Change state form # Change state form
write(("<tr><td></td><td><input name='entity_id' class='form-control' " write(("<tr><td></td><td><input name='entity_id' class='form-control' "

View file

@ -9,6 +9,7 @@ import logging
import homeassistant.components as components import homeassistant.components as components
DOMAIN = "keyboard" DOMAIN = "keyboard"
DEPENDENCIES = []
def volume_up(hass): def volume_up(hass):
@ -41,13 +42,14 @@ def media_prev_track(hass):
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK)
def setup(hass): # pylint: disable=unused-argument
def setup(hass, config):
""" Listen for keyboard events. """ """ Listen for keyboard events. """
try: try:
import pykeyboard import pykeyboard
except ImportError: except ImportError:
logging.getLogger(__name__).exception( logging.getLogger(__name__).exception(
"MediaButtons: Error while importing dependency PyUserInput.") "Error while importing dependency PyUserInput.")
return False return False

View file

@ -55,6 +55,7 @@ from collections import namedtuple
import os import os
import csv import csv
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (group, extract_entity_ids, from homeassistant.components import (group, extract_entity_ids,
STATE_ON, STATE_OFF, STATE_ON, STATE_OFF,
@ -63,6 +64,7 @@ from homeassistant.components import (group, extract_entity_ids,
DOMAIN = "light" DOMAIN = "light"
DEPENDENCIES = []
GROUP_NAME_ALL_LIGHTS = 'all_lights' GROUP_NAME_ALL_LIGHTS = 'all_lights'
ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format( ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format(
@ -136,11 +138,26 @@ def turn_off(hass, entity_id=None, transition=None):
# pylint: disable=too-many-branches, too-many-locals # pylint: disable=too-many-branches, too-many-locals
def setup(hass, light_control): def setup(hass, config):
""" Exposes light control via statemachine and services. """ """ Exposes light control via statemachine and services. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger):
return False
light_type = config[DOMAIN][ha.CONF_TYPE]
if light_type == 'hue':
light_init = HueLightControl
else:
logger.error("Found unknown light type: {}".format(light_type))
return False
light_control = light_init(config[DOMAIN])
ent_to_light = {} ent_to_light = {}
light_to_ent = {} light_to_ent = {}
@ -188,9 +205,9 @@ def setup(hass, light_control):
""" Update the state of all the lights. """ """ Update the state of all the lights. """
# First time this method gets called, force_reload should be True # First time this method gets called, force_reload should be True
if (force_reload or if force_reload or \
datetime.now() - update_lights_state.last_updated > datetime.now() - update_lights_state.last_updated > \
MIN_TIME_BETWEEN_SCANS): MIN_TIME_BETWEEN_SCANS:
logger.info("Updating light status") logger.info("Updating light status")
update_lights_state.last_updated = datetime.now() update_lights_state.last_updated = datetime.now()
@ -206,7 +223,7 @@ def setup(hass, light_control):
return False return False
# Track all lights in a group # 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 # Load built-in profiles and custom profiles
profile_paths = [os.path.dirname(__file__), os.getcwd()] profile_paths = [os.path.dirname(__file__), os.getcwd()]
@ -336,9 +353,11 @@ def _hue_to_light_state(info):
class HueLightControl(object): class HueLightControl(object):
""" Class to interface with the Hue light system. """ """ Class to interface with the Hue light system. """
def __init__(self, host=None): def __init__(self, config):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
host = config.get(ha.CONF_HOST, None)
try: try:
import phue import phue
except ImportError: except ImportError:

View file

@ -26,12 +26,13 @@ from homeassistant.components import STATE_ON, STATE_OFF
import homeassistant.util as util import homeassistant.util as util
DOMAIN = 'process' DOMAIN = 'process'
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
PS_STRING = 'ps awx' PS_STRING = 'ps awx'
def setup(hass, processes): def setup(hass, config):
""" Sets up a check if specified processes are running. """ Sets up a check if specified processes are running.
processes: dict mapping entity id to substring to search for 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 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 # pylint: disable=unused-argument
def update_process_states(time): def update_process_states(time):

View file

@ -7,8 +7,11 @@ Provides functionality to keep track of the sun.
import logging import logging
from datetime import timedelta from datetime import timedelta
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
DEPENDENCIES = []
DOMAIN = "sun"
ENTITY_ID = "sun.sun" ENTITY_ID = "sun.sun"
STATE_ABOVE_HORIZON = "above_horizon" STATE_ABOVE_HORIZON = "above_horizon"
@ -49,10 +52,16 @@ def next_rising(hass):
return None return None
def setup(hass, latitude, longitude): def setup(hass, config):
""" Tracks the state of the sun. """ """ Tracks the state of the sun. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not util.validate_config(config,
{ha.DOMAIN: [ha.CONF_LATITUDE,
ha.CONF_LONGITUDE]},
logger):
return False
try: try:
import ephem import ephem
except ImportError: except ImportError:
@ -61,12 +70,15 @@ def setup(hass, latitude, longitude):
sun = ephem.Sun() # pylint: disable=no-member 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 def update_sun_state(now): # pylint: disable=unused-argument
""" Method to update the current state of the sun and """ Method to update the current state of the sun and
set time of next setting and rising. """ set time of next setting and rising. """
observer = ephem.Observer() observer = ephem.Observer()
observer.lat = latitude observer.lat = latitude # pylint: disable=assigning-non-slot
observer.long = longitude observer.long = longitude # pylint: disable=assigning-non-slot
next_rising_dt = ephem.localtime(observer.next_rising(sun)) next_rising_dt = ephem.localtime(observer.next_rising(sun))
next_setting_dt = ephem.localtime(observer.next_setting(sun)) next_setting_dt = ephem.localtime(observer.next_setting(sun))

View file

@ -4,12 +4,14 @@ Component to interface with WeMo devices on the network.
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (group, extract_entity_ids, from homeassistant.components import (group, extract_entity_ids,
STATE_ON, STATE_OFF, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF,
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
DOMAIN = 'wemo' DOMAIN = 'wemo'
DEPENDENCIES = []
GROUP_NAME_ALL_WEMOS = 'all_wemos' GROUP_NAME_ALL_WEMOS = 'all_wemos'
ENTITY_ID_ALL_WEMOS = group.ENTITY_ID_FORMAT.format( 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 # pylint: disable=too-many-branches
def setup(hass, hosts=None): def setup(hass, config):
""" Track states and offer events for WeMo switches. """ """ Track states and offer events for WeMo switches. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,10 +63,10 @@ def setup(hass, hosts=None):
return False return False
if hosts: if ha.CONF_HOSTS in config[DOMAIN]:
devices = [] devices = []
for host in hosts: for host in config[DOMAIN][ha.CONF_HOSTS].split(","):
device = pywemo.device_from_host(host) device = pywemo.device_from_host(host)
if device: if device:
@ -125,9 +127,9 @@ def setup(hass, hosts=None):
""" Update states of all WeMo devices. """ """ Update states of all WeMo devices. """
# First time this method gets called, force_reload should be True # First time this method gets called, force_reload should be True
if (force_reload or if force_reload or \
datetime.now() - update_wemos_state.last_updated > datetime.now() - update_wemos_state.last_updated > \
MIN_TIME_BETWEEN_SCANS): MIN_TIME_BETWEEN_SCANS:
logger.info("Updating WeMo status") logger.info("Updating WeMo status")
update_wemos_state.last_updated = datetime.now() update_wemos_state.last_updated = datetime.now()
@ -138,7 +140,7 @@ def setup(hass, hosts=None):
update_wemos_state(None, True) update_wemos_state(None, True)
# Track all lights in a group # 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): def handle_wemo_service(service):
""" Handles calls to the WeMo service. """ """ Handles calls to the WeMo service. """

View file

@ -95,7 +95,8 @@ class HomeAssistant(ha.HomeAssistant):
def __init__(self, remote_api, local_api=None): def __init__(self, remote_api, local_api=None):
if not remote_api.validate_api(): if not remote_api.validate_api():
raise ha.HomeAssistantError( 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.remote_api = remote_api
self.local_api = local_api self.local_api = local_api
@ -113,7 +114,10 @@ class HomeAssistant(ha.HomeAssistant):
import homeassistant.components.http as http import homeassistant.components.http as http
import random 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) ha.Timer(self)

View file

@ -40,7 +40,8 @@ def ensure_homeassistant_started():
hass.bus.listen('test_event', len) hass.bus.listen('test_event', len)
hass.states.set('test', 'a_state') hass.states.set('test', 'a_state')
http.setup(hass, API_PASSWORD) http.setup(hass,
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD}})
hass.start() hass.start()
@ -55,12 +56,16 @@ def ensure_homeassistant_started():
def ensure_slave_started(): def ensure_slave_started():
""" Ensure a home assistant slave is started. """ """ Ensure a home assistant slave is started. """
ensure_homeassistant_started()
if not HAHelper.slave: if not HAHelper.slave:
local_api = remote.API("127.0.0.1", API_PASSWORD, 8124) local_api = remote.API("127.0.0.1", API_PASSWORD, 8124)
remote_api = remote.API("127.0.0.1", API_PASSWORD) 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() slave.start()
@ -73,7 +78,7 @@ def ensure_slave_started():
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
class TestHTTPInterface(unittest.TestCase): class TestHTTP(unittest.TestCase):
""" Test the HTTP debug interface and API. """ """ Test the HTTP debug interface and API. """
@classmethod @classmethod

View file

@ -167,6 +167,30 @@ class OrderedEnum(enum.Enum):
return NotImplemented 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 # Reason why I decided to roll my own ThreadPool instead of using
# multiprocessing.dummy.pool or even better, use multiprocessing.pool and # multiprocessing.dummy.pool or even better, use multiprocessing.pool and
# not be hurt by the GIL in the cpython interpreter: # not be hurt by the GIL in the cpython interpreter: