Initial script condition support (#1910)

This commit is contained in:
Paulus Schoutsen 2016-04-28 12:03:57 +02:00
parent 953223b81b
commit 6354399d55
19 changed files with 656 additions and 427 deletions

View File

@ -11,7 +11,8 @@ import voluptuous as vol
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.helpers import extract_domain_configs, script from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -73,10 +74,11 @@ _CONDITION_SCHEMA = vol.Any(
[ [
vol.All( vol.All(
vol.Schema({ vol.Schema({
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN), CONF_PLATFORM: str,
CONF_CONDITION: str,
}, extra=vol.ALLOW_EXTRA), }, extra=vol.ALLOW_EXTRA),
_platform_validator(METHOD_IF_ACTION, 'IF_ACTION_SCHEMA'), cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
) ),
] ]
) )
) )
@ -93,15 +95,17 @@ PLATFORM_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup the automation.""" """Setup the automation."""
success = False
for config_key in extract_domain_configs(config, DOMAIN): for config_key in extract_domain_configs(config, DOMAIN):
conf = config[config_key] conf = config[config_key]
for list_no, config_block in enumerate(conf): for list_no, config_block in enumerate(conf):
name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key, name = config_block.get(CONF_ALIAS, "{}, {}".format(config_key,
list_no)) list_no))
_setup_automation(hass, config_block, name, config) success = (_setup_automation(hass, config_block, name, config) or
success)
return True return success
def _setup_automation(hass, config_block, name, config): def _setup_automation(hass, config_block, name, config):
@ -136,7 +140,6 @@ def _process_if(hass, config, p_config, action):
"""Process if checks.""" """Process if checks."""
cond_type = p_config.get(CONF_CONDITION_TYPE, cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower() DEFAULT_CONDITION_TYPE).lower()
if_configs = p_config.get(CONF_CONDITION) if_configs = p_config.get(CONF_CONDITION)
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
@ -145,28 +148,32 @@ def _process_if(hass, config, p_config, action):
checks = [] checks = []
for if_config in if_configs: for if_config in if_configs:
platform = _resolve_platform(METHOD_IF_ACTION, hass, config, if CONF_PLATFORM in if_config:
if_config.get(CONF_PLATFORM)) if not use_trigger:
if platform is None: _LOGGER.warning("Please switch your condition configuration "
continue "to use 'condition' instead of 'platform'.")
if_config = dict(if_config)
check = platform.if_action(hass, if_config) if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
try:
checks.append(condition.from_config(if_config))
except HomeAssistantError as ex:
# Invalid conditions are allowed if we base it on trigger # Invalid conditions are allowed if we base it on trigger
if check is None and not use_trigger: if use_trigger:
_LOGGER.warning('Ignoring invalid condition: %s', ex)
else:
_LOGGER.warning('Invalid condition: %s', ex)
return None return None
checks.append(check)
if cond_type == CONDITION_TYPE_AND: if cond_type == CONDITION_TYPE_AND:
def if_action(variables=None): def if_action(variables=None):
"""AND all conditions.""" """AND all conditions."""
if all(check(variables) for check in checks): if all(check(hass, variables) for check in checks):
action(variables) action(variables)
else: else:
def if_action(variables=None): def if_action(variables=None):
"""OR all conditions.""" """OR all conditions."""
if any(check(variables) for check in checks): if any(check(hass, variables) for check in checks):
action(variables) action(variables)
return if_action return if_action

View File

@ -6,20 +6,26 @@ at https://home-assistant.io/components/automation/#event-trigger
""" """
import logging import logging
import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
CONF_EVENT_TYPE = "event_type" CONF_EVENT_TYPE = "event_type"
CONF_EVENT_DATA = "event_data" CONF_EVENT_DATA = "event_data"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'event',
vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict,
})
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE) event_type = config.get(CONF_EVENT_TYPE)
if event_type is None:
_LOGGER.error("Missing configuration key %s", CONF_EVENT_TYPE)
return False
event_data = config.get(CONF_EVENT_DATA) event_data = config.get(CONF_EVENT_DATA)
def handle_event(event): def handle_event(event):

View File

@ -5,54 +5,36 @@ For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger at https://home-assistant.io/components/automation/#numeric-state-trigger
""" """
import logging import logging
from functools import partial
from homeassistant.const import CONF_VALUE_TEMPLATE import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.helpers import template from homeassistant.helpers import condition, config_validation as cv
CONF_ENTITY_ID = "entity_id" TRIGGER_SCHEMA = vol.All(vol.Schema({
CONF_BELOW = "below" vol.Required(CONF_PLATFORM): 'numeric_state',
CONF_ABOVE = "above" vol.Required(CONF_ENTITY_ID): cv.entity_id,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _renderer(hass, value_template, state, variables=None):
"""Render the state value."""
if value_template is None:
return state.state
variables = dict(variables or {})
variables['state'] = state
return template.render(hass, value_template, variables)
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return False
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return False
renderer = partial(_renderer, hass, value_template)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
# Fire action if we go from outside range into range
if to_s is None: if to_s is None:
return return
@ -64,14 +46,20 @@ def trigger(hass, config, action):
'above': above, 'above': above,
} }
} }
to_s_value = renderer(to_s, variables)
from_s_value = None if from_s is None else renderer(from_s, variables) # If new one doesn't match, nothing to do
if _in_range(above, below, to_s_value) and \ if not condition.numeric_state(
(from_s is None or not _in_range(above, below, from_s_value)): hass, to_s, below, above, value_template, variables):
return
# Only match if old didn't exist or existed but didn't match
# Written as: skip if old one did exist and matched
if from_s is not None and condition.numeric_state(
hass, from_s, below, above, value_template, variables):
return
variables['trigger']['from_state'] = from_s variables['trigger']['from_state'] = from_s
variables['trigger']['from_value'] = from_s_value
variables['trigger']['to_state'] = to_s variables['trigger']['to_state'] = to_s
variables['trigger']['to_value'] = to_s_value
action(variables) action(variables)
@ -79,48 +67,3 @@ def trigger(hass, config, action):
hass, entity_id, state_automation_listener) hass, entity_id, state_automation_listener)
return True return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
entity_id = config.get(CONF_ENTITY_ID)
if entity_id is None:
_LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID)
return None
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None:
_LOGGER.error("Missing configuration key."
" One of %s or %s is required",
CONF_BELOW, CONF_ABOVE)
return None
renderer = partial(_renderer, hass, value_template)
def if_numeric_state(variables):
"""Test numeric state condition."""
state = hass.states.get(entity_id)
return state is not None and _in_range(above, below, renderer(state))
return if_numeric_state
def _in_range(range_start, range_end, value):
"""Check if value is inside the range."""
try:
value = float(value)
except ValueError:
_LOGGER.warning("Value returned from template is not a number: %s",
value)
return False
if range_start is not None and range_end is not None:
return float(range_start) <= value < float(range_end)
elif range_end is not None:
return value < float(range_end)
else:
return float(range_start) <= value

View File

@ -4,15 +4,11 @@ Offer state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#state-trigger at https://home-assistant.io/components/automation/#state-trigger
""" """
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM) EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, CONF_PLATFORM)
from homeassistant.components.automation.time import (
CONF_HOURS, CONF_MINUTES, CONF_SECONDS)
from homeassistant.helpers.event import track_state_change, track_point_in_time from homeassistant.helpers.event import track_state_change, track_point_in_time
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,46 +18,19 @@ CONF_TO = "to"
CONF_STATE = "state" CONF_STATE = "state"
CONF_FOR = "for" CONF_FOR = "for"
BASE_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'state', vol.Required(CONF_PLATFORM): 'state',
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
# These are str on purpose. Want to catch YAML conversions
CONF_STATE: str,
CONF_FOR: vol.All(vol.Schema({
CONF_HOURS: vol.Coerce(int),
CONF_MINUTES: vol.Coerce(int),
CONF_SECONDS: vol.Coerce(int),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS)),
})
TRIGGER_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA.extend({
# These are str on purpose. Want to catch YAML conversions # These are str on purpose. Want to catch YAML conversions
CONF_FROM: str, CONF_FROM: str,
CONF_TO: str, CONF_TO: str,
CONF_STATE: str,
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
}), }),
vol.Any(cv.key_dependency(CONF_FOR, CONF_TO), vol.Any(cv.key_dependency(CONF_FOR, CONF_TO),
cv.key_dependency(CONF_FOR, CONF_STATE)) cv.key_dependency(CONF_FOR, CONF_STATE))
)) )
IF_ACTION_SCHEMA = vol.Schema(vol.All(
BASE_SCHEMA,
cv.key_dependency(CONF_FOR, CONF_STATE)
))
def get_time_config(config):
"""Helper function to extract the time specified in the configuration."""
if CONF_FOR not in config:
return None
hours = config[CONF_FOR].get(CONF_HOURS)
minutes = config[CONF_FOR].get(CONF_MINUTES)
seconds = config[CONF_FOR].get(CONF_SECONDS)
return timedelta(hours=(hours or 0.0),
minutes=(minutes or 0.0),
seconds=(seconds or 0.0))
def trigger(hass, config, action): def trigger(hass, config, action):
@ -69,7 +38,7 @@ def trigger(hass, config, action):
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
from_state = config.get(CONF_FROM, MATCH_ALL) from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
time_delta = get_time_config(config) time_delta = config.get(CONF_FOR)
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
@ -97,7 +66,7 @@ def trigger(hass, config, action):
def state_for_cancel_listener(entity, inner_from_s, inner_to_s): def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
"""Fire on changes and cancel for listener if changed.""" """Fire on changes and cancel for listener if changed."""
if inner_to_s == to_s: if inner_to_s.state == to_s.state:
return return
hass.bus.remove_listener(EVENT_TIME_CHANGED, hass.bus.remove_listener(EVENT_TIME_CHANGED,
attached_state_for_listener) attached_state_for_listener)
@ -114,20 +83,3 @@ def trigger(hass, config, action):
hass, entity_id, state_automation_listener, from_state, to_state) hass, entity_id, state_automation_listener, from_state, to_state)
return True return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
entity_id = config.get(CONF_ENTITY_ID)
state = config.get(CONF_STATE)
time_delta = get_time_config(config)
def if_state(variables):
"""Test if condition."""
is_state = hass.states.is_state(entity_id, state)
return (time_delta is None and is_state or
time_delta is not None and
dt_util.utcnow() - time_delta >
hass.states.get(entity_id).last_changed)
return if_state

View File

@ -10,8 +10,6 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM
import homeassistant.util.dt as dt_util
from homeassistant.components import sun
from homeassistant.helpers.event import track_sunrise, track_sunset from homeassistant.helpers.event import track_sunrise, track_sunset
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -29,26 +27,13 @@ EVENT_SUNRISE = 'sunrise'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET))
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'sun', vol.Required(CONF_PLATFORM): 'sun',
vol.Required(CONF_EVENT): _SUN_EVENT, vol.Required(CONF_EVENT):
vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET)),
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
}) })
IF_ACTION_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_PLATFORM): 'sun',
CONF_BEFORE: _SUN_EVENT,
CONF_AFTER: _SUN_EVENT,
vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period,
vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period,
}),
cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER),
)
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
@ -72,55 +57,3 @@ def trigger(hass, config, action):
track_sunset(hass, call_action, offset) track_sunset(hass, call_action, offset)
return True return True
def if_action(hass, config):
"""Wrap action method with sun based condition."""
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
before_offset = config.get(CONF_BEFORE_OFFSET)
after_offset = config.get(CONF_AFTER_OFFSET)
if before is None:
def before_func():
"""Return no point in time."""
return None
elif before == EVENT_SUNRISE:
def before_func():
"""Return time before sunrise."""
return sun.next_rising(hass) + before_offset
else:
def before_func():
"""Return time before sunset."""
return sun.next_setting(hass) + before_offset
if after is None:
def after_func():
"""Return no point in time."""
return None
elif after == EVENT_SUNRISE:
def after_func():
"""Return time after sunrise."""
return sun.next_rising(hass) + after_offset
else:
def after_func():
"""Return time after sunset."""
return sun.next_setting(hass) + after_offset
def time_if(variables):
"""Validate time based if-condition."""
now = dt_util.now()
before = before_func()
after = after_func()
if before is not None and now > now.replace(hour=before.hour,
minute=before.minute):
return False
if after is not None and now < now.replace(hour=after.hour,
minute=after.minute):
return False
return True
return time_if

View File

@ -10,8 +10,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL) CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
from homeassistant.exceptions import TemplateError from homeassistant.helpers import condition
from homeassistant.helpers import template
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -34,7 +33,7 @@ def trigger(hass, config, action):
def state_changed_listener(entity_id, from_s, to_s): def state_changed_listener(entity_id, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal already_triggered nonlocal already_triggered
template_result = _check_template(hass, value_template) template_result = condition.template(hass, value_template)
# Check to see if template returns true # Check to see if template returns true
if template_result and not already_triggered: if template_result and not already_triggered:
@ -52,27 +51,3 @@ def trigger(hass, config, action):
track_state_change(hass, MATCH_ALL, state_changed_listener) track_state_change(hass, MATCH_ALL, state_changed_listener)
return True return True
def if_action(hass, config):
"""Wrap action method with state based condition."""
value_template = config.get(CONF_VALUE_TEMPLATE)
return lambda variables: _check_template(hass, value_template,
variables=variables)
def _check_template(hass, value_template, variables=None):
"""Check if result of template is true."""
try:
value = template.render(hass, value_template, variables)
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning(ex)
else:
_LOGGER.error(ex)
return False
return value.lower() == 'true'

View File

@ -6,38 +6,38 @@ at https://home-assistant.io/components/automation/#time-trigger
""" """
import logging import logging
import homeassistant.util.dt as dt_util import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_change from homeassistant.helpers.event import track_time_change
CONF_HOURS = "hours" CONF_HOURS = "hours"
CONF_MINUTES = "minutes" CONF_MINUTES = "minutes"
CONF_SECONDS = "seconds" CONF_SECONDS = "seconds"
CONF_BEFORE = "before"
CONF_AFTER = "after" CONF_AFTER = "after"
CONF_WEEKDAY = "weekday"
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
CONF_AFTER: cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES,
CONF_SECONDS, CONF_AFTER))
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
if CONF_AFTER in config: if CONF_AFTER in config:
after = dt_util.parse_time(config[CONF_AFTER]) after = config.get(CONF_AFTER)
if after is None:
_error_time(config[CONF_AFTER], CONF_AFTER)
return False
hours, minutes, seconds = after.hour, after.minute, after.second hours, minutes, seconds = after.hour, after.minute, after.second
elif (CONF_HOURS in config or CONF_MINUTES in config or else:
CONF_SECONDS in config):
hours = config.get(CONF_HOURS) hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES) minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS) seconds = config.get(CONF_SECONDS)
else:
_LOGGER.error('One of %s, %s, %s OR %s needs to be specified',
CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AFTER)
return False
def time_automation_listener(now): def time_automation_listener(now):
"""Listen for time changes and calls action.""" """Listen for time changes and calls action."""
@ -52,58 +52,3 @@ def trigger(hass, config, action):
hour=hours, minute=minutes, second=seconds) hour=hours, minute=minutes, second=seconds)
return True return True
def if_action(hass, config):
"""Wrap action method with time based condition."""
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
weekday = config.get(CONF_WEEKDAY)
if before is None and after is None and weekday is None:
_LOGGER.error(
"Missing if-condition configuration key %s, %s or %s",
CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY)
return None
if before is not None:
before = dt_util.parse_time(before)
if before is None:
_error_time(before, CONF_BEFORE)
return None
if after is not None:
after = dt_util.parse_time(after)
if after is None:
_error_time(after, CONF_AFTER)
return None
def time_if(variables):
"""Validate time based if-condition."""
now = dt_util.now()
if before is not None and now > now.replace(hour=before.hour,
minute=before.minute):
return False
if after is not None and now < now.replace(hour=after.hour,
minute=after.minute):
return False
if weekday is not None:
now_weekday = WEEKDAYS[now.weekday()]
if isinstance(weekday, str) and weekday != now_weekday or \
now_weekday not in weekday:
return False
return True
return time_if
def _error_time(value, key):
"""Helper method to print error."""
_LOGGER.error(
"Received invalid value for '%s': %s", key, value)
if isinstance(value, int):
_LOGGER.error('Make sure you wrap time values in quotes')

View File

@ -6,11 +6,10 @@ at https://home-assistant.io/components/automation/#zone-trigger
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.components import zone from homeassistant.const import MATCH_ALL, CONF_PLATFORM
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, CONF_PLATFORM)
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import (
condition, config_validation as cv, location)
CONF_ENTITY_ID = "entity_id" CONF_ENTITY_ID = "entity_id"
CONF_ZONE = "zone" CONF_ZONE = "zone"
@ -27,12 +26,6 @@ TRIGGER_SCHEMA = vol.Schema({
vol.Any(EVENT_ENTER, EVENT_LEAVE), vol.Any(EVENT_ENTER, EVENT_LEAVE),
}) })
IF_ACTION_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'zone',
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_ZONE): cv.entity_id,
})
def trigger(hass, config, action): def trigger(hass, config, action):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
@ -42,15 +35,16 @@ def trigger(hass, config, action):
def zone_automation_listener(entity, from_s, to_s): def zone_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
if from_s and None in (from_s.attributes.get(ATTR_LATITUDE), if from_s and not location.has_location(from_s) or \
from_s.attributes.get(ATTR_LONGITUDE)) or \ not location.has_location(to_s):
None in (to_s.attributes.get(ATTR_LATITUDE),
to_s.attributes.get(ATTR_LONGITUDE)):
return return
zone_state = hass.states.get(zone_entity_id) zone_state = hass.states.get(zone_entity_id)
from_match = _in_zone(hass, zone_state, from_s) if from_s else None if from_s:
to_match = _in_zone(hass, zone_state, to_s) from_match = condition.zone(hass, zone_state, from_s)
else:
from_match = False
to_match = condition.zone(hass, zone_state, to_s)
# pylint: disable=too-many-boolean-expressions # pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \ if event == EVENT_ENTER and not from_match and to_match or \
@ -69,28 +63,3 @@ def trigger(hass, config, action):
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL)
return True return True
def if_action(hass, config):
"""Wrap action method with zone based condition."""
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get(CONF_ZONE)
def if_in_zone(variables):
"""Test if condition."""
zone_state = hass.states.get(zone_entity_id)
return _in_zone(hass, zone_state, hass.states.get(entity_id))
return if_in_zone
def _in_zone(hass, zone_state, state):
"""Check if state is in zone."""
if not state or None in (state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE)):
return False
return zone_state and zone.in_zone(
zone_state, state.attributes.get(ATTR_LATITUDE),
state.attributes.get(ATTR_LONGITUDE),
state.attributes.get(ATTR_GPS_ACCURACY, 0))

View File

@ -12,6 +12,8 @@ MATCH_ALL = '*'
# If no name is specified # If no name is specified
DEVICE_DEFAULT_NAME = "Unnamed Device" DEVICE_DEFAULT_NAME = "Unnamed Device"
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
# #### CONFIG #### # #### CONFIG ####
CONF_ALIAS = "alias" CONF_ALIAS = "alias"
CONF_ICON = "icon" CONF_ICON = "icon"
@ -34,9 +36,13 @@ CONF_ACCESS_TOKEN = "access_token"
CONF_FILENAME = "filename" CONF_FILENAME = "filename"
CONF_MONITORED_CONDITIONS = 'monitored_conditions' CONF_MONITORED_CONDITIONS = 'monitored_conditions'
CONF_OPTIMISTIC = 'optimistic' CONF_OPTIMISTIC = 'optimistic'
CONF_ENTITY_ID = "entity_id"
CONF_ENTITY_NAMESPACE = "entity_namespace" CONF_ENTITY_NAMESPACE = "entity_namespace"
CONF_SCAN_INTERVAL = "scan_interval" CONF_SCAN_INTERVAL = "scan_interval"
CONF_VALUE_TEMPLATE = "value_template" CONF_VALUE_TEMPLATE = "value_template"
CONF_CONDITION = 'condition'
CONF_BELOW = 'below'
CONF_ABOVE = 'above'
# #### EVENTS #### # #### EVENTS ####
EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_START = "homeassistant_start"

View File

@ -0,0 +1,290 @@
"""Offer reusable conditions."""
from datetime import timedelta
import logging
import sys
from homeassistant.components import (
zone as zone_cmp, sun as sun_cmp)
from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION,
WEEKDAYS)
from homeassistant.exceptions import TemplateError, HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template import render
import homeassistant.util.dt as dt_util
FROM_CONFIG_FORMAT = '{}_from_config'
CONF_BELOW = 'below'
CONF_ABOVE = 'above'
CONF_STATE = 'state'
CONF_ZONE = 'zone'
EVENT_SUNRISE = 'sunrise'
EVENT_SUNSET = 'sunset'
_LOGGER = logging.getLogger(__name__)
def from_config(config, config_validation=True):
"""Turn a condition configuration into a method."""
factory = getattr(
sys.modules[__name__],
FROM_CONFIG_FORMAT.format(config.get(CONF_CONDITION)), None)
if factory is None:
raise HomeAssistantError('Invalid condition "{}" specified {}'.format(
config.get(CONF_CONDITION), config))
return factory(config, config_validation)
def and_from_config(config, config_validation=True):
"""Create multi condition matcher using 'AND'."""
if config_validation:
config = cv.AND_CONDITION_SCHEMA(config)
checks = [from_config(entry) for entry in config['conditions']]
def if_and_condition(hass, variables=None):
"""Test and condition."""
return all(check(hass, variables) for check in checks)
return if_and_condition
def or_from_config(config, config_validation=True):
"""Create multi condition matcher using 'OR'."""
if config_validation:
config = cv.OR_CONDITION_SCHEMA(config)
checks = [from_config(entry) for entry in config['conditions']]
def if_or_condition(hass, variables=None):
"""Test and condition."""
return any(check(hass, variables) for check in checks)
return if_or_condition
# pylint: disable=too-many-arguments
def numeric_state(hass, entity, below=None, above=None, value_template=None,
variables=None):
"""Test a numeric state condition."""
if isinstance(entity, str):
entity = hass.states.get(entity)
if entity is None:
return False
if value_template is None:
value = entity.state
else:
variables = dict(variables or {})
variables['state'] = entity
try:
value = render(hass, value_template, variables)
except TemplateError as ex:
_LOGGER.error(ex)
return False
try:
value = float(value)
except ValueError:
_LOGGER.warning("Value cannot be processed as a number: %s", value)
return False
print(below, value, above)
if below is not None and value > below:
return False
if above is not None and value < above:
return False
return True
def numeric_state_from_config(config, config_validation=True):
"""Wrap action method with state based condition."""
if config_validation:
config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config)
entity_id = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
def if_numeric_state(hass, variables=None):
"""Test numeric state condition."""
return numeric_state(hass, entity_id, below, above, value_template,
variables)
return if_numeric_state
def state(hass, entity, req_state, for_period=None):
"""Test if state matches requirements."""
if isinstance(entity, str):
entity = hass.states.get(entity)
if entity is None:
return False
is_state = entity.state == req_state
if for_period is None or not is_state:
return is_state
return dt_util.utcnow() - for_period > entity.last_changed
def state_from_config(config, config_validation=True):
"""Wrap action method with state based condition."""
if config_validation:
config = cv.STATE_CONDITION_SCHEMA(config)
entity_id = config.get(CONF_ENTITY_ID)
req_state = config.get(CONF_STATE)
for_period = config.get('for')
def if_state(hass, variables=None):
"""Test if condition."""
return state(hass, entity_id, req_state, for_period)
return if_state
def sun(hass, before=None, after=None, before_offset=None, after_offset=None):
"""Test if current time matches sun requirements."""
now = dt_util.now().time()
before_offset = before_offset or timedelta(0)
after_offset = after_offset or timedelta(0)
if before == EVENT_SUNRISE and now > (sun_cmp.next_rising(hass) +
before_offset).time():
return False
elif before == EVENT_SUNSET and now > (sun_cmp.next_setting(hass) +
before_offset).time():
return False
if after == EVENT_SUNRISE and now < (sun_cmp.next_rising(hass) +
after_offset).time():
return False
elif after == EVENT_SUNSET and now < (sun_cmp.next_setting(hass) +
after_offset).time():
return False
return True
def sun_from_config(config, config_validation=True):
"""Wrap action method with sun based condition."""
if config_validation:
config = cv.SUN_CONDITION_SCHEMA(config)
before = config.get('before')
after = config.get('after')
before_offset = config.get('before_offset')
after_offset = config.get('after_offset')
def time_if(hass, variables=None):
"""Validate time based if-condition."""
return sun(hass, before, after, before_offset, after_offset)
return time_if
def template(hass, value_template, variables=None):
"""Test if template condition matches."""
try:
value = render(hass, value_template, variables)
except TemplateError as ex:
_LOGGER.error('Error duriong template condition: %s', ex)
return False
return value.lower() == 'true'
def template_from_config(config, config_validation=True):
"""Wrap action method with state based condition."""
if config_validation:
config = cv.TEMPLATE_CONDITION_SCHEMA(config)
value_template = config.get(CONF_VALUE_TEMPLATE)
def template_if(hass, variables=None):
"""Validate template based if-condition."""
return template(hass, value_template, variables)
return template_if
def time(before=None, after=None, weekday=None):
"""Test if local time condition matches."""
now = dt_util.now()
now_time = now.time()
if before is not None and now_time > before:
return False
if after is not None and now_time < after:
return False
if weekday is not None:
now_weekday = WEEKDAYS[now.weekday()]
if isinstance(weekday, str) and weekday != now_weekday or \
now_weekday not in weekday:
return False
return True
def time_from_config(config, config_validation=True):
"""Wrap action method with time based condition."""
if config_validation:
config = cv.TIME_CONDITION_SCHEMA(config)
before = config.get('before')
after = config.get('after')
weekday = config.get('weekday')
def time_if(hass, variables=None):
"""Validate time based if-condition."""
return time(before, after, weekday)
return time_if
def zone(hass, zone_ent, entity):
"""Test if zone-condition matches."""
if isinstance(zone_ent, str):
zone_ent = hass.states.get(zone_ent)
if zone_ent is None:
return False
if isinstance(entity, str):
entity = hass.states.get(entity)
if entity is None:
return False
latitude = entity.attributes.get(ATTR_LATITUDE)
longitude = entity.attributes.get(ATTR_LONGITUDE)
if latitude is None or longitude is None:
return False
return zone_cmp.in_zone(zone_ent, latitude, longitude,
entity.attributes.get(ATTR_GPS_ACCURACY, 0))
def zone_from_config(config, config_validation=True):
"""Wrap action method with zone based condition."""
if config_validation:
config = cv.ZONE_CONDITION_SCHEMA(config)
entity_id = config.get(CONF_ENTITY_ID)
zone_entity_id = config.get('zone')
def if_in_zone(hass, variables=None):
"""Test if condition."""
return zone(hass, zone_entity_id, entity_id)
return if_in_zone

View File

@ -7,13 +7,16 @@ import voluptuous as vol
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.const import ( from homeassistant.const import (
CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_ALIAS) CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS,
CONF_CONDITION, CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.entity import valid_entity_id from homeassistant.helpers.entity import valid_entity_id
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util import slugify from homeassistant.util import slugify
# pylint: disable=invalid-name # pylint: disable=invalid-name
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'"
# Home Assistant types # Home Assistant types
byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255)) byte = vol.All(vol.Coerce(int), vol.Range(min=0, max=255))
small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1)) small_float = vol.All(vol.Coerce(float), vol.Range(min=0, max=1))
@ -105,9 +108,10 @@ time_period_dict = vol.All(
def time_period_str(value): def time_period_str(value):
"""Validate and transform time offset.""" """Validate and transform time offset."""
if not isinstance(value, str): if isinstance(value, int):
raise vol.Invalid( raise vol.Invalid('Make sure you wrap time values in quotes')
'offset {} should be format HH:MM or HH:MM:SS'.format(value)) elif not isinstance(value, str):
raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
negative_offset = False negative_offset = False
if value.startswith('-'): if value.startswith('-'):
@ -119,8 +123,7 @@ def time_period_str(value):
try: try:
parsed = [int(x) for x in value.split(':')] parsed = [int(x) for x in value.split(':')]
except ValueError: except ValueError:
raise vol.Invalid( raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
'offset {} should be format HH:MM or HH:MM:SS'.format(value))
if len(parsed) == 2: if len(parsed) == 2:
hour, minute = parsed hour, minute = parsed
@ -128,8 +131,7 @@ def time_period_str(value):
elif len(parsed) == 3: elif len(parsed) == 3:
hour, minute, second = parsed hour, minute, second = parsed
else: else:
raise vol.Invalid( raise vol.Invalid(TIME_PERIOD_ERROR.format(value))
'offset {} should be format HH:MM or HH:MM:SS'.format(value))
offset = timedelta(hours=hour, minutes=minute, seconds=second) offset = timedelta(hours=hour, minutes=minute, seconds=second)
@ -217,6 +219,16 @@ def template(value):
raise vol.Invalid('invalid template ({})'.format(ex)) raise vol.Invalid('invalid template ({})'.format(ex))
def time(value):
"""Validate time."""
time_val = dt_util.parse_time(value)
if time_val is None:
raise vol.Invalid('Invalid time specified: {}'.format(value))
return time_val
def time_zone(value): def time_zone(value):
"""Validate timezone.""" """Validate timezone."""
if dt_util.get_time_zone(value) is not None: if dt_util.get_time_zone(value) is not None:
@ -225,6 +237,8 @@ def time_zone(value):
'Invalid time zone passed in. Valid options can be found here: ' 'Invalid time zone passed in. Valid options can be found here: '
'http://en.wikipedia.org/wiki/List_of_tz_database_time_zones') 'http://en.wikipedia.org/wiki/List_of_tz_database_time_zones')
weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)])
# Validator helpers # Validator helpers
@ -261,9 +275,83 @@ SERVICE_SCHEMA = vol.All(vol.Schema({
vol.Exclusive('service_template', 'service name'): template, vol.Exclusive('service_template', 'service name'): template,
vol.Optional('data'): dict, vol.Optional('data'): dict,
vol.Optional('data_template'): {match_all: template}, vol.Optional('data_template'): {match_all: template},
vol.Optional('entity_id'): entity_ids, vol.Optional(CONF_ENTITY_ID): entity_ids,
}), has_at_least_one_key('service', 'service_template')) }), has_at_least_one_key('service', 'service_template'))
NUMERIC_STATE_CONDITION_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_CONDITION): 'numeric_state',
vol.Required(CONF_ENTITY_ID): entity_id,
CONF_BELOW: vol.Coerce(float),
CONF_ABOVE: vol.Coerce(float),
vol.Optional(CONF_VALUE_TEMPLATE): template,
}), has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
STATE_CONDITION_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_CONDITION): 'state',
vol.Required(CONF_ENTITY_ID): entity_id,
vol.Required('state'): str,
vol.Optional('for'): vol.All(time_period, positive_timedelta),
# To support use_trigger_value in automation
# Deprecated 2016/04/25
vol.Optional('from'): str,
}), key_dependency('for', 'state'))
SUN_CONDITION_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_CONDITION): 'sun',
vol.Optional('before'): vol.Any('sunset', 'sunrise'),
vol.Optional('before_offset'): time_period,
vol.Optional('after'): vol.All(vol.Lower, vol.Any('sunset', 'sunrise')),
vol.Optional('after_offset'): time_period,
}), has_at_least_one_key('before', 'after'))
TEMPLATE_CONDITION_SCHEMA = vol.Schema({
vol.Required(CONF_CONDITION): 'template',
vol.Required(CONF_VALUE_TEMPLATE): template,
})
TIME_CONDITION_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_CONDITION): 'time',
'before': time,
'after': time,
'weekday': weekdays,
}), has_at_least_one_key('before', 'after', 'weekday'))
ZONE_CONDITION_SCHEMA = vol.Schema({
vol.Required(CONF_CONDITION): 'zone',
vol.Required(CONF_ENTITY_ID): entity_id,
'zone': entity_id,
# To support use_trigger_value in automation
# Deprecated 2016/04/25
vol.Optional('event'): vol.Any('enter', 'leave'),
})
AND_CONDITION_SCHEMA = vol.Schema({
vol.Required(CONF_CONDITION): 'and',
vol.Required('conditions'): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
)
})
OR_CONDITION_SCHEMA = vol.Schema({
vol.Required(CONF_CONDITION): 'or',
vol.Required('conditions'): vol.All(
ensure_list,
# pylint: disable=unnecessary-lambda
[lambda value: CONDITION_SCHEMA(value)],
)
})
CONDITION_SCHEMA = vol.Any(
NUMERIC_STATE_CONDITION_SCHEMA,
STATE_CONDITION_SCHEMA,
SUN_CONDITION_SCHEMA,
TEMPLATE_CONDITION_SCHEMA,
TIME_CONDITION_SCHEMA,
ZONE_CONDITION_SCHEMA,
)
_SCRIPT_DELAY_SCHEMA = vol.Schema({ _SCRIPT_DELAY_SCHEMA = vol.Schema({
vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_ALIAS): string,
vol.Required("delay"): vol.All(time_period, positive_timedelta) vol.Required("delay"): vol.All(time_period, positive_timedelta)
@ -271,5 +359,6 @@ _SCRIPT_DELAY_SCHEMA = vol.Schema({
SCRIPT_SCHEMA = vol.All( SCRIPT_SCHEMA = vol.All(
ensure_list, ensure_list,
[vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA, EVENT_SCHEMA)], [vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA, EVENT_SCHEMA,
CONDITION_SCHEMA)],
) )

View File

@ -4,9 +4,9 @@ import threading
from itertools import islice from itertools import islice
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.const import EVENT_TIME_CHANGED, CONF_CONDITION
from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers import service from homeassistant.helpers import service, condition
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,6 +76,10 @@ class Script():
self._change_listener() self._change_listener()
return return
elif CONF_CONDITION in action:
if not self._check_condition(action, variables):
break
elif CONF_EVENT in action: elif CONF_EVENT in action:
self._fire_event(action) self._fire_event(action)
@ -111,6 +115,13 @@ class Script():
self._log("Executing step %s", self.last_action) self._log("Executing step %s", self.last_action)
self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA)) self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA))
def _check_condition(self, action, variables):
"""Test if condition is matching."""
self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION])
check = condition.from_config(action)(self.hass, False)
self._log("Test condition %s: %s", self.last_action, check)
return check
def _remove_listener(self): def _remove_listener(self):
"""Remove point in time listener, if any.""" """Remove point in time listener, if any."""
if self._delay_listener: if self._delay_listener:

View File

@ -33,8 +33,8 @@ def render_with_possible_json_value(hass, template, value,
try: try:
return render(hass, template, variables) return render(hass, template, variables)
except TemplateError: except TemplateError as ex:
_LOGGER.exception('Error parsing value') _LOGGER.error('Error parsing value: %s', ex)
return value if error_value is _SENTINEL else error_value return value if error_value is _SENTINEL else error_value

View File

@ -146,12 +146,12 @@ class TestAutomation(unittest.TestCase):
], ],
'condition': [ 'condition': [
{ {
'platform': 'state', 'condition': 'state',
'entity_id': entity_id, 'entity_id': entity_id,
'state': '100' 'state': '100'
}, },
{ {
'platform': 'numeric_state', 'condition': 'numeric_state',
'entity_id': entity_id, 'entity_id': entity_id,
'below': 150 'below': 150
} }
@ -231,6 +231,7 @@ class TestAutomation(unittest.TestCase):
{ {
'platform': 'state', 'platform': 'state',
'entity_id': entity_id, 'entity_id': entity_id,
'from': '120',
'state': '100' 'state': '100'
}, },
{ {
@ -248,10 +249,14 @@ class TestAutomation(unittest.TestCase):
self.hass.states.set(entity_id, 100) self.hass.states.set(entity_id, 100)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls)) self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 120) self.hass.states.set(entity_id, 120)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 100)
self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls)) self.assertEqual(2, len(self.calls))
self.hass.states.set(entity_id, 151) self.hass.states.set(entity_id, 151)

View File

@ -253,6 +253,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.another_entity', 'entity_id': 'test.another_entity',
'below': 100,
}, },
'action': { 'action': {
'service': 'test.automation' 'service': 'test.automation'
@ -441,13 +442,11 @@ class TestAutomationNumericState(unittest.TestCase):
'data_template': { 'data_template': {
'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join((
'platform', 'entity_id', 'below', 'above', 'platform', 'entity_id', 'below', 'above',
'from_state.state', 'from_value', 'from_state.state', 'to_state.state'))
'to_state.state', 'to_value'))
}, },
} }
} }
}) })
# 9 is below 10
self.hass.states.set('test.entity', 'test state 1', self.hass.states.set('test.entity', 'test state 1',
{'test_attribute': '1.2'}) {'test_attribute': '1.2'})
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -456,8 +455,8 @@ class TestAutomationNumericState(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
self.assertEqual( self.assertEqual(
'numeric_state - test.entity - 10 - None - test state 1 - 12.0 - ' 'numeric_state - test.entity - 10.0 - None - test state 1 - '
'test state 2 - 9.0', 'test state 2',
self.calls[0].data['some']) self.calls[0].data['some'])
def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self):

View File

@ -40,7 +40,7 @@ class TestAutomationSun(unittest.TestCase):
now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC)
trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.util.dt.utcnow',
return_value=now): return_value=now):
_setup_component(self.hass, automation.DOMAIN, { _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
@ -67,7 +67,7 @@ class TestAutomationSun(unittest.TestCase):
now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC)
trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.util.dt.utcnow',
return_value=now): return_value=now):
_setup_component(self.hass, automation.DOMAIN, { _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
@ -94,7 +94,7 @@ class TestAutomationSun(unittest.TestCase):
now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC)
trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.util.dt.utcnow',
return_value=now): return_value=now):
_setup_component(self.hass, automation.DOMAIN, { _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
@ -128,7 +128,7 @@ class TestAutomationSun(unittest.TestCase):
now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC)
trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.util.dt.utcnow',
return_value=now): return_value=now):
_setup_component(self.hass, automation.DOMAIN, { _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
@ -170,14 +170,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -206,14 +206,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -243,14 +243,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -280,14 +280,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -318,21 +318,21 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -364,7 +364,7 @@ class TestAutomationSun(unittest.TestCase):
# Before # Before
now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain'))
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -372,7 +372,7 @@ class TestAutomationSun(unittest.TestCase):
# After # After
now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain'))
with patch('homeassistant.components.automation.sun.dt_util.now', with patch('homeassistant.util.dt.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()

View File

@ -194,7 +194,7 @@ class TestAutomationTime(unittest.TestCase):
def test_if_not_working_if_no_values_in_conf_provided(self): def test_if_not_working_if_no_values_in_conf_provided(self):
"""Test for failure if no configuration.""" """Test for failure if no configuration."""
assert _setup_component(self.hass, automation.DOMAIN, { assert not _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
'trigger': { 'trigger': {
'platform': 'time', 'platform': 'time',
@ -211,13 +211,12 @@ class TestAutomationTime(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
@patch('homeassistant.components.automation.time._LOGGER.error') def test_if_not_fires_using_wrong_after(self):
def test_if_not_fires_using_wrong_after(self, mock_error):
"""YAML translates time values to total seconds. """YAML translates time values to total seconds.
This should break the before rule. This should break the before rule.
""" """
assert _setup_component(self.hass, automation.DOMAIN, { assert not _setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: { automation.DOMAIN: {
'trigger': { 'trigger': {
'platform': 'time', 'platform': 'time',
@ -235,7 +234,6 @@ class TestAutomationTime(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
self.assertEqual(2, mock_error.call_count)
def test_if_action_before(self): def test_if_action_before(self):
"""Test for if action before.""" """Test for if action before."""
@ -258,14 +256,14 @@ class TestAutomationTime(unittest.TestCase):
before_10 = dt_util.now().replace(hour=8) before_10 = dt_util.now().replace(hour=8)
after_10 = dt_util.now().replace(hour=14) after_10 = dt_util.now().replace(hour=14)
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=before_10): return_value=before_10):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=after_10): return_value=after_10):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -293,14 +291,14 @@ class TestAutomationTime(unittest.TestCase):
before_10 = dt_util.now().replace(hour=8) before_10 = dt_util.now().replace(hour=8)
after_10 = dt_util.now().replace(hour=14) after_10 = dt_util.now().replace(hour=14)
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=before_10): return_value=before_10):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=after_10): return_value=after_10):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -329,14 +327,14 @@ class TestAutomationTime(unittest.TestCase):
monday = dt_util.now() - timedelta(days=days_past_monday) monday = dt_util.now() - timedelta(days=days_past_monday)
tuesday = monday + timedelta(days=1) tuesday = monday + timedelta(days=1)
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=monday): return_value=monday):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=tuesday): return_value=tuesday):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -366,21 +364,21 @@ class TestAutomationTime(unittest.TestCase):
tuesday = monday + timedelta(days=1) tuesday = monday + timedelta(days=1)
wednesday = tuesday + timedelta(days=1) wednesday = tuesday + timedelta(days=1)
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=monday): return_value=monday):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=tuesday): return_value=tuesday):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls)) self.assertEqual(2, len(self.calls))
with patch('homeassistant.components.automation.time.dt_util.now', with patch('homeassistant.helpers.condition.dt_util.now',
return_value=wednesday): return_value=wednesday):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()

View File

@ -0,0 +1,68 @@
"""Test the condition helper."""
from homeassistant.helpers import condition
from tests.common import get_test_home_assistant
class TestConditionHelper:
"""Test condition helpers."""
def setup_method(self, method):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
def teardown_method(self, method):
"""Stop everything that was started."""
self.hass.stop()
def test_and_condition(self):
"""Test the 'and' condition."""
test = condition.from_config({
'condition': 'and',
'conditions': [
{
'condition': 'state',
'entity_id': 'sensor.temperature',
'state': '100',
}, {
'condition': 'numeric_state',
'entity_id': 'sensor.temperature',
'below': 110,
}
]
})
self.hass.states.set('sensor.temperature', 120)
assert not test(self.hass)
self.hass.states.set('sensor.temperature', 105)
assert not test(self.hass)
self.hass.states.set('sensor.temperature', 100)
assert test(self.hass)
def test_or_condition(self):
"""Test the 'or' condition."""
test = condition.from_config({
'condition': 'or',
'conditions': [
{
'condition': 'state',
'entity_id': 'sensor.temperature',
'state': '100',
}, {
'condition': 'numeric_state',
'entity_id': 'sensor.temperature',
'below': 110,
}
]
})
self.hass.states.set('sensor.temperature', 120)
assert not test(self.hass)
self.hass.states.set('sensor.temperature', 105)
assert test(self.hass)
self.hass.states.set('sensor.temperature', 100)
assert test(self.hass)

View File

@ -216,3 +216,36 @@ class TestScriptHelper(unittest.TestCase):
assert not script_obj.is_running assert not script_obj.is_running
assert len(calls) == 2 assert len(calls) == 2
assert calls[-1].data['hello'] == 'universe' assert calls[-1].data['hello'] == 'universe'
def test_condition(self):
"""Test if we can use conditions in a script."""
event = 'test_event'
events = []
def record_event(event):
"""Add recorded event to set."""
events.append(event)
self.hass.bus.listen(event, record_event)
self.hass.states.set('test.entity', 'hello')
script_obj = script.Script(self.hass, [
{'event': event},
{
'condition': 'state',
'entity_id': 'test.entity',
'state': 'hello',
},
{'event': event},
])
script_obj.run()
self.hass.pool.block_till_done()
assert len(events) == 2
self.hass.states.set('test.entity', 'goodbye')
script_obj.run()
self.hass.pool.block_till_done()
assert len(events) == 3