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.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
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue