From 35f0270688d2bd1e31e45ebbc1771f1ff940bff6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Jul 2015 19:49:54 -0700 Subject: [PATCH 1/2] Sun component: ephem->astral --- homeassistant/components/sun.py | 78 ++++++++++++++++++--------------- requirements.txt | 2 +- tests/test_component_sun.py | 42 ++++++++++-------- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index b1d1d755348..a3286ebc9ee 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -22,18 +22,13 @@ 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" @@ -99,24 +94,29 @@ 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 + sun = Sun(hass, latitude, longitude) sun.point_in_time_listener(dt_util.utcnow()) return True @@ -128,13 +128,12 @@ class Sun(Entity): entity_id = ENTITY_ID def __init__(self, hass, latitude, longitude): - 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 + from astral import Astral + self.hass = hass + self.latitude = latitude + self.longitude = longitude + self.astral = Astral() self._state = self.next_rising = self.next_setting = None @property @@ -167,17 +166,26 @@ 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.astral.sunrise_utc( + utc_point_in_time + + timedelta(days=mod), self.latitude, self.longitude) + 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.astral.sunset_utc( + utc_point_in_time + + timedelta(days=mod), self.latitude, self.longitude)) + 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_sun.py b/tests/test_component_sun.py index aec97ede6a8..d102622be8c 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' + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude 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 - + 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) From 1fda362ca392ebb205208cd84ddbd387b37c5be0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Jul 2015 21:12:18 -0700 Subject: [PATCH 2/2] Take elevation into consideration --- homeassistant/components/sun.py | 36 ++++++++++++------- ...test_component_device_sun_light_trigger.py | 2 +- tests/test_component_sun.py | 4 +-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index a3286ebc9ee..fd2cfa46b72 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -32,6 +32,8 @@ REQUIREMENTS = ['astral>=0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" +CONF_ELEVATION = 'elevation' + STATE_ABOVE_HORIZON = "above_horizon" STATE_BELOW_HORIZON = "below_horizon" @@ -116,7 +118,21 @@ def setup(hass, config): _LOGGER.error('Invalid configuration received: %s', ", ".join(errors)) return False - sun = Sun(hass, latitude, longitude) + 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,13 +143,9 @@ class Sun(Entity): entity_id = ENTITY_ID - def __init__(self, hass, latitude, longitude): - from astral import Astral - + def __init__(self, hass, location): self.hass = hass - self.latitude = latitude - self.longitude = longitude - self.astral = Astral() + self.location = location self._state = self.next_rising = self.next_setting = None @property @@ -168,18 +180,16 @@ class Sun(Entity): """ Calculate sun state at a point in UTC time. """ mod = -1 while True: - next_rising_dt = self.astral.sunrise_utc( - utc_point_in_time + - timedelta(days=mod), self.latitude, self.longitude) + 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 mod = -1 while True: - next_setting_dt = (self.astral.sunset_utc( - utc_point_in_time + - timedelta(days=mod), self.latitude, self.longitude)) + 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 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 d102622be8c..705caadcd3a 100644 --- a/tests/test_component_sun.py +++ b/tests/test_component_sun.py @@ -40,7 +40,7 @@ class TestSun(unittest.TestCase): # Compare it with the real data self.hass.config.latitude = latitude self.hass.config.longitude = longitude - sun.setup(self.hass, None) + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) astral = Astral() utc_now = dt_util.utcnow() @@ -77,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