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:
parent
216c2682f0
commit
8cff98d07b
2 changed files with 189 additions and 18 deletions
|
@ -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."""
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue