From Dusk till Dawn (#6857)

* Added dawn, dusk, noon and midnight to the Sun component

* Created a helper method for the solar events
This commit is contained in:
Nate 2017-04-07 07:59:41 +02:00 committed by Paulus Schoutsen
parent 216c2682f0
commit 8cff98d07b
2 changed files with 189 additions and 18 deletions

View file

@ -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."""

View file

@ -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'))