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
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: <entity_id>=<search string within ps>

View file

@ -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):

View file

@ -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)

View file

@ -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):

View file

@ -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. """

View file

@ -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:

View file

@ -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()

View file

@ -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<param>\w*) = (?P<value>.*);")
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")

View file

@ -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):

View file

@ -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

View file

@ -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<file>[a-zA-Z\._\-0-9/]+)'),
@ -407,7 +422,7 @@ class RequestHandler(BaseHTTPRequestHandler):
"href='#'></a>").format(action, state.entity_id))
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
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
DOMAIN = "keyboard"
DEPENDENCIES = []
def volume_up(hass):
@ -41,13 +42,14 @@ def media_prev_track(hass):
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK)
def setup(hass):
# pylint: disable=unused-argument
def setup(hass, config):
""" Listen for keyboard events. """
try:
import pykeyboard
except ImportError:
logging.getLogger(__name__).exception(
"MediaButtons: Error while importing dependency PyUserInput.")
"Error while importing dependency PyUserInput.")
return False

View file

@ -55,6 +55,7 @@ from collections import namedtuple
import os
import csv
import homeassistant as ha
import homeassistant.util as util
from homeassistant.components import (group, extract_entity_ids,
STATE_ON, STATE_OFF,
@ -63,6 +64,7 @@ from homeassistant.components import (group, extract_entity_ids,
DOMAIN = "light"
DEPENDENCIES = []
GROUP_NAME_ALL_LIGHTS = 'all_lights'
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
def setup(hass, light_control):
def setup(hass, config):
""" Exposes light control via statemachine and services. """
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 = {}
light_to_ent = {}
@ -188,9 +205,9 @@ def setup(hass, light_control):
""" Update the state of all the lights. """
# First time this method gets called, force_reload should be True
if (force_reload or
datetime.now() - update_lights_state.last_updated >
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:

View file

@ -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):

View file

@ -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))

View file

@ -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. """

View file

@ -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)

View file

@ -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

View file

@ -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: