Skip to content

Commit

Permalink
From Dusk till Dawn (home-assistant#6857)
Browse files Browse the repository at this point in the history
* Added dawn, dusk, noon and midnight to the Sun component

* Created a helper method for the solar events
  • Loading branch information
BillyNate authored and balloob committed Apr 7, 2017
1 parent 216c268 commit 8cff98d
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 18 deletions.
163 changes: 145 additions & 18 deletions homeassistant/components/sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@

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'

Expand All @@ -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.
Expand Down Expand Up @@ -153,6 +269,8 @@ def __init__(self, hass, location):
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)
Expand All @@ -174,6 +292,10 @@ def state(self):
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),
Expand All @@ -183,36 +305,41 @@ def state_attributes(self):
@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."""
Expand Down
44 changes: 44 additions & 0 deletions tests/components/test_sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,38 @@ def test_setting_rising(self):
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 +
Expand All @@ -60,15 +92,27 @@ def test_setting_rising(self):
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'))

Expand Down

0 comments on commit 8cff98d

Please sign in to comment.