diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0bb47a97a3c..45859617624 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -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 diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index bb936d36a1b..8baa0a01d46 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -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. """ diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index a7afa183ba0..821295fdffa 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -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 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index d8fecf20db8..35795a7ae7f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -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 diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index df8b199b700..6a011a072a5 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -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)) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 991c3e066d4..b0410c75014 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -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: { diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 96579af9aa8..f7187592c66 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -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))