From 3e0eb8763f94d544c6af1ffc867ff48ad9123354 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 29 Aug 2017 10:18:37 -0400 Subject: [PATCH] Support for season sensor (#8958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- homeassistant/components/sensor/season.py | 122 +++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_season.py | 183 ++++++++++++++++++++++ 5 files changed, 312 insertions(+) create mode 100644 homeassistant/components/sensor/season.py create mode 100644 tests/components/sensor/test_season.py diff --git a/homeassistant/components/sensor/season.py b/homeassistant/components/sensor/season.py new file mode 100644 index 00000000000..e02f3cac2b0 --- /dev/null +++ b/homeassistant/components/sensor/season.py @@ -0,0 +1,122 @@ +""" +Support for tracking which astronomical or meteorological season it is. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor/season/ +""" +import logging +from datetime import datetime + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_TYPE +from homeassistant.helpers.entity import Entity +import homeassistant.util as util + +REQUIREMENTS = ['ephem==3.7.6.0'] + +_LOGGER = logging.getLogger(__name__) + +NORTHERN = 'northern' +SOUTHERN = 'southern' +EQUATOR = 'equator' +STATE_SPRING = 'Spring' +STATE_SUMMER = 'Summer' +STATE_AUTUMN = 'Autumn' +STATE_WINTER = 'Winter' +TYPE_ASTRONOMICAL = 'astronomical' +TYPE_METEOROLOGICAL = 'meteorological' +VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] + +HEMISPHERE_SEASON_SWAP = {STATE_WINTER: STATE_SUMMER, + STATE_SPRING: STATE_AUTUMN, + STATE_AUTUMN: STATE_SPRING, + STATE_SUMMER: STATE_WINTER} + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Display the current season.""" + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + latitude = util.convert(hass.config.latitude, float) + _type = config.get(CONF_TYPE) + + if latitude < 0: + hemisphere = SOUTHERN + elif latitude > 0: + hemisphere = NORTHERN + else: + hemisphere = EQUATOR + + _LOGGER.debug(_type) + add_devices([Season(hass, hemisphere, _type)]) + + return True + + +def get_season(date, hemisphere, season_tracking_type): + """Calculate the current season.""" + import ephem + + if hemisphere == 'equator': + return None + + if season_tracking_type == TYPE_ASTRONOMICAL: + spring_start = ephem.next_equinox(str(date.year)).datetime() + summer_start = ephem.next_solstice(str(date.year)).datetime() + autumn_start = ephem.next_equinox(spring_start).datetime() + winter_start = ephem.next_solstice(summer_start).datetime() + else: + spring_start = datetime(2017, 3, 1).replace(year=date.year) + summer_start = spring_start.replace(month=6) + autumn_start = spring_start.replace(month=9) + winter_start = spring_start.replace(month=12) + + if spring_start <= date < summer_start: + season = STATE_SPRING + elif summer_start <= date < autumn_start: + season = STATE_SUMMER + elif autumn_start <= date < winter_start: + season = STATE_AUTUMN + elif winter_start <= date or spring_start > date: + season = STATE_WINTER + + # If user is located in the southern hemisphere swap the season + if hemisphere == NORTHERN: + return season + return HEMISPHERE_SEASON_SWAP.get(season) + + +class Season(Entity): + """Representation of the current season.""" + + def __init__(self, hass, hemisphere, season_tracking_type): + """Initialize the season.""" + self.hass = hass + self.hemisphere = hemisphere + self.datetime = datetime.now() + self.type = season_tracking_type + self.season = get_season(self.datetime, self.hemisphere, self.type) + + @property + def name(self): + """Return the name.""" + return "Season" + + @property + def state(self): + """Return the current season.""" + return self.season + + def update(self): + """Update season.""" + self.datetime = datetime.now() + self.season = get_season(self.datetime, self.hemisphere, self.type) diff --git a/requirements_all.txt b/requirements_all.txt index 35170465cd9..34c1d5f1e72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,6 +202,9 @@ enocean==0.31 # homeassistant.components.sensor.envirophat # envirophat==0.0.6 +# homeassistant.components.sensor.season +ephem==3.7.6.0 + # homeassistant.components.keyboard_remote # evdev==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f286555833e..7695f83497b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,6 +39,9 @@ apns2==0.1.1 # homeassistant.components.sensor.dsmr dsmr_parser==0.8 +# homeassistant.components.sensor.season +ephem==3.7.6.0 + # homeassistant.components.climate.honeywell evohomeclient==0.2.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ba7a49cc7c0..8a215cd2873 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -70,6 +70,7 @@ TEST_REQUIREMENTS = ( 'restrictedpython', 'pyunifi', 'prometheus_client', + 'ephem' ) IGNORE_PACKAGES = ( diff --git a/tests/components/sensor/test_season.py b/tests/components/sensor/test_season.py new file mode 100644 index 00000000000..10e147bcff9 --- /dev/null +++ b/tests/components/sensor/test_season.py @@ -0,0 +1,183 @@ +"""The tests for the Season sensor platform.""" +# pylint: disable=protected-access +import unittest +from datetime import datetime + +import homeassistant.components.sensor.season as season + +from tests.common import get_test_home_assistant + + +# pylint: disable=invalid-name +class TestSeason(unittest.TestCase): + """Test the season platform.""" + + DEVICE = None + CONFIG_ASTRONOMICAL = {'type': 'astronomical'} + CONFIG_METEOROLOGICAL = {'type': 'meteorological'} + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + self.DEVICE = device + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_season_should_be_summer_northern_astonomical(self): + """Test that season should be summer.""" + # A known day in summer + summer_day = datetime(2017, 9, 3, 0, 0) + current_season = season.get_season(summer_day, season.NORTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_SUMMER, + current_season) + + def test_season_should_be_summer_northern_meteorological(self): + """Test that season should be summer.""" + # A known day in summer + summer_day = datetime(2017, 8, 13, 0, 0) + current_season = season.get_season(summer_day, season.NORTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_SUMMER, + current_season) + + def test_season_should_be_autumn_northern_astonomical(self): + """Test that season should be autumn.""" + # A known day in autumn + autumn_day = datetime(2017, 9, 23, 0, 0) + current_season = season.get_season(autumn_day, season.NORTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_AUTUMN, + current_season) + + def test_season_should_be_autumn_northern_meteorological(self): + """Test that season should be autumn.""" + # A known day in autumn + autumn_day = datetime(2017, 9, 3, 0, 0) + current_season = season.get_season(autumn_day, season.NORTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_AUTUMN, + current_season) + + def test_season_should_be_winter_northern_astonomical(self): + """Test that season should be winter.""" + # A known day in winter + winter_day = datetime(2017, 12, 25, 0, 0) + current_season = season.get_season(winter_day, season.NORTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_WINTER, + current_season) + + def test_season_should_be_winter_northern_meteorological(self): + """Test that season should be winter.""" + # A known day in winter + winter_day = datetime(2017, 12, 3, 0, 0) + current_season = season.get_season(winter_day, season.NORTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_WINTER, + current_season) + + def test_season_should_be_spring_northern_astonomical(self): + """Test that season should be spring.""" + # A known day in spring + spring_day = datetime(2017, 4, 1, 0, 0) + current_season = season.get_season(spring_day, season.NORTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_SPRING, + current_season) + + def test_season_should_be_spring_northern_meteorological(self): + """Test that season should be spring.""" + # A known day in spring + spring_day = datetime(2017, 3, 3, 0, 0) + current_season = season.get_season(spring_day, season.NORTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_SPRING, + current_season) + + def test_season_should_be_winter_southern_astonomical(self): + """Test that season should be winter.""" + # A known day in winter + winter_day = datetime(2017, 9, 3, 0, 0) + current_season = season.get_season(winter_day, season.SOUTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_WINTER, + current_season) + + def test_season_should_be_winter_southern_meteorological(self): + """Test that season should be winter.""" + # A known day in winter + winter_day = datetime(2017, 8, 13, 0, 0) + current_season = season.get_season(winter_day, season.SOUTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_WINTER, + current_season) + + def test_season_should_be_spring_southern_astonomical(self): + """Test that season should be spring.""" + # A known day in spring + spring_day = datetime(2017, 9, 23, 0, 0) + current_season = season.get_season(spring_day, season.SOUTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_SPRING, + current_season) + + def test_season_should_be_spring_southern_meteorological(self): + """Test that season should be spring.""" + # A known day in spring + spring_day = datetime(2017, 9, 3, 0, 0) + current_season = season.get_season(spring_day, season.SOUTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_SPRING, + current_season) + + def test_season_should_be_summer_southern_astonomical(self): + """Test that season should be summer.""" + # A known day in summer + summer_day = datetime(2017, 12, 25, 0, 0) + current_season = season.get_season(summer_day, season.SOUTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_SUMMER, + current_season) + + def test_season_should_be_summer_southern_meteorological(self): + """Test that season should be summer.""" + # A known day in summer + summer_day = datetime(2017, 12, 3, 0, 0) + current_season = season.get_season(summer_day, season.SOUTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_SUMMER, + current_season) + + def test_season_should_be_autumn_southern_astonomical(self): + """Test that season should be spring.""" + # A known day in spring + autumn_day = datetime(2017, 4, 1, 0, 0) + current_season = season.get_season(autumn_day, season.SOUTHERN, + season.TYPE_ASTRONOMICAL) + self.assertEqual(season.STATE_AUTUMN, + current_season) + + def test_season_should_be_autumn_southern_meteorological(self): + """Test that season should be autumn.""" + # A known day in autumn + autumn_day = datetime(2017, 3, 3, 0, 0) + current_season = season.get_season(autumn_day, season.SOUTHERN, + season.TYPE_METEOROLOGICAL) + self.assertEqual(season.STATE_AUTUMN, + current_season) + + def test_on_equator_results_in_none(self): + """Test that season should be unknown.""" + # A known day in summer if astronomical and northern + summer_day = datetime(2017, 9, 3, 0, 0) + current_season = season.get_season(summer_day, + season.EQUATOR, + season.TYPE_ASTRONOMICAL) + self.assertEqual(None, current_season)