Allow triggers to be used as condition

This commit is contained in:
Paulus Schoutsen 2015-09-15 08:56:06 -07:00
parent 0584c10ef9
commit c18294ee76
7 changed files with 166 additions and 35 deletions

View file

@ -11,22 +11,24 @@ from homeassistant.util import split_entity_id
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.components import logbook
DOMAIN = "automation"
DOMAIN = 'automation'
DEPENDENCIES = ["group"]
DEPENDENCIES = ['group']
CONF_ALIAS = "alias"
CONF_SERVICE = "execute_service"
CONF_SERVICE_ENTITY_ID = "service_entity_id"
CONF_SERVICE_DATA = "service_data"
CONF_ALIAS = 'alias'
CONF_SERVICE = 'execute_service'
CONF_SERVICE_ENTITY_ID = 'service_entity_id'
CONF_SERVICE_DATA = 'service_data'
CONF_CONDITION = "condition"
CONF_CONDITION = 'condition'
CONF_ACTION = 'action'
CONF_TRIGGER = "trigger"
CONF_CONDITION_TYPE = "condition_type"
CONF_TRIGGER = 'trigger'
CONF_CONDITION_TYPE = 'condition_type'
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
CONDITION_TYPE_AND = 'and'
CONDITION_TYPE_OR = 'or'
CONDITION_TYPE_AND = "and"
CONDITION_TYPE_OR = "or"
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
_LOGGER = logging.getLogger(__name__)
@ -48,11 +50,8 @@ def setup(hass, config):
if action is None:
continue
if CONF_CONDITION in p_config:
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
action = _process_if(hass, config, p_config[CONF_CONDITION],
action, cond_type)
if CONF_CONDITION in p_config or CONF_CONDITION_TYPE in p_config:
action = _process_if(hass, config, p_config, action)
if action is None:
continue
@ -126,22 +125,32 @@ def _migrate_old_config(config):
return new_conf
def _process_if(hass, config, if_configs, action, cond_type):
def _process_if(hass, config, p_config, action):
""" Processes if checks. """
cond_type = p_config.get(CONF_CONDITION_TYPE,
DEFAULT_CONDITION_TYPE).lower()
if_configs = p_config.get(CONF_CONDITION)
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
if use_trigger:
if_configs = p_config[CONF_TRIGGER]
if isinstance(if_configs, dict):
if_configs = [if_configs]
checks = []
for if_config in if_configs:
platform = _resolve_platform('condition', hass, config,
platform = _resolve_platform('if_action', hass, config,
if_config.get(CONF_PLATFORM))
if platform is None:
continue
check = platform.if_action(hass, if_config)
if check is None:
# Invalid conditions are allowed if we base it on trigger
if check is None and not use_trigger:
return None
checks.append(check)
@ -177,15 +186,15 @@ def _process_trigger(hass, config, trigger_configs, name, action):
_LOGGER.error("Error setting up rule %s", name)
def _resolve_platform(requester, hass, config, platform):
def _resolve_platform(method, hass, config, platform):
""" Find automation platform. """
if platform is None:
return None
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
if platform is None:
if platform is None or not hasattr(platform, method):
_LOGGER.error("Unknown automation platform specified for %s: %s",
requester, platform)
method, platform)
return None
return platform

View file

@ -26,7 +26,7 @@ def trigger(hass, config, action):
return False
from_state = config.get(CONF_FROM, MATCH_ALL)
to_state = config.get(CONF_TO, MATCH_ALL)
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """

View file

@ -22,6 +22,14 @@ WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
if CONF_AFTER in config:
after = dt_util.parse_time_str(config[CONF_AFTER])
if after is None:
logging.getLogger(__name__).error(
'Received invalid after value: %s', config[CONF_AFTER])
return False
hours, minutes, seconds = after.hour, after.minute, after.second
hours = convert(config.get(CONF_HOURS), int)
minutes = convert(config.get(CONF_MINUTES), int)
seconds = convert(config.get(CONF_SECONDS), int)
@ -51,22 +59,22 @@ def if_action(hass, config):
def time_if():
""" Validate time based if-condition """
now = dt_util.now()
if before is not None:
# Strip seconds if given
before_h, before_m = before.split(':')[0:2]
time = dt_util.parse_time_str(before)
if time is None:
return False
before_point = now.replace(hour=int(before_h),
minute=int(before_m))
before_point = now.replace(hour=time.hour, minute=time.minute)
if now > before_point:
return False
if after is not None:
# Strip seconds if given
after_h, after_m = after.split(':')[0:2]
time = dt_util.parse_time_str(after)
if time is None:
return False
after_point = now.replace(hour=int(after_h), minute=int(after_m))
after_point = now.replace(hour=time.hour, minute=time.minute)
if now < after_point:
return False

View file

@ -131,3 +131,20 @@ def date_str_to_date(dt_str):
def strip_microseconds(dattim):
""" Returns a copy of dattime object but with microsecond set to 0. """
return dattim.replace(microsecond=0)
def parse_time_str(time_str):
""" Parse a time string (00:20:00) into Time object.
Return None if invalid.
"""
parts = str(time_str).split(':')
if len(parts) < 2:
return None
try:
hour = int(parts[0])
minute = int(parts[1])
second = int(parts[2]) if len(parts) > 2 else 0
return dt.time(hour, minute, second)
except ValueError:
# ValueError if value cannot be converted to an int or not in range
return None

View file

@ -258,3 +258,67 @@ class TestAutomationEvent(unittest.TestCase):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls))
def test_using_trigger_as_condition(self):
""" """
entity_id = 'test.entity'
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': [
{
'platform': 'state',
'entity_id': entity_id,
'state': 100
},
{
'platform': 'numeric_state',
'entity_id': entity_id,
'below': 150
}
],
'condition': 'use_trigger_values',
'action': {
'execute_service': 'test.automation',
}
}
})
self.hass.states.set(entity_id, 100)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 120)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set(entity_id, 151)
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_using_trigger_as_condition_with_invalid_condition(self):
""" Event is not a valid condition. Will it still work? """
entity_id = 'test.entity'
self.hass.states.set(entity_id, 100)
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': [
{
'platform': 'event',
'event_type': 'test_event',
},
{
'platform': 'numeric_state',
'entity_id': entity_id,
'below': 150
}
],
'condition': 'use_trigger_values',
'action': {
'execute_service': 'test.automation',
}
}
})
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))

View file

@ -209,6 +209,24 @@ class TestAutomationState(unittest.TestCase):
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_entity_change_with_state_filter(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'state',
'entity_id': 'test.entity',
'state': 'world'
},
'action': {
'execute_service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_entity_change_with_both_filters(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {

View file

@ -241,7 +241,6 @@ class TestAutomationTime(unittest.TestCase):
fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
@ -260,7 +259,6 @@ class TestAutomationTime(unittest.TestCase):
fire_time_changed(self.hass, dt_util.utcnow().replace(minute=0))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
@ -279,7 +277,6 @@ class TestAutomationTime(unittest.TestCase):
fire_time_changed(self.hass, dt_util.utcnow().replace(second=0))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
@ -301,7 +298,25 @@ class TestAutomationTime(unittest.TestCase):
fire_time_changed(self.hass, dt_util.utcnow().replace(
hour=0, minute=0, second=0))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_using_after(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'time',
'after': '5:00:00',
},
'action': {
'execute_service': 'test.automation'
}
}
}))
fire_time_changed(self.hass, dt_util.utcnow().replace(
hour=5, minute=0, second=0))
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))