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:
parent
cb33b3bf24
commit
997c2e8ef6
19 changed files with 473 additions and 309 deletions
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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. """
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' "
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue