Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade Astral to 2.2 #48573

Merged
merged 2 commits into from
Apr 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions homeassistant/components/moon/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Support for tracking the moon phases."""
from astral import Astral
from astral import moon
import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
Expand Down Expand Up @@ -48,7 +48,6 @@ def __init__(self, name):
"""Initialize the moon sensor."""
self._name = name
self._state = None
self._astral = Astral()

@property
def name(self):
Expand Down Expand Up @@ -87,4 +86,4 @@ def icon(self):
async def async_update(self):
"""Get the time and updates the states."""
today = dt_util.as_local(dt_util.utcnow()).date()
self._state = self._astral.moon_phase(today)
self._state = moon.phase(today)
20 changes: 11 additions & 9 deletions homeassistant/components/sun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def __init__(self, hass):
"""Initialize the sun."""
self.hass = hass
self.location = None
self.elevation = 0.0
self._state = self.next_rising = self.next_setting = None
self.next_dawn = self.next_dusk = None
self.next_midnight = self.next_noon = None
Expand All @@ -100,10 +101,11 @@ def __init__(self, hass):
self._next_change = None

def update_location(_event):
location = get_astral_location(self.hass)
location, elevation = get_astral_location(self.hass)
if location == self.location:
return
self.location = location
self.elevation = elevation
self.update_events()

update_location(None)
Expand Down Expand Up @@ -140,7 +142,7 @@ def extra_state_attributes(self):

def _check_event(self, utc_point_in_time, sun_event, before):
next_utc = get_location_astral_event_next(
self.location, sun_event, utc_point_in_time
self.location, self.elevation, sun_event, utc_point_in_time
)
if next_utc < self._next_change:
self._next_change = next_utc
Expand Down Expand Up @@ -169,7 +171,7 @@ def update_events(self, now=None):
)
self.location.solar_depression = -10
self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY)
self.next_noon = self._check_event(utc_point_in_time, "solar_noon", None)
self.next_noon = self._check_event(utc_point_in_time, "noon", None)
self._check_event(utc_point_in_time, "dusk", PHASE_DAY)
self.next_setting = self._check_event(
utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY
Expand All @@ -180,17 +182,15 @@ def update_events(self, now=None):
self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT)
self.location.solar_depression = "astronomical"
self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT)
self.next_midnight = self._check_event(
utc_point_in_time, "solar_midnight", None
)
self.next_midnight = self._check_event(utc_point_in_time, "midnight", None)
self.location.solar_depression = "civil"

# if the event was solar midday or midnight, phase will now
# be None. Solar noon doesn't always happen when the sun is
# even in the day at the poles, so we can't rely on it.
# Need to calculate phase if next is noon or midnight
if self.phase is None:
elevation = self.location.solar_elevation(self._next_change)
elevation = self.location.solar_elevation(self._next_change, self.elevation)
if elevation >= 10:
self.phase = PHASE_DAY
elif elevation >= 0:
Expand Down Expand Up @@ -222,9 +222,11 @@ def update_sun_position(self, now=None):
"""Calculate the position of the sun."""
# Grab current time in case system clock changed since last time we ran.
utc_point_in_time = dt_util.utcnow()
self.solar_azimuth = round(self.location.solar_azimuth(utc_point_in_time), 2)
self.solar_azimuth = round(
self.location.solar_azimuth(utc_point_in_time, self.elevation), 2
)
self.solar_elevation = round(
self.location.solar_elevation(utc_point_in_time), 2
self.location.solar_elevation(utc_point_in_time, self.elevation), 2
)

_LOGGER.debug(
Expand Down
14 changes: 0 additions & 14 deletions homeassistant/components/tod/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,20 +173,6 @@ def _calculate_initial_boudary_time(self):

self._time_before = before_event_date

# We are calculating the _time_after value assuming that it will happen today
# But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00
# If _time_before and _time_after are ahead of current_datetime:
# _time_before is set to 12:00 next day
# _time_after is set to 23:00 today
# current_datetime is set to 10:00 today
if (
self._time_after > self.current_datetime
and self._time_before > self.current_datetime + timedelta(days=1)
):
# remove one day from _time_before and _time_after
self._time_after -= timedelta(days=1)
self._time_before -= timedelta(days=1)

# Add offset to utc boundaries according to the configuration
self._time_after += self._after_offset
self._time_before += self._before_offset
Expand Down
45 changes: 29 additions & 16 deletions homeassistant/helpers/sun.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,32 @@

DATA_LOCATION_CACHE = "astral_location_cache"

ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight")


@callback
@bind_hass
def get_astral_location(hass: HomeAssistant) -> astral.Location:
def get_astral_location(
hass: HomeAssistant,
) -> tuple[astral.location.Location, astral.Elevation]:
"""Get an astral location for the current Home Assistant configuration."""
from astral import Location # pylint: disable=import-outside-toplevel
from astral import LocationInfo # pylint: disable=import-outside-toplevel
from astral.location import Location # pylint: disable=import-outside-toplevel

latitude = hass.config.latitude
longitude = hass.config.longitude
timezone = str(hass.config.time_zone)
elevation = hass.config.elevation
info = ("", "", latitude, longitude, timezone, elevation)
info = ("", "", timezone, latitude, longitude)

# Cache astral locations so they aren't recreated with the same args
if DATA_LOCATION_CACHE not in hass.data:
hass.data[DATA_LOCATION_CACHE] = {}

if info not in hass.data[DATA_LOCATION_CACHE]:
hass.data[DATA_LOCATION_CACHE][info] = Location(info)
hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info))

return hass.data[DATA_LOCATION_CACHE][info]
return hass.data[DATA_LOCATION_CACHE][info], elevation


@callback
Expand All @@ -46,40 +51,46 @@ def get_astral_event_next(
offset: datetime.timedelta | None = None,
) -> datetime.datetime:
"""Calculate the next specified solar event."""
location = get_astral_location(hass)
return get_location_astral_event_next(location, event, utc_point_in_time, offset)
location, elevation = get_astral_location(hass)
return get_location_astral_event_next(
location, elevation, event, utc_point_in_time, offset
)


@callback
def get_location_astral_event_next(
location: astral.Location,
location: astral.location.Location,
elevation: astral.Elevation,
event: str,
utc_point_in_time: datetime.datetime | None = None,
offset: datetime.timedelta | None = None,
) -> datetime.datetime:
"""Calculate the next specified solar event."""
from astral import AstralError # pylint: disable=import-outside-toplevel

if offset is None:
offset = datetime.timedelta()

if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()

kwargs = {"local": False}
if event not in ELEVATION_AGNOSTIC_EVENTS:
kwargs["observer_elevation"] = elevation

mod = -1
while True:
try:
next_dt: datetime.datetime = (
getattr(location, event)(
dt_util.as_local(utc_point_in_time).date()
+ datetime.timedelta(days=mod),
local=False,
**kwargs,
)
+ offset
)
if next_dt > utc_point_in_time:
return next_dt
except AstralError:
except ValueError:
pass
mod += 1

Expand All @@ -92,19 +103,21 @@ def get_astral_event_date(
date: datetime.date | datetime.datetime | None = None,
) -> datetime.datetime | None:
"""Calculate the astral event time for the specified date."""
from astral import AstralError # pylint: disable=import-outside-toplevel

location = get_astral_location(hass)
location, elevation = get_astral_location(hass)

if date is None:
date = dt_util.now().date()

if isinstance(date, datetime.datetime):
date = dt_util.as_local(date).date()

kwargs = {"local": False}
if event not in ELEVATION_AGNOSTIC_EVENTS:
kwargs["observer_elevation"] = elevation

try:
return getattr(location, event)(date, local=False) # type: ignore
except AstralError:
return getattr(location, event)(date, **kwargs) # type: ignore
except ValueError:
# Event never occurs for specified date.
return None

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PyNaCl==1.3.0
aiodiscover==1.3.2
aiohttp==3.7.4.post0
aiohttp_cors==0.7.0
astral==1.10.1
astral==2.2
async-upnp-client==0.16.0
async_timeout==3.0.1
attrs==20.3.0
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Home Assistant Core
aiohttp==3.7.4.post0
astral==1.10.1
astral==2.2
async_timeout==3.0.1
attrs==20.3.0
awesomeversion==21.2.3
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

REQUIRES = [
"aiohttp==3.7.4.post0",
"astral==1.10.1",
"astral==2.2",
"async_timeout==3.0.1",
"attrs==20.3.0",
"awesomeversion==21.2.3",
Expand Down
37 changes: 20 additions & 17 deletions tests/components/sun/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,61 +22,64 @@ async def test_setting_rising(hass, legacy_patchable_time):
await hass.async_block_till_done()
state = hass.states.get(sun.ENTITY_ID)

from astral import Astral
from astral import LocationInfo
import astral.sun

astral = Astral()
utc_today = utc_now.date()

latitude = hass.config.latitude
longitude = hass.config.longitude
location = LocationInfo(
latitude=hass.config.latitude, longitude=hass.config.longitude
)

mod = -1
while True:
next_dawn = astral.dawn_utc(
utc_today + timedelta(days=mod), latitude, longitude
next_dawn = astral.sun.dawn(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_dawn > utc_now:
break
mod += 1

mod = -1
while True:
next_dusk = astral.dusk_utc(
utc_today + timedelta(days=mod), latitude, longitude
next_dusk = astral.sun.dusk(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_dusk > utc_now:
break
mod += 1

mod = -1
while True:
next_midnight = astral.solar_midnight_utc(
utc_today + timedelta(days=mod), longitude
next_midnight = astral.sun.midnight(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_midnight > utc_now:
break
mod += 1

mod = -1
while True:
next_noon = astral.solar_noon_utc(utc_today + timedelta(days=mod), longitude)
next_noon = astral.sun.noon(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_noon > utc_now:
break
mod += 1

mod = -1
while True:
next_rising = astral.sunrise_utc(
utc_today + timedelta(days=mod), latitude, longitude
next_rising = astral.sun.sunrise(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_rising > utc_now:
break
mod += 1

mod = -1
while True:
next_setting = astral.sunset_utc(
utc_today + timedelta(days=mod), latitude, longitude
next_setting = astral.sun.sunset(
location.observer, date=utc_today + timedelta(days=mod)
)
if next_setting > utc_now:
break
Expand Down Expand Up @@ -152,10 +155,10 @@ async def test_norway_in_june(hass):

assert dt_util.parse_datetime(
state.attributes[sun.STATE_ATTR_NEXT_RISING]
) == datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC)
) == datetime(2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(copy-paste of relevant information from above)
The new Astral both produces different times and provides times down to the MS level. To deal with that the tests need to be re-written. Tests were written to match old Astrals less accurate data.

I got confirmation here:
#48282 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference looks to be more than 24 hours. So it shouldn't just be the milliseconds accuracy. Can you explain the other differences that would result in this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, this confused and stumped me for some time. Long story short, since the day goes on for more than a month, it seems many calculators are in disagreement as to the date of sunrise and sunset here.

For example:
https://www.timeanddate.com/sun/norway/tromso?month=6&year=2016
https://www.esrl.noaa.gov/gmd/grad/solcalc/table.php?lat=69.6&lon=18.8&year=2016

It seems both versions of astral are wrong, but is entirely dependant on the altitude specified, as the sun barely makes it to the 7 degree inclination required for it to be considered a sunrise, the more altitude you add the sooner time the sunrise is ( in this case even by almost a day! ) The new version of Astral uses the location of the user ( via hass config ) to calculate times, as such the sunrise and sunset times will vary because of that. You will see I made edits to the tests in many places to specify a location's elevation ( based off the elevation at that latitude and longitude of coordinates ) as to increase the accuracy of apparent sunrise.

assert dt_util.parse_datetime(
state.attributes[sun.STATE_ATTR_NEXT_SETTING]
) == datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC)
) == datetime(2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC)

assert state.state == sun.STATE_ABOVE_HORIZON

Expand Down
Loading