diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 19f0b0fabba..f5fb0115a43 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -30,6 +30,10 @@ STATE_BELOW_HORIZON = 'below_horizon' STATE_ATTR_AZIMUTH = 'azimuth' STATE_ATTR_ELEVATION = 'elevation' +STATE_ATTR_NEXT_DAWN = 'next_dawn' +STATE_ATTR_NEXT_DUSK = 'next_dusk' +STATE_ATTR_NEXT_MIDNIGHT = 'next_midnight' +STATE_ATTR_NEXT_NOON = 'next_noon' STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' @@ -47,6 +51,118 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) +def next_dawn(hass, entity_id=None): + """Local datetime object of the next dawn. + + Async friendly. + """ + utc_next = next_dawn_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_dawn_utc(hass, entity_id=None): + """UTC datetime object of the next dawn. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_DAWN]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_DAWN does not exist + return None + + +def next_dusk(hass, entity_id=None): + """Local datetime object of the next dusk. + + Async friendly. + """ + utc_next = next_dusk_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_dusk_utc(hass, entity_id=None): + """UTC datetime object of the next dusk. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_DUSK]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_DUSK does not exist + return None + + +def next_midnight(hass, entity_id=None): + """Local datetime object of the next midnight. + + Async friendly. + """ + utc_next = next_midnight_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_midnight_utc(hass, entity_id=None): + """UTC datetime object of the next midnight. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_MIDNIGHT]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_MIDNIGHT does not exist + return None + + +def next_noon(hass, entity_id=None): + """Local datetime object of the next solar noon. + + Async friendly. + """ + utc_next = next_noon_utc(hass, entity_id) + + return dt_util.as_local(utc_next) if utc_next else None + + +def next_noon_utc(hass, entity_id=None): + """UTC datetime object of the next noon. + + Async friendly. + """ + entity_id = entity_id or ENTITY_ID + + state = hass.states.get(ENTITY_ID) + + try: + return dt_util.parse_datetime( + state.attributes[STATE_ATTR_NEXT_NOON]) + except (AttributeError, KeyError): + # AttributeError if state is None + # KeyError if STATE_ATTR_NEXT_NOON does not exist + return None + + def next_setting(hass, entity_id=None): """Local datetime object of the next sun setting. @@ -153,6 +269,8 @@ class Sun(Entity): self.hass = hass self.location = location self._state = self.next_rising = self.next_setting = None + self.next_dawn = self.next_dusk = None + self.next_midnight = self.next_noon = None self.solar_elevation = self.solar_azimuth = 0 track_utc_time_change(hass, self.timer_update, second=30) @@ -174,6 +292,10 @@ class Sun(Entity): def state_attributes(self): """Return the state attributes of the sun.""" return { + STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), + STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), + STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), + STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), STATE_ATTR_ELEVATION: round(self.solar_elevation, 2), @@ -183,36 +305,41 @@ class Sun(Entity): @property def next_change(self): """Datetime when the next change to the state is.""" - return min(self.next_rising, self.next_setting) + return min(self.next_dawn, self.next_dusk, self.next_midnight, + self.next_noon, self.next_rising, self.next_setting) - def update_as_of(self, utc_point_in_time): + @staticmethod + def get_next_solar_event(callable_on_astral_location, + utc_point_in_time, mod, increment): """Calculate sun state at a point in UTC time.""" import astral - mod = -1 while True: try: - next_rising_dt = self.location.sunrise( + next_dt = callable_on_astral_location( utc_point_in_time + timedelta(days=mod), local=False) - if next_rising_dt > utc_point_in_time: + if next_dt > utc_point_in_time: break except astral.AstralError: pass - mod += 1 + mod += increment - mod = -1 - while True: - try: - next_setting_dt = (self.location.sunset( - utc_point_in_time + timedelta(days=mod), local=False)) - if next_setting_dt > utc_point_in_time: - break - except astral.AstralError: - pass - mod += 1 + return next_dt - self.next_rising = next_rising_dt - self.next_setting = next_setting_dt + def update_as_of(self, utc_point_in_time): + """Update the attributes containing solar events.""" + self.next_dawn = Sun.get_next_solar_event( + self.location.dawn, utc_point_in_time, -1, 1) + self.next_dusk = Sun.get_next_solar_event( + self.location.dusk, utc_point_in_time, -1, 1) + self.next_midnight = Sun.get_next_solar_event( + self.location.solar_midnight, utc_point_in_time, -1, 1) + self.next_noon = Sun.get_next_solar_event( + self.location.solar_noon, utc_point_in_time, -1, 1) + self.next_rising = Sun.get_next_solar_event( + self.location.sunrise, utc_point_in_time, -1, 1) + self.next_setting = Sun.get_next_solar_event( + self.location.sunset, utc_point_in_time, -1, 1) def update_sun_position(self, utc_point_in_time): """Calculate the position of the sun.""" diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 33fc5ad40c5..659e4b1a43d 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -44,6 +44,38 @@ class TestSun(unittest.TestCase): latitude = self.hass.config.latitude longitude = self.hass.config.longitude + mod = -1 + while True: + next_dawn = (astral.dawn_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_dawn > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_dusk = (astral.dusk_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_dusk > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_midnight = (astral.solar_midnight_utc(utc_now + + timedelta(days=mod), longitude)) + if next_midnight > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_noon = (astral.solar_noon_utc(utc_now + + timedelta(days=mod), longitude)) + if next_noon > utc_now: + break + mod += 1 + mod = -1 while True: next_rising = (astral.sunrise_utc(utc_now + @@ -60,15 +92,27 @@ class TestSun(unittest.TestCase): break mod += 1 + self.assertEqual(next_dawn, sun.next_dawn_utc(self.hass)) + self.assertEqual(next_dusk, sun.next_dusk_utc(self.hass)) + self.assertEqual(next_midnight, sun.next_midnight_utc(self.hass)) + self.assertEqual(next_noon, sun.next_noon_utc(self.hass)) self.assertEqual(next_rising, sun.next_rising_utc(self.hass)) self.assertEqual(next_setting, sun.next_setting_utc(self.hass)) # Point it at a state without the proper attributes self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) + self.assertIsNone(sun.next_dawn(self.hass)) + self.assertIsNone(sun.next_dusk(self.hass)) + self.assertIsNone(sun.next_midnight(self.hass)) + self.assertIsNone(sun.next_noon(self.hass)) self.assertIsNone(sun.next_rising(self.hass)) self.assertIsNone(sun.next_setting(self.hass)) # Point it at a non-existing state + self.assertIsNone(sun.next_dawn(self.hass, 'non.existing')) + self.assertIsNone(sun.next_dusk(self.hass, 'non.existing')) + self.assertIsNone(sun.next_midnight(self.hass, 'non.existing')) + self.assertIsNone(sun.next_noon(self.hass, 'non.existing')) self.assertIsNone(sun.next_rising(self.hass, 'non.existing')) self.assertIsNone(sun.next_setting(self.hass, 'non.existing'))