diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index b1d1d755348..fd2cfa46b72 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -22,21 +22,18 @@ The sun event need to have the type 'sun', which service to call, which event import logging from datetime import timedelta -try: - import ephem -except ImportError: - # Will be fixed during setup - ephem = None - +import homeassistant.util as util import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener DEPENDENCIES = [] -REQUIREMENTS = ['pyephem>=3.7'] +REQUIREMENTS = ['astral>=0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" +CONF_ELEVATION = 'elevation' + STATE_ABOVE_HORIZON = "above_horizon" STATE_BELOW_HORIZON = "below_horizon" @@ -99,24 +96,43 @@ def next_rising_utc(hass, entity_id=None): def setup(hass, config): """ 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): - logger.error("Latitude or longitude not set in Home Assistant config") + _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - try: - sun = Sun(hass, str(hass.config.latitude), str(hass.config.longitude)) - except ValueError: - # Raised when invalid latitude or longitude is given to Observer - logger.exception("Invalid value for latitude or longitude") + latitude = util.convert(hass.config.latitude, float) + longitude = util.convert(hass.config.longitude, float) + errors = [] + + 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 + 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()) return True @@ -127,14 +143,9 @@ class Sun(Entity): entity_id = ENTITY_ID - def __init__(self, hass, latitude, longitude): + def __init__(self, hass, location): self.hass = hass - self.observer = ephem.Observer() - # pylint: disable=assigning-non-slot - self.observer.lat = latitude - # pylint: disable=assigning-non-slot - self.observer.long = longitude - + self.location = location self._state = self.next_rising = self.next_setting = None @property @@ -167,17 +178,24 @@ class Sun(Entity): def update_as_of(self, utc_point_in_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 - self.observer.date = ephem.date(utc_point_in_time) + mod = -1 + 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( - sun, - 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) + self.next_rising = next_rising_dt + self.next_setting = next_setting_dt def point_in_time_listener(self, now): """ Called when the state of the sun has changed. """ diff --git a/requirements.txt b/requirements.txt index 83780721c2c..c7a569fad33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pytz>=2015.2 zeroconf>=0.16.0 # Sun (sun) -pyephem>=3.7 +astral>=0.8.1 # Philips Hue library (lights.hue) phue>=0.8 diff --git a/tests/test_component_device_sun_light_trigger.py b/tests/test_component_device_sun_light_trigger.py index 7a05f63099f..05452a830ec 100644 --- a/tests/test_component_device_sun_light_trigger.py +++ b/tests/test_component_device_sun_light_trigger.py @@ -67,7 +67,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): 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 """ Stop down stuff we started. """ diff --git a/tests/test_component_sun.py b/tests/test_component_sun.py index aec97ede6a8..705caadcd3a 100644 --- a/tests/test_component_sun.py +++ b/tests/test_component_sun.py @@ -8,7 +8,7 @@ Tests Sun component. import unittest from datetime import timedelta -import ephem +from astral import Astral import homeassistant as ha import homeassistant.util.dt as dt_util @@ -34,29 +34,35 @@ class TestSun(unittest.TestCase): def test_setting_rising(self): """ Test retrieving sun setting and rising. """ + latitude = 32.87336 + longitude = 117.22743 + # Compare it with the real data - self.hass.config.latitude = '32.87336' - self.hass.config.longitude = '117.22743' - sun.setup(self.hass, None) - - observer = ephem.Observer() - observer.lat = '32.87336' # pylint: disable=assigning-non-slot - observer.long = '117.22743' # pylint: disable=assigning-non-slot + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + astral = Astral() 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 - # strip it out of the datetime objects - next_rising_dt = dt_util.strip_microseconds(next_rising_dt) - next_setting_dt = dt_util.strip_microseconds(next_setting_dt) + mod = -1 + while True: + next_rising = (astral.sunrise_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 - self.assertEqual(next_rising_dt, sun.next_rising_utc(self.hass)) - self.assertEqual(next_setting_dt, sun.next_setting_utc(self.hass)) + mod = -1 + 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 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. """ self.hass.config.latitude = '32.87336' self.hass.config.longitude = '117.22743' - sun.setup(self.hass, None) + sun.setup(self.hass, {}) if sun.is_on(self.hass): test_state = sun.STATE_BELOW_HORIZON