diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index dddf38fdf9e..e87a607b167 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, datetime +from datetime import datetime import ephem @@ -12,7 +12,7 @@ from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util.dt import utcnow +from homeassistant.util import dt as dt_util from .const import DOMAIN, TYPE_ASTRONOMICAL @@ -50,7 +50,7 @@ async def async_setup_entry( def get_season( - current_date: date, hemisphere: str, season_tracking_type: str + current_datetime: datetime, hemisphere: str, season_tracking_type: str ) -> str | None: """Calculate the current season.""" @@ -58,22 +58,36 @@ def get_season( return None if season_tracking_type == TYPE_ASTRONOMICAL: - spring_start = ephem.next_equinox(str(current_date.year)).datetime() - summer_start = ephem.next_solstice(str(current_date.year)).datetime() - autumn_start = ephem.next_equinox(spring_start).datetime() - winter_start = ephem.next_solstice(summer_start).datetime() + spring_start = ( + ephem.next_equinox(str(current_datetime.year)) + .datetime() + .replace(tzinfo=dt_util.UTC) + ) + summer_start = ( + ephem.next_solstice(str(current_datetime.year)) + .datetime() + .replace(tzinfo=dt_util.UTC) + ) + autumn_start = ( + ephem.next_equinox(spring_start).datetime().replace(tzinfo=dt_util.UTC) + ) + winter_start = ( + ephem.next_solstice(summer_start).datetime().replace(tzinfo=dt_util.UTC) + ) else: - spring_start = datetime(2017, 3, 1).replace(year=current_date.year) + spring_start = current_datetime.replace( + month=3, day=1, hour=0, minute=0, second=0, microsecond=0 + ) summer_start = spring_start.replace(month=6) autumn_start = spring_start.replace(month=9) winter_start = spring_start.replace(month=12) season = STATE_WINTER - if spring_start <= current_date < summer_start: + if spring_start <= current_datetime < summer_start: season = STATE_SPRING - elif summer_start <= current_date < autumn_start: + elif summer_start <= current_datetime < autumn_start: season = STATE_SUMMER - elif autumn_start <= current_date < winter_start: + elif autumn_start <= current_datetime < winter_start: season = STATE_AUTUMN # If user is located in the southern hemisphere swap the season @@ -104,6 +118,4 @@ class SeasonSensorEntity(SensorEntity): def update(self) -> None: """Update season.""" - self._attr_native_value = get_season( - utcnow().replace(tzinfo=None), self.hemisphere, self.type - ) + self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type) diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 881192c95f0..fc19b414d91 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Season integration.""" from datetime import datetime +from zoneinfo import ZoneInfo from freezegun import freeze_time import pytest @@ -20,6 +21,8 @@ from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass from homeassistant.const import ATTR_DEVICE_CLASS, CONF_TYPE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.util.dt import UTC from tests.common import MockConfigEntry @@ -44,25 +47,25 @@ HEMISPHERE_EMPTY = { } NORTHERN_PARAMETERS = [ - (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_SUMMER), - (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_SUMMER), - (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_AUTUMN), - (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_AUTUMN), - (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_WINTER), - (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_WINTER), - (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_SPRING), - (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_SPRING), + (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SUMMER), + (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_SUMMER), + (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_AUTUMN), + (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN), + (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_WINTER), + (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_WINTER), + (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_SPRING), + (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_SPRING), ] SOUTHERN_PARAMETERS = [ - (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_SUMMER), - (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_SUMMER), - (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_AUTUMN), - (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_AUTUMN), - (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_WINTER), - (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_WINTER), - (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_SPRING), - (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_SPRING), + (TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_SUMMER), + (TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_SUMMER), + (TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_AUTUMN), + (TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN), + (TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_WINTER), + (TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_WINTER), + (TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_SPRING), + (TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SPRING), ] @@ -154,7 +157,7 @@ async def test_season_equator( hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"] mock_config_entry.add_to_hass(hass) - with freeze_time(datetime(2017, 9, 3, 0, 0)): + with freeze_time(datetime(2017, 9, 3, 0, 0, tzinfo=UTC)): await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -165,3 +168,43 @@ async def test_season_equator( entry = entity_registry.async_get("sensor.season") assert entry assert entry.unique_id == mock_config_entry.entry_id + + +async def test_season_local_midnight( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that season changes at local midnight, not UTC.""" + await hass.config.async_set_time_zone("Australia/Sydney") + hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"] + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, + unique_id=TYPE_METEOROLOGICAL, + data={CONF_TYPE: TYPE_METEOROLOGICAL}, + ) + + sydney_tz = ZoneInfo("Australia/Sydney") + + # The day before autumn starts, at 23:59:59 local time (summer) + day_before = datetime(2017, 2, 28, 23, 59, 59, tzinfo=sydney_tz) + + with freeze_time(day_before): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.season") + assert state + assert state.state == STATE_SUMMER + + # Exactly midnight local time (autumn) + midnight = datetime(2017, 3, 1, 0, 0, 0, tzinfo=sydney_tz) + + with freeze_time(midnight): + await async_update_entity(hass, "sensor.season") + await hass.async_block_till_done() + + state = hass.states.get("sensor.season") + assert state + assert state.state == STATE_AUTUMN