Merge pull request #222 from balloob/change-dependency-sun

Sun component: ephem->astral
This commit is contained in:
Paulus Schoutsen 2015-07-16 21:35:06 -07:00
commit f4562fa352
4 changed files with 82 additions and 58 deletions

View File

@ -22,21 +22,18 @@ The sun event need to have the type 'sun', which service to call, which event
import logging import logging
from datetime import timedelta from datetime import timedelta
try: import homeassistant.util as util
import ephem
except ImportError:
# Will be fixed during setup
ephem = None
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.scheduler import ServiceEventListener from homeassistant.components.scheduler import ServiceEventListener
DEPENDENCIES = [] DEPENDENCIES = []
REQUIREMENTS = ['pyephem>=3.7'] REQUIREMENTS = ['astral>=0.8.1']
DOMAIN = "sun" DOMAIN = "sun"
ENTITY_ID = "sun.sun" ENTITY_ID = "sun.sun"
CONF_ELEVATION = 'elevation'
STATE_ABOVE_HORIZON = "above_horizon" STATE_ABOVE_HORIZON = "above_horizon"
STATE_BELOW_HORIZON = "below_horizon" STATE_BELOW_HORIZON = "below_horizon"
@ -99,24 +96,43 @@ def next_rising_utc(hass, entity_id=None):
def setup(hass, config): def setup(hass, config):
""" Tracks the state of the sun. """ """ Tracks the state of the sun. """
logger = logging.getLogger(__name__)
global ephem # pylint: disable=invalid-name
if ephem is None:
import ephem as ephem_
ephem = ephem_
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
logger.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
try: latitude = util.convert(hass.config.latitude, float)
sun = Sun(hass, str(hass.config.latitude), str(hass.config.longitude)) longitude = util.convert(hass.config.longitude, float)
except ValueError: errors = []
# Raised when invalid latitude or longitude is given to Observer
logger.exception("Invalid value for latitude or longitude") if latitude is None:
errors.append('Latitude needs to be a decimal value')
elif -90 > latitude < 90:
errors.append('Latitude needs to be -90 .. 90')
if longitude is None:
errors.append('Longitude needs to be a decimal value')
elif -180 > longitude < 180:
errors.append('Longitude needs to be -180 .. 180')
if errors:
_LOGGER.error('Invalid configuration received: %s', ", ".join(errors))
return False return False
platform_config = config.get(DOMAIN, {})
elevation = platform_config.get(CONF_ELEVATION)
from astral import Location, GoogleGeocoder
location = Location(('', '', latitude, longitude, hass.config.time_zone,
elevation or 0))
if elevation is None:
google = GoogleGeocoder()
google._get_elevation(location) # pylint: disable=protected-access
_LOGGER.info('Retrieved elevation from Google: %s', location.elevation)
sun = Sun(hass, location)
sun.point_in_time_listener(dt_util.utcnow()) sun.point_in_time_listener(dt_util.utcnow())
return True return True
@ -127,14 +143,9 @@ class Sun(Entity):
entity_id = ENTITY_ID entity_id = ENTITY_ID
def __init__(self, hass, latitude, longitude): def __init__(self, hass, location):
self.hass = hass self.hass = hass
self.observer = ephem.Observer() self.location = location
# pylint: disable=assigning-non-slot
self.observer.lat = latitude
# pylint: disable=assigning-non-slot
self.observer.long = longitude
self._state = self.next_rising = self.next_setting = None self._state = self.next_rising = self.next_setting = None
@property @property
@ -167,17 +178,24 @@ class Sun(Entity):
def update_as_of(self, utc_point_in_time): def update_as_of(self, utc_point_in_time):
""" Calculate sun state at a point in UTC time. """ """ Calculate sun state at a point in UTC time. """
sun = ephem.Sun() # pylint: disable=no-member mod = -1
while True:
next_rising_dt = self.location.sunrise(
utc_point_in_time + timedelta(days=mod), local=False)
if next_rising_dt > utc_point_in_time:
break
mod += 1
# pylint: disable=assigning-non-slot mod = -1
self.observer.date = ephem.date(utc_point_in_time) while True:
next_setting_dt = (self.location.sunset(
utc_point_in_time + timedelta(days=mod), local=False))
if next_setting_dt > utc_point_in_time:
break
mod += 1
self.next_rising = self.observer.next_rising( self.next_rising = next_rising_dt
sun, self.next_setting = next_setting_dt
start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC)
self.next_setting = self.observer.next_setting(
sun,
start=utc_point_in_time).datetime().replace(tzinfo=dt_util.UTC)
def point_in_time_listener(self, now): def point_in_time_listener(self, now):
""" Called when the state of the sun has changed. """ """ Called when the state of the sun has changed. """

View File

@ -9,7 +9,7 @@ pytz>=2015.2
zeroconf>=0.16.0 zeroconf>=0.16.0
# Sun (sun) # Sun (sun)
pyephem>=3.7 astral>=0.8.1
# Philips Hue library (lights.hue) # Philips Hue library (lights.hue)
phue>=0.8 phue>=0.8

View File

@ -67,7 +67,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
light.DOMAIN: {CONF_PLATFORM: 'test'} light.DOMAIN: {CONF_PLATFORM: 'test'}
}) })
sun.setup(self.hass, {}) sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """

View File

@ -8,7 +8,7 @@ Tests Sun component.
import unittest import unittest
from datetime import timedelta from datetime import timedelta
import ephem from astral import Astral
import homeassistant as ha import homeassistant as ha
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -34,29 +34,35 @@ class TestSun(unittest.TestCase):
def test_setting_rising(self): def test_setting_rising(self):
""" Test retrieving sun setting and rising. """ """ Test retrieving sun setting and rising. """
latitude = 32.87336
longitude = 117.22743
# Compare it with the real data # Compare it with the real data
self.hass.config.latitude = '32.87336' self.hass.config.latitude = latitude
self.hass.config.longitude = '117.22743' self.hass.config.longitude = longitude
sun.setup(self.hass, None) sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}})
observer = ephem.Observer()
observer.lat = '32.87336' # pylint: disable=assigning-non-slot
observer.long = '117.22743' # pylint: disable=assigning-non-slot
astral = Astral()
utc_now = dt_util.utcnow() utc_now = dt_util.utcnow()
body_sun = ephem.Sun() # pylint: disable=no-member
next_rising_dt = observer.next_rising(
body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC)
next_setting_dt = observer.next_setting(
body_sun, start=utc_now).datetime().replace(tzinfo=dt_util.UTC)
# Home Assistant strips out microseconds mod = -1
# strip it out of the datetime objects while True:
next_rising_dt = dt_util.strip_microseconds(next_rising_dt) next_rising = (astral.sunrise_utc(utc_now +
next_setting_dt = dt_util.strip_microseconds(next_setting_dt) timedelta(days=mod), latitude, longitude))
if next_rising > utc_now:
break
mod += 1
self.assertEqual(next_rising_dt, sun.next_rising_utc(self.hass)) mod = -1
self.assertEqual(next_setting_dt, sun.next_setting_utc(self.hass)) while True:
next_setting = (astral.sunset_utc(utc_now +
timedelta(days=mod), latitude, longitude))
if next_setting > utc_now:
break
mod += 1
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 # Point it at a state without the proper attributes
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON)
@ -71,7 +77,7 @@ class TestSun(unittest.TestCase):
""" Test if the state changes at next setting/rising. """ """ Test if the state changes at next setting/rising. """
self.hass.config.latitude = '32.87336' self.hass.config.latitude = '32.87336'
self.hass.config.longitude = '117.22743' self.hass.config.longitude = '117.22743'
sun.setup(self.hass, None) sun.setup(self.hass, {})
if sun.is_on(self.hass): if sun.is_on(self.hass):
test_state = sun.STATE_BELOW_HORIZON test_state = sun.STATE_BELOW_HORIZON