diff --git a/homeassistant/components/binary_sensor/tod.py b/homeassistant/components/binary_sensor/tod.py new file mode 100644 index 00000000000..7dc6e5ebe81 --- /dev/null +++ b/homeassistant/components/binary_sensor/tod.py @@ -0,0 +1,217 @@ +"""Support for representing current time of the day as binary sensors.""" +from datetime import datetime, timedelta +import logging + +import pytz +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import ( + CONF_AFTER, CONF_BEFORE, CONF_NAME, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_AFTER = 'after' +ATTR_BEFORE = 'before' +ATTR_NEXT_UPDATE = 'next_update' + +CONF_AFTER_OFFSET = 'after_offset' +CONF_BEFORE_OFFSET = 'before_offset' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_AFTER): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_BEFORE): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period, + vol.Optional(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period, +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the ToD sensors.""" + if hass.config.time_zone is None: + _LOGGER.error("Timezone is not set in Home Assistant configuration") + return + + after = config[CONF_AFTER] + after_offset = config[CONF_AFTER_OFFSET] + before = config[CONF_BEFORE] + before_offset = config[CONF_BEFORE_OFFSET] + name = config[CONF_NAME] + sensor = TodSensor(name, after, after_offset, before, before_offset) + + async_add_entities([sensor]) + + +def is_sun_event(event): + """Return true if event is sun event not time.""" + return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + + +class TodSensor(BinarySensorDevice): + """Time of the Day Sensor.""" + + def __init__(self, name, after, after_offset, before, before_offset): + """Init the ToD Sensor...""" + self._name = name + self._time_before = self._time_after = self._next_update = None + self._after_offset = after_offset + self._before_offset = before_offset + self._before = before + self._after = after + + @property + def should_poll(self): + """Sensor does not need to be polled.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def after(self): + """Return the timestamp for the begining of the period.""" + return self._time_after + + @property + def before(self): + """Return the timestamp for the end of the period.""" + return self._time_before + + @property + def is_on(self): + """Return True is sensor is on.""" + if self.after < self.before: + return self.after <= self.current_datetime < self.before + return False + + @property + def current_datetime(self): + """Return local current datetime according to hass configuration.""" + return dt_util.utcnow() + + @property + def next_update(self): + """Return the next update point in the UTC time.""" + return self._next_update + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_AFTER: self.after.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_BEFORE: self.before.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_NEXT_UPDATE: self.next_update.astimezone( + self.hass.config.time_zone).isoformat(), + } + + def _calculate_initial_boudary_time(self): + """Calculate internal absolute time boudaries.""" + nowutc = self.current_datetime + # If after value is a sun event instead of absolute time + if is_sun_event(self._after): + # Calculate the today's event utc time or + # if not available take next + after_event_date = \ + get_astral_event_date(self.hass, self._after, nowutc) or \ + get_astral_event_next(self.hass, self._after, nowutc) + else: + # Convert local time provided to UTC today + # datetime.combine(date, time, tzinfo) is not supported + # in python 3.5. The self._after is provided + # with hass configured TZ not system wide + after_event_date = datetime.combine( + nowutc, self._after.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + self._time_after = after_event_date + + # If before value is a sun event instead of absolute time + if is_sun_event(self._before): + # Calculate the today's event utc time or if not available take + # next + before_event_date = \ + get_astral_event_date(self.hass, self._before, nowutc) or \ + get_astral_event_next(self.hass, self._before, nowutc) + # Before is earlier than after + if before_event_date < after_event_date: + # Take next day for before + before_event_date = get_astral_event_next( + self.hass, self._before, after_event_date) + else: + # Convert local time provided to UTC today, see above + before_event_date = datetime.combine( + nowutc, self._before.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + # It is safe to add timedelta days=1 to UTC as there is no DST + if before_event_date < after_event_date + self._after_offset: + before_event_date += timedelta(days=1) + + self._time_before = before_event_date + + # Add offset to utc boundaries according to the configuration + self._time_after += self._after_offset + self._time_before += self._before_offset + + def _turn_to_next_day(self): + """Turn to to the next day.""" + if is_sun_event(self._after): + self._time_after = get_astral_event_next( + self.hass, self._after, + self._time_after - self._after_offset) + self._time_after += self._after_offset + else: + # Offset is already there + self._time_after += timedelta(days=1) + + if is_sun_event(self._before): + self._time_before = get_astral_event_next( + self.hass, self._before, + self._time_before - self._before_offset) + self._time_before += self._before_offset + else: + # Offset is already there + self._time_before += timedelta(days=1) + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._calculate_initial_boudary_time() + self._calculate_next_update() + self._point_in_time_listener(dt_util.now()) + + def _calculate_next_update(self): + """Datetime when the next update to the state.""" + now = self.current_datetime + if now < self.after: + self._next_update = self.after + return + if now < self.before: + self._next_update = self.before + return + self._turn_to_next_day() + self._next_update = self.after + + @callback + def _point_in_time_listener(self, now): + """Run when the state of the sensor should be updated.""" + self._calculate_next_update() + self.async_schedule_update_ha_state() + + async_track_point_in_utc_time( + self.hass, self._point_in_time_listener, self.next_update) diff --git a/tests/components/binary_sensor/test_tod.py b/tests/components/binary_sensor/test_tod.py new file mode 100644 index 00000000000..3c083141962 --- /dev/null +++ b/tests/components/binary_sensor/test_tod.py @@ -0,0 +1,839 @@ +"""Test Times of the Day Binary Sensor.""" +import unittest +from unittest.mock import patch +from datetime import timedelta, datetime +import pytz + +from homeassistant import setup +import homeassistant.core as ha +from homeassistant.const import STATE_OFF, STATE_ON +import homeassistant.util.dt as dt_util +from homeassistant.setup import setup_component +from tests.common import ( + get_test_home_assistant, assert_setup_component) +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) + + +class TestBinarySensorTod(unittest.TestCase): + """Test for Binary sensor tod platform.""" + + hass = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.latitute = 50.27583 + self.hass.config.longitude = 18.98583 + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup(self): + """Test the setup.""" + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Early Morning', + 'after': 'sunrise', + 'after_offset': '-02:00', + 'before': '7:00', + 'before_offset': '1:00' + }, + { + 'platform': 'tod', + 'name': 'Morning', + 'after': 'sunrise', + 'before': '12:00' + } + ], + } + with assert_setup_component(2): + assert setup.setup_component( + self.hass, 'binary_sensor', config) + + def test_setup_no_sensors(self): + """Test setup with no sensors.""" + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'tod' + } + }) + + def test_in_period_on_start(self): + """Test simple setting.""" + test_time = datetime( + 2019, 1, 10, 18, 43, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'before': '22:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.evening') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 22, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_after_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 21, 00, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + self.hass.block_till_done() + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time + timedelta(hours=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: test_time + timedelta(hours=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_outside_period(self): + """Test midnight turnover setting before midnight outside period.""" + test_time = datetime( + 2019, 1, 10, 20, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_midnight_turnover_after_midnight_outside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 20, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + switchover_time = datetime( + 2019, 1, 11, 4, 59, 0, tzinfo=self.hass.config.time_zone) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time}) + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time + timedelta( + minutes=1, seconds=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time + timedelta( + minutes=1, seconds=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_from_sunrise_to_sunset(self): + """Test period from sunrise to sunset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_from_sunset_to_sunrise(self): + """Test period from sunset to sunrise.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', test_time)) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', sunset)) + # assert sunset == sunrise + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': 'sunset', + 'before': 'sunrise' + } + ] + } + entity_id = 'binary_sensor.night' + testtime = sunset + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunset + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.hass.block_till_done() + # assert state == "dupa" + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset(self): + """Test offset.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + before = datetime( + 2019, 1, 10, 22, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=45) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '1:45' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = before + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset_overnight(self): + """Test offset overnight.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '3:00' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_norwegian_case_winter(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 1, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_norwegian_case_summer(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 6, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + print(sunrise) + print(sunset) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_sun_offset(self): + """Test sun event with offset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time)) + + timedelta(hours=1, minutes=30)) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'after_offset': '-1:30', + 'before': 'sunset', + 'before_offset': '1:30' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + test_time = test_time + timedelta(days=1) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_dst(self): + """Test sun event with offset.""" + self.hass.config.time_zone = pytz.timezone('CET') + test_time = datetime( + 2019, 3, 30, 3, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': '2:30', + 'before': '2:40' + } + ] + } + # after 2019-03-30 03:00 CET the next update should ge scheduled + # at 3:30 not 2:30 local time + # Internally the + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + state.attributes['after'] == '2019-03-31T03:30:00+02:00' + state.attributes['before'] == '2019-03-31T03:40:00+02:00' + state.attributes['next_update'] == '2019-03-31T03:30:00+02:00' + assert state.state == STATE_OFF