From 2a11d02fe499d3a9d6495bd8e1d5f530689fdb9f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Sep 2015 22:25:42 -0700 Subject: [PATCH] Add if to automation --- .../components/automation/__init__.py | 35 ++++- homeassistant/components/automation/event.py | 2 +- homeassistant/components/automation/mqtt.py | 2 +- .../components/automation/numeric_state.py | 2 +- homeassistant/components/automation/state.py | 24 +++- homeassistant/components/automation/time.py | 56 +++++++- tests/components/automation/test_state.py | 30 +++- tests/components/automation/test_time.py | 135 +++++++++++++++++- 8 files changed, 275 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8dcb158dea4..9273ed49054 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,7 +9,7 @@ import logging from homeassistant.bootstrap import prepare_setup_platform from homeassistant.helpers import config_per_platform from homeassistant.util import split_entity_id -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM DOMAIN = "automation" @@ -19,6 +19,7 @@ CONF_ALIAS = "alias" CONF_SERVICE = "execute_service" CONF_SERVICE_ENTITY_ID = "service_entity_id" CONF_SERVICE_DATA = "service_data" +CONF_IF = "if" _LOGGER = logging.getLogger(__name__) @@ -34,7 +35,12 @@ def setup(hass, config): _LOGGER.error("Unknown automation platform specified: %s", p_type) continue - if platform.register(hass, p_config, _get_action(hass, p_config)): + action = _get_action(hass, p_config) + + if CONF_IF in p_config: + action = _process_if(hass, config, p_config[CONF_IF], action) + + if platform.trigger(hass, p_config, action): _LOGGER.info( "Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, "")) success = True @@ -72,3 +78,28 @@ def _get_action(hass, config): hass.services.call(domain, service, service_data) return action + + +def _process_if(hass, config, if_configs, action): + """ Processes if checks. """ + + if isinstance(if_configs, dict): + if_configs = [if_configs] + + for if_config in if_configs: + p_type = if_config.get(CONF_PLATFORM) + if p_type is None: + _LOGGER.error("No platform defined found for if-statement %s", + if_config) + continue + + platform = prepare_setup_platform(hass, config, DOMAIN, p_type) + + if platform is None or not hasattr(platform, 'if_action'): + _LOGGER.error("Unsupported if-statement platform specified: %s", + p_type) + continue + + action = platform.if_action(hass, if_config, action) + + return action diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 8a78f20d485..22be921f66a 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -12,7 +12,7 @@ CONF_EVENT_DATA = "event_data" _LOGGER = logging.getLogger(__name__) -def register(hass, config, action): +def trigger(hass, config, action): """ Listen for events based on config. """ event_type = config.get(CONF_EVENT_TYPE) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 6b4e6b1e039..7004b919c72 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -14,7 +14,7 @@ CONF_TOPIC = 'mqtt_topic' CONF_PAYLOAD = 'mqtt_payload' -def register(hass, config, action): +def trigger(hass, config, action): """ Listen for state changes based on `config`. """ topic = config.get(CONF_TOPIC) payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 79312bc9e58..127f3cc99a1 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -16,7 +16,7 @@ CONF_ABOVE = "state_above" _LOGGER = logging.getLogger(__name__) -def register(hass, config, action): +def trigger(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index ba96debf9ac..d336fcaa3d7 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -13,15 +13,16 @@ from homeassistant.const import MATCH_ALL CONF_ENTITY_ID = "state_entity_id" CONF_FROM = "state_from" CONF_TO = "state_to" +CONF_STATE = "state" -def register(hass, config, action): +def trigger(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) if entity_id is None: logging.getLogger(__name__).error( - "Missing configuration key %s", CONF_ENTITY_ID) + "Missing trigger configuration key %s", CONF_ENTITY_ID) return False from_state = config.get(CONF_FROM, MATCH_ALL) @@ -35,3 +36,22 @@ def register(hass, config, action): hass, entity_id, state_automation_listener, from_state, to_state) return True + + +def if_action(hass, config, action): + """ Wraps action method with state based condition. """ + entity_id = config.get(CONF_ENTITY_ID) + state = config.get(CONF_STATE) + + if entity_id is None or state is None: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s or %s", CONF_ENTITY_ID, + CONF_STATE) + return action + + def state_if(): + """ Execute action if state matches. """ + if hass.states.is_state(entity_id, state): + action() + + return state_if diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 77bd40a7a41..b97f3e2f7f5 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -4,15 +4,23 @@ homeassistant.components.automation.time Offers time listening automation rules. """ +import logging + from homeassistant.util import convert +import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_time_change CONF_HOURS = "time_hours" CONF_MINUTES = "time_minutes" CONF_SECONDS = "time_seconds" +CONF_BEFORE = "before" +CONF_AFTER = "after" +CONF_WEEKDAY = "weekday" + +WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] -def register(hass, config, action): +def trigger(hass, config, action): """ Listen for state changes based on `config`. """ hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) @@ -26,3 +34,49 @@ def register(hass, config, action): hour=hours, minute=minutes, second=seconds) return True + + +def if_action(hass, config, action): + """ Wraps 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: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s, %s or %s", + CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY) + + 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] + + before_point = now.replace(hour=int(before_h), + minute=int(before_m)) + + if now > before_point: + return + + if after is not None: + # Strip seconds if given + after_h, after_m = after.split(':')[0:2] + + after_point = now.replace(hour=int(after_h), minute=int(after_m)) + + if now < after_point: + return + + 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 + + action() + + return time_if diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 47d612cbb02..9dcfa49d54c 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -8,7 +8,7 @@ import unittest import homeassistant.core as ha import homeassistant.components.automation as automation -import homeassistant.components.automation.state as state +from homeassistant.components.automation import event, state from homeassistant.const import CONF_PLATFORM @@ -137,3 +137,31 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) + + def test_if_action(self): + entity_id = 'domain.test_entity' + test_state = 'new_state' + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_IF: [{ + CONF_PLATFORM: 'state', + state.CONF_ENTITY_ID: entity_id, + state.CONF_STATE: test_state, + }] + } + }) + + self.hass.states.set(entity_id, test_state) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + self.hass.states.set(entity_id, test_state + 'something') + 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_time.py b/tests/components/automation/test_time.py index 05c5ade1d53..48544bca25a 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -4,13 +4,14 @@ tests.test_component_demo Tests demo component. """ +from datetime import timedelta import unittest +from unittest.mock import patch import homeassistant.core as ha -import homeassistant.loader as loader import homeassistant.util.dt as dt_util import homeassistant.components.automation as automation -import homeassistant.components.automation.time as time +from homeassistant.components.automation import time, event from homeassistant.const import CONF_PLATFORM from tests.common import fire_time_changed @@ -94,3 +95,133 @@ class TestAutomationTime(unittest.TestCase): self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_action_before(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_IF: { + CONF_PLATFORM: 'time', + time.CONF_BEFORE: '10:00' + } + } + }) + + before_10 = dt_util.now().replace(hour=8) + after_10 = dt_util.now().replace(hour=14) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=before_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=after_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_after(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_IF: { + CONF_PLATFORM: 'time', + time.CONF_AFTER: '10:00' + } + } + }) + + before_10 = dt_util.now().replace(hour=8) + after_10 = dt_util.now().replace(hour=14) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=before_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(0, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=after_10): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_one_weekday(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_IF: { + CONF_PLATFORM: 'time', + time.CONF_WEEKDAY: 'mon', + } + } + }) + + days_past_monday = dt_util.now().weekday() + monday = dt_util.now() - timedelta(days=days_past_monday) + tuesday = monday + timedelta(days=1) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=monday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=tuesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + def test_if_action_list_weekday(self): + automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'event', + event.CONF_EVENT_TYPE: 'test_event', + automation.CONF_SERVICE: 'test.automation', + automation.CONF_IF: { + CONF_PLATFORM: 'time', + time.CONF_WEEKDAY: ['mon', 'tue'], + } + } + }) + + days_past_monday = dt_util.now().weekday() + monday = dt_util.now() - timedelta(days=days_past_monday) + tuesday = monday + timedelta(days=1) + wednesday = tuesday + timedelta(days=1) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=monday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(1, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=tuesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(2, len(self.calls)) + + with patch('homeassistant.components.automation.time.dt_util.now', + return_value=wednesday): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + self.assertEqual(2, len(self.calls))