mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Simplify sun entity (#174464)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
co-authored by
Franck Nijhof
parent
e1c8f3da78
commit
91f4168439
@@ -4,7 +4,8 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from astral.location import Elevation, Location
|
||||
from astral import Observer
|
||||
import astral.sun
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -17,8 +18,8 @@ from homeassistant.helpers import event
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.sun import (
|
||||
get_astral_location,
|
||||
get_location_astral_event_next,
|
||||
get_astral_observer,
|
||||
get_observer_astral_event_next,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -66,6 +67,13 @@ PHASE_SMALL_DAY = "small_day"
|
||||
# > 10° above horizon
|
||||
PHASE_DAY = "day"
|
||||
|
||||
# Depression angle (degrees below the horizon) of the sun at each dawn/dusk
|
||||
# phase boundary. A negative value means the sun is above the horizon.
|
||||
DEPRESSION_ASTRONOMICAL = 18.0
|
||||
DEPRESSION_NAUTICAL = 12.0
|
||||
DEPRESSION_CIVIL = 6.0
|
||||
DEPRESSION_SMALL_DAY = -10.0
|
||||
|
||||
# 4 mins is one degree of arc change of the sun on its circle.
|
||||
# During the night and the middle of the day we don't update
|
||||
# that much since it's not important.
|
||||
@@ -99,8 +107,7 @@ class Sun(Entity):
|
||||
_attr_name = "Sun"
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
location: Location
|
||||
elevation: Elevation
|
||||
observer: Observer
|
||||
next_rising: datetime
|
||||
next_setting: datetime
|
||||
next_dawn: datetime
|
||||
@@ -132,11 +139,10 @@ class Sun(Entity):
|
||||
@callback
|
||||
def update_location(self, _: Event | None = None, initial: bool = False) -> None:
|
||||
"""Update location."""
|
||||
location, elevation = get_astral_location(self.hass)
|
||||
if not initial and location == self.location:
|
||||
observer = get_astral_observer(self.hass)
|
||||
if not initial and observer == self.observer:
|
||||
return
|
||||
self.location = location
|
||||
self.elevation = elevation
|
||||
self.observer = observer
|
||||
if self._update_events_listener:
|
||||
self._update_events_listener()
|
||||
self.update_events()
|
||||
@@ -176,10 +182,14 @@ class Sun(Entity):
|
||||
}
|
||||
|
||||
def _check_event(
|
||||
self, utc_point_in_time: datetime, sun_event: str, before: str | None
|
||||
self,
|
||||
utc_point_in_time: datetime,
|
||||
sun_event: str,
|
||||
before: str | None,
|
||||
depression: float | None = None,
|
||||
) -> datetime:
|
||||
next_utc = get_location_astral_event_next(
|
||||
self.location, self.elevation, sun_event, utc_point_in_time
|
||||
next_utc = get_observer_astral_event_next(
|
||||
self.observer, sun_event, utc_point_in_time, depression=depression
|
||||
)
|
||||
if next_utc < self._next_change:
|
||||
self._next_change = next_utc
|
||||
@@ -195,39 +205,46 @@ class Sun(Entity):
|
||||
|
||||
# Work our way around the solar cycle, figure out the next
|
||||
# phase. Some of these are stored.
|
||||
self.location.solar_depression = "astronomical"
|
||||
self._check_event(utc_point_in_time, "dawn", PHASE_NIGHT)
|
||||
self.location.solar_depression = "nautical"
|
||||
self._check_event(utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT)
|
||||
self.location.solar_depression = "civil"
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_NIGHT, DEPRESSION_ASTRONOMICAL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT, DEPRESSION_NAUTICAL
|
||||
)
|
||||
self.next_dawn = self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT
|
||||
utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT, DEPRESSION_CIVIL
|
||||
)
|
||||
self.next_rising = self._check_event(
|
||||
utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT
|
||||
)
|
||||
self.location.solar_depression = -10
|
||||
self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dawn", PHASE_SMALL_DAY, DEPRESSION_SMALL_DAY
|
||||
)
|
||||
self.next_noon = self._check_event(utc_point_in_time, "noon", None)
|
||||
self._check_event(utc_point_in_time, "dusk", PHASE_DAY)
|
||||
self._check_event(utc_point_in_time, "dusk", PHASE_DAY, DEPRESSION_SMALL_DAY)
|
||||
self.next_setting = self._check_event(
|
||||
utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY
|
||||
)
|
||||
self.location.solar_depression = "civil"
|
||||
self.next_dusk = self._check_event(utc_point_in_time, "dusk", PHASE_TWILIGHT)
|
||||
self.location.solar_depression = "nautical"
|
||||
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_dusk = self._check_event(
|
||||
utc_point_in_time, "dusk", PHASE_TWILIGHT, DEPRESSION_CIVIL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT, DEPRESSION_NAUTICAL
|
||||
)
|
||||
self._check_event(
|
||||
utc_point_in_time,
|
||||
"dusk",
|
||||
PHASE_ASTRONOMICAL_TWILIGHT,
|
||||
DEPRESSION_ASTRONOMICAL,
|
||||
)
|
||||
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, self.elevation)
|
||||
elevation = astral.sun.elevation(self.observer, self._next_change)
|
||||
if elevation >= 10:
|
||||
self.phase = PHASE_DAY
|
||||
elif elevation >= 0:
|
||||
@@ -263,10 +280,10 @@ class Sun(Entity):
|
||||
# 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, self.elevation), 2
|
||||
astral.sun.azimuth(self.observer, utc_point_in_time), 2
|
||||
)
|
||||
self.solar_elevation = round(
|
||||
self.location.solar_elevation(utc_point_in_time, self.elevation), 2
|
||||
astral.sun.elevation(self.observer, utc_point_in_time), 2
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -8,15 +8,19 @@ from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .deprecation import deprecated_function
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import astral
|
||||
import astral.location
|
||||
|
||||
ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight")
|
||||
|
||||
type _AstralSunEventCallable = Callable[..., datetime.datetime]
|
||||
|
||||
|
||||
@deprecated_function(
|
||||
"homeassistant.helpers.sun.get_astral_observer",
|
||||
breaks_in_ha_version="2027.7",
|
||||
)
|
||||
@callback
|
||||
def get_astral_location(
|
||||
hass: HomeAssistant,
|
||||
@@ -33,6 +37,14 @@ def get_astral_location(
|
||||
return Location(LocationInfo("", "", timezone, latitude, longitude)), elevation
|
||||
|
||||
|
||||
@callback
|
||||
def get_astral_observer(hass: HomeAssistant) -> astral.Observer:
|
||||
"""Get an astral observer for the current Home Assistant configuration."""
|
||||
from astral import Observer # noqa: PLC0415
|
||||
|
||||
return Observer(hass.config.latitude, hass.config.longitude, hass.config.elevation)
|
||||
|
||||
|
||||
@callback
|
||||
def get_astral_event_next(
|
||||
hass: HomeAssistant,
|
||||
@@ -41,12 +53,14 @@ def get_astral_event_next(
|
||||
offset: datetime.timedelta | None = None,
|
||||
) -> datetime.datetime:
|
||||
"""Calculate the next specified solar event."""
|
||||
location, elevation = get_astral_location(hass)
|
||||
return get_location_astral_event_next(
|
||||
location, elevation, event, utc_point_in_time, offset
|
||||
)
|
||||
observer = get_astral_observer(hass)
|
||||
return get_observer_astral_event_next(observer, event, utc_point_in_time, offset)
|
||||
|
||||
|
||||
@deprecated_function(
|
||||
"homeassistant.helpers.sun.get_observer_astral_event_next",
|
||||
breaks_in_ha_version="2027.7",
|
||||
)
|
||||
@callback
|
||||
def get_location_astral_event_next(
|
||||
location: astral.location.Location,
|
||||
@@ -56,6 +70,25 @@ def get_location_astral_event_next(
|
||||
offset: datetime.timedelta | None = None,
|
||||
) -> datetime.datetime:
|
||||
"""Calculate the next specified solar event."""
|
||||
from astral import Observer # noqa: PLC0415
|
||||
|
||||
observer = Observer(location.latitude, location.longitude, elevation)
|
||||
depression = location.solar_depression if event in ("dawn", "dusk") else None
|
||||
return get_observer_astral_event_next(
|
||||
observer, event, utc_point_in_time, offset, depression
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def get_observer_astral_event_next(
|
||||
observer: astral.Observer,
|
||||
event: str,
|
||||
utc_point_in_time: datetime.datetime | None = None,
|
||||
offset: datetime.timedelta | None = None,
|
||||
depression: float | None = None,
|
||||
) -> datetime.datetime:
|
||||
"""Calculate the next specified solar event."""
|
||||
import astral.sun # noqa: PLC0415
|
||||
|
||||
if offset is None:
|
||||
offset = datetime.timedelta()
|
||||
@@ -63,16 +96,18 @@ def get_location_astral_event_next(
|
||||
if utc_point_in_time is None:
|
||||
utc_point_in_time = dt_util.utcnow()
|
||||
|
||||
kwargs: dict[str, Any] = {"local": False}
|
||||
if event not in ELEVATION_AGNOSTIC_EVENTS:
|
||||
kwargs["observer_elevation"] = elevation
|
||||
event_func = cast(_AstralSunEventCallable, getattr(astral.sun, event))
|
||||
kwargs: dict[str, Any] = {}
|
||||
if depression is not None:
|
||||
kwargs["depression"] = depression
|
||||
|
||||
mod = -1
|
||||
first_err = None
|
||||
while mod < 367:
|
||||
try:
|
||||
next_dt = (
|
||||
cast(_AstralSunEventCallable, getattr(location, event))(
|
||||
event_func(
|
||||
observer,
|
||||
dt_util.as_local(utc_point_in_time).date()
|
||||
+ datetime.timedelta(days=mod),
|
||||
**kwargs,
|
||||
@@ -97,7 +132,9 @@ def get_astral_event_date(
|
||||
date: datetime.date | datetime.datetime | None = None,
|
||||
) -> datetime.datetime | None:
|
||||
"""Calculate the astral event time for the specified date."""
|
||||
location, elevation = get_astral_location(hass)
|
||||
import astral.sun # noqa: PLC0415
|
||||
|
||||
observer = get_astral_observer(hass)
|
||||
|
||||
if date is None:
|
||||
date = dt_util.now().date()
|
||||
@@ -105,12 +142,9 @@ def get_astral_event_date(
|
||||
if isinstance(date, datetime.datetime):
|
||||
date = dt_util.as_local(date).date()
|
||||
|
||||
kwargs: dict[str, Any] = {"local": False}
|
||||
if event not in ELEVATION_AGNOSTIC_EVENTS:
|
||||
kwargs["observer_elevation"] = elevation
|
||||
|
||||
event_func = cast(_AstralSunEventCallable, getattr(astral.sun, event))
|
||||
try:
|
||||
return cast(_AstralSunEventCallable, getattr(location, event))(date, **kwargs)
|
||||
return event_func(observer, date)
|
||||
except ValueError:
|
||||
# Event never occurs for specified date.
|
||||
return None
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from astral import LocationInfo
|
||||
from astral.location import Location
|
||||
import astral.sun
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
@@ -200,3 +201,44 @@ def test_impossible_elevation(hass: HomeAssistant) -> None:
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june)
|
||||
|
||||
|
||||
def test_deprecated_get_astral_location(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test the deprecated get_astral_location helper."""
|
||||
location, elevation = sun.get_astral_location(hass)
|
||||
|
||||
observer = sun.get_astral_observer(hass)
|
||||
assert location.latitude == observer.latitude
|
||||
assert location.longitude == observer.longitude
|
||||
assert elevation == observer.elevation
|
||||
assert (
|
||||
"The deprecated function get_astral_location was called. It will be removed "
|
||||
"in HA Core 2027.7. Use homeassistant.helpers.sun.get_astral_observer instead"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
def test_deprecated_get_location_astral_event_next(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test the deprecated get_location_astral_event_next helper."""
|
||||
utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC)
|
||||
location = Location(
|
||||
LocationInfo(
|
||||
"",
|
||||
"",
|
||||
str(hass.config.time_zone),
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
)
|
||||
)
|
||||
|
||||
assert sun.get_location_astral_event_next(
|
||||
location, hass.config.elevation, SUN_EVENT_SUNRISE, utc_now
|
||||
) == sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, utc_now)
|
||||
assert (
|
||||
"The deprecated function get_location_astral_event_next was called. It will "
|
||||
"be removed in HA Core 2027.7. Use "
|
||||
"homeassistant.helpers.sun.get_observer_astral_event_next instead"
|
||||
) in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user