Allow triggers to be used as condition
This commit is contained in:
parent
0584c10ef9
commit
c18294ee76
7 changed files with 166 additions and 35 deletions
|
@ -11,22 +11,24 @@ from homeassistant.util import split_entity_id
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||||
from homeassistant.components import logbook
|
from homeassistant.components import logbook
|
||||||
|
|
||||||
DOMAIN = "automation"
|
DOMAIN = 'automation'
|
||||||
|
|
||||||
DEPENDENCIES = ["group"]
|
DEPENDENCIES = ['group']
|
||||||
|
|
||||||
CONF_ALIAS = "alias"
|
CONF_ALIAS = 'alias'
|
||||||
CONF_SERVICE = "execute_service"
|
CONF_SERVICE = 'execute_service'
|
||||||
CONF_SERVICE_ENTITY_ID = "service_entity_id"
|
CONF_SERVICE_ENTITY_ID = 'service_entity_id'
|
||||||
CONF_SERVICE_DATA = "service_data"
|
CONF_SERVICE_DATA = 'service_data'
|
||||||
|
|
||||||
CONF_CONDITION = "condition"
|
CONF_CONDITION = 'condition'
|
||||||
CONF_ACTION = 'action'
|
CONF_ACTION = 'action'
|
||||||
CONF_TRIGGER = "trigger"
|
CONF_TRIGGER = 'trigger'
|
||||||
CONF_CONDITION_TYPE = "condition_type"
|
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
|
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -48,11 +50,8 @@ def setup(hass, config):
|
||||||
if action is None:
|
if action is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if CONF_CONDITION in p_config:
|
if CONF_CONDITION in p_config or CONF_CONDITION_TYPE in p_config:
|
||||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
action = _process_if(hass, config, p_config, action)
|
||||||
DEFAULT_CONDITION_TYPE).lower()
|
|
||||||
action = _process_if(hass, config, p_config[CONF_CONDITION],
|
|
||||||
action, cond_type)
|
|
||||||
|
|
||||||
if action is None:
|
if action is None:
|
||||||
continue
|
continue
|
||||||
|
@ -126,22 +125,32 @@ def _migrate_old_config(config):
|
||||||
return new_conf
|
return new_conf
|
||||||
|
|
||||||
|
|
||||||
def _process_if(hass, config, if_configs, action, cond_type):
|
def _process_if(hass, config, p_config, action):
|
||||||
""" Processes if checks. """
|
""" 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 isinstance(if_configs, dict):
|
||||||
if_configs = [if_configs]
|
if_configs = [if_configs]
|
||||||
|
|
||||||
checks = []
|
checks = []
|
||||||
for if_config in if_configs:
|
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_config.get(CONF_PLATFORM))
|
||||||
if platform is None:
|
if platform is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
check = platform.if_action(hass, if_config)
|
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
|
return None
|
||||||
|
|
||||||
checks.append(check)
|
checks.append(check)
|
||||||
|
@ -177,15 +186,15 @@ def _process_trigger(hass, config, trigger_configs, name, action):
|
||||||
_LOGGER.error("Error setting up rule %s", name)
|
_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. """
|
""" Find automation platform. """
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return None
|
return None
|
||||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
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",
|
_LOGGER.error("Unknown automation platform specified for %s: %s",
|
||||||
requester, platform)
|
method, platform)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return platform
|
return platform
|
||||||
|
|
|
@ -26,7 +26,7 @@ def trigger(hass, config, action):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
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):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
""" Listens for state changes and calls action. """
|
""" Listens for state changes and calls action. """
|
||||||
|
|
|
@ -22,6 +22,14 @@ WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def trigger(hass, config, action):
|
||||||
""" Listen for state changes based on `config`. """
|
""" 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)
|
hours = convert(config.get(CONF_HOURS), int)
|
||||||
minutes = convert(config.get(CONF_MINUTES), int)
|
minutes = convert(config.get(CONF_MINUTES), int)
|
||||||
seconds = convert(config.get(CONF_SECONDS), int)
|
seconds = convert(config.get(CONF_SECONDS), int)
|
||||||
|
@ -51,22 +59,22 @@ def if_action(hass, config):
|
||||||
def time_if():
|
def time_if():
|
||||||
""" Validate time based if-condition """
|
""" Validate time based if-condition """
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
|
|
||||||
if before is not None:
|
if before is not None:
|
||||||
# Strip seconds if given
|
time = dt_util.parse_time_str(before)
|
||||||
before_h, before_m = before.split(':')[0:2]
|
if time is None:
|
||||||
|
return False
|
||||||
|
|
||||||
before_point = now.replace(hour=int(before_h),
|
before_point = now.replace(hour=time.hour, minute=time.minute)
|
||||||
minute=int(before_m))
|
|
||||||
|
|
||||||
if now > before_point:
|
if now > before_point:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if after is not None:
|
if after is not None:
|
||||||
# Strip seconds if given
|
time = dt_util.parse_time_str(after)
|
||||||
after_h, after_m = after.split(':')[0:2]
|
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:
|
if now < after_point:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -131,3 +131,20 @@ def date_str_to_date(dt_str):
|
||||||
def strip_microseconds(dattim):
|
def strip_microseconds(dattim):
|
||||||
""" Returns a copy of dattime object but with microsecond set to 0. """
|
""" Returns a copy of dattime object but with microsecond set to 0. """
|
||||||
return dattim.replace(microsecond=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
|
||||||
|
|
|
@ -258,3 +258,67 @@ class TestAutomationEvent(unittest.TestCase):
|
||||||
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))
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
|
@ -209,6 +209,24 @@ class TestAutomationState(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))
|
||||||
|
|
||||||
|
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):
|
def test_if_fires_on_entity_change_with_both_filters(self):
|
||||||
self.assertTrue(automation.setup(self.hass, {
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
|
|
|
@ -241,7 +241,6 @@ class TestAutomationTime(unittest.TestCase):
|
||||||
|
|
||||||
fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0))
|
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.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(self.calls))
|
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))
|
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.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(self.calls))
|
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))
|
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.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(self.calls))
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
@ -301,7 +298,25 @@ class TestAutomationTime(unittest.TestCase):
|
||||||
fire_time_changed(self.hass, dt_util.utcnow().replace(
|
fire_time_changed(self.hass, dt_util.utcnow().replace(
|
||||||
hour=0, minute=0, second=0))
|
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.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(self.calls))
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue