Skip to content

Commit

Permalink
Allow reloading automation without restarting HA (#3002)
Browse files Browse the repository at this point in the history
  • Loading branch information
balloob authored and kellerza committed Sep 4, 2016
1 parent 641d531 commit e9813b2
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 105 deletions.
129 changes: 71 additions & 58 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,67 +90,12 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
domain, domain)
return False

component = loader.get_component(domain)
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
if dep not in hass.config.components]
config = prepare_setup_component(hass, config, domain)

if missing_deps:
_LOGGER.error(
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return False

if hasattr(component, 'CONFIG_SCHEMA'):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
log_exception(ex, domain, config)
return False

elif hasattr(component, 'PLATFORM_SCHEMA'):
platforms = []
for p_name, p_config in config_per_platform(config, domain):
# Validate component specific platform schema
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
log_exception(ex, domain, p_config)
return False

# Not all platform components follow same pattern for platforms
# So if p_name is None we are not going to validate platform
# (the automation component is one of them)
if p_name is None:
platforms.append(p_validated)
continue

platform = prepare_setup_platform(hass, config, domain,
p_name)

if platform is None:
return False

# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return False

platforms.append(p_validated)

# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
filter_keys = extract_domain_configs(config, domain)
config = {key: value for key, value in config.items()
if key not in filter_keys}
config[domain] = platforms

if not _handle_requirements(hass, component, domain):
if config is None:
return False

component = loader.get_component(domain)
_CURRENT_SETUP.append(domain)

try:
Expand Down Expand Up @@ -182,6 +127,74 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
return True


def prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str):
"""Prepare setup of a component and return processed config."""
# pylint: disable=too-many-return-statements
component = loader.get_component(domain)
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
if dep not in hass.config.components]

if missing_deps:
_LOGGER.error(
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return None

if hasattr(component, 'CONFIG_SCHEMA'):
try:
config = component.CONFIG_SCHEMA(config)
except vol.MultipleInvalid as ex:
log_exception(ex, domain, config)
return None

elif hasattr(component, 'PLATFORM_SCHEMA'):
platforms = []
for p_name, p_config in config_per_platform(config, domain):
# Validate component specific platform schema
try:
p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.MultipleInvalid as ex:
log_exception(ex, domain, p_config)
return None

# Not all platform components follow same pattern for platforms
# So if p_name is None we are not going to validate platform
# (the automation component is one of them)
if p_name is None:
platforms.append(p_validated)
continue

platform = prepare_setup_platform(hass, config, domain,
p_name)

if platform is None:
return None

# Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'):
try:
p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.MultipleInvalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated)
return None

platforms.append(p_validated)

# Create a copy of the configuration with all config for current
# component removed and add validated config back in.
filter_keys = extract_domain_configs(config, domain)
config = {key: value for key, value in config.items()
if key not in filter_keys}
config[domain] = platforms

if not _handle_requirements(hass, component, domain):
return None

return config


def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup."""
Expand Down
103 changes: 76 additions & 27 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
"""
from functools import partial
import logging
import os

import voluptuous as vol

from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.bootstrap import (
prepare_setup_platform, prepare_setup_component)
from homeassistant import config as conf_util
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
SERVICE_TOGGLE)
Expand Down Expand Up @@ -46,6 +49,7 @@
ATTR_LAST_TRIGGERED = 'last_triggered'
ATTR_VARIABLES = 'variables'
SERVICE_TRIGGER = 'trigger'
SERVICE_RELOAD = 'reload'

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -112,6 +116,8 @@ def validator(config):
vol.Optional(ATTR_VARIABLES, default={}): dict,
})

RELOAD_SERVICE_SCHEMA = vol.Schema({})


def is_on(hass, entity_id=None):
"""
Expand Down Expand Up @@ -148,40 +154,23 @@ def trigger(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_TRIGGER, data)


def reload(hass):
"""Reload the automation from config."""
hass.services.call(DOMAIN, SERVICE_RELOAD)


def setup(hass, config):
"""Setup the automation."""
# pylint: disable=too-many-locals
component = EntityComponent(_LOGGER, DOMAIN, hass)

success = False
for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key]

for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)

action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)

if CONF_CONDITION in config_block:
cond_func = _process_if(hass, config, config_block)

if cond_func is None:
continue
else:
def cond_func(variables):
"""Condition will always pass."""
return True

attach_triggers = partial(_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name)
entity = AutomationEntity(name, attach_triggers, cond_func, action)
component.add_entities((entity,))
success = True
success = _process_config(hass, config, component)

if not success:
return False

descriptions = conf_util.load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))

def trigger_service_handler(service_call):
"""Handle automation triggers."""
for entity in component.extract_from_service(service_call):
Expand All @@ -192,11 +181,34 @@ def service_handler(service_call):
for entity in component.extract_from_service(service_call):
getattr(entity, service_call.service)()

def reload_service_handler(service_call):
"""Remove all automations and load new ones from config."""
try:
path = conf_util.find_config_file(hass.config.config_dir)
conf = conf_util.load_yaml_config_file(path)
except HomeAssistantError as err:
_LOGGER.error(err)
return

conf = prepare_setup_component(hass, conf, DOMAIN)

if conf is None:
return

component.reset()
_process_config(hass, conf, component)

hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
descriptions.get(SERVICE_TRIGGER),
schema=TRIGGER_SERVICE_SCHEMA)

hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
descriptions.get(SERVICE_RELOAD),
schema=RELOAD_SERVICE_SCHEMA)

for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE):
hass.services.register(DOMAIN, service, service_handler,
descriptions.get(service),
schema=SERVICE_SCHEMA)

return True
Expand Down Expand Up @@ -263,6 +275,43 @@ def trigger(self, variables):
self._last_triggered = utcnow()
self.update_ha_state()

def remove(self):
"""Remove automation from HASS."""
self.turn_off()
super().remove()


def _process_config(hass, config, component):
"""Process config and add automations."""
success = False

for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key]

for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
list_no)

action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)

if CONF_CONDITION in config_block:
cond_func = _process_if(hass, config, config_block)

if cond_func is None:
continue
else:
def cond_func(variables):
"""Condition will always pass."""
return True

attach_triggers = partial(_process_trigger, hass, config,
config_block.get(CONF_TRIGGER, []), name)
entity = AutomationEntity(name, attach_triggers, cond_func, action)
component.add_entities((entity,))
success = True

return success


def _get_action(hass, config, name):
"""Return an action based on a configuration."""
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/automation/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
turn_on:
description: Enable an automation.

fields:
entity_id:
description: Name of the automation to turn on.
example: 'automation.notify_home'

turn_off:
description: Disable an automation.

fields:
entity_id:
description: Name of the automation to turn off.
example: 'automation.notify_home'

toggle:
description: Toggle an automation.

fields:
entity_id:
description: Name of the automation to toggle on/off.
example: 'automation.notify_home'

trigger:
description: Trigger the action of an automation.

fields:
entity_id:
description: Name of the automation to trigger.
example: 'automation.notify_home'

reload:
description: Reload the automation configuration.
4 changes: 4 additions & 0 deletions homeassistant/helpers/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ def update_ha_state(self, force_refresh=False):
return self.hass.states.set(
self.entity_id, state, attr, self.force_update)

def remove(self) -> None:
"""Remove entitiy from HASS."""
self.hass.states.remove(self.entity_id)

def _attr_setter(self, name, typ, attr, attrs):
"""Helper method to populate attributes based on properties."""
if attr in attrs:
Expand Down
Loading

0 comments on commit e9813b2

Please sign in to comment.