diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py new file mode 100644 index 00000000000..103df6c9b39 --- /dev/null +++ b/homeassistant/components/automation/sun.py @@ -0,0 +1,103 @@ +""" +homeassistant.components.automation.sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Offers sun based automation rules. +""" +import logging +from datetime import timedelta + +from homeassistant.components import sun +from homeassistant.helpers.event import track_point_in_utc_time +import homeassistant.util.dt as dt_util + +DEPENDENCIES = ['sun'] + +CONF_OFFSET = 'offset' +CONF_EVENT = 'event' + +EVENT_SUNSET = 'sunset' +EVENT_SUNRISE = 'sunrise' + +_LOGGER = logging.getLogger(__name__) + + +def trigger(hass, config, action): + """ Listen for events based on config. """ + event = config.get(CONF_EVENT) + + if event is None: + _LOGGER.error("Missing configuration key %s", CONF_EVENT) + return False + + event = event.lower() + if event not in (EVENT_SUNRISE, EVENT_SUNSET): + _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) + return False + + if CONF_OFFSET in config: + raw_offset = config.get(CONF_OFFSET) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + else: + offset = timedelta(0) + + # Do something to call action + if event == EVENT_SUNRISE: + trigger_sunrise(hass, action, offset) + else: + trigger_sunset(hass, action, offset) + + return True + + +def trigger_sunrise(hass, action, offset): + """ Trigger action at next sun rise. """ + def next_rise(): + """ Returns next sunrise. """ + next_time = sun.next_rising_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunrise_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + action() + + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + + +def trigger_sunset(hass, action, offset): + """ Trigger action at next sun set. """ + def next_set(): + """ Returns next sunrise. """ + next_time = sun.next_setting_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunset_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + action() + + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py new file mode 100644 index 00000000000..c2b292ea0e5 --- /dev/null +++ b/tests/components/automation/test_sun.py @@ -0,0 +1,128 @@ +""" +tests.components.automation.test_sun +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests sun automation. +""" +from datetime import datetime +import unittest + +import homeassistant.core as ha +from homeassistant.components import sun +import homeassistant.components.automation as automation +import homeassistant.util.dt as dt_util + +from tests.common import fire_time_changed + + +class TestAutomationSun(unittest.TestCase): + """ Test the sun automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.config.components.append('sun') + + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_sunset_trigger(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunrise_trigger(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunset_trigger_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '02:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + 'offset': '0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_sunrise_trigger_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'sun', + 'event': 'sunset', + 'offset': '-0:30:00' + }, + 'action': { + 'execute_service': 'test.automation', + } + } + })) + + fire_time_changed(self.hass, trigger_time) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls))