Files
core/homeassistant/helpers/sun.py
T
2026-06-22 20:29:59 +02:00

165 lines
4.9 KiB
Python

"""Helpers for sun events."""
from collections.abc import Callable
import datetime
from typing import TYPE_CHECKING, Any, cast
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
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,
) -> tuple[astral.location.Location, astral.Elevation]:
"""Get an astral location for the current Home Assistant configuration."""
from astral import LocationInfo # noqa: PLC0415
from astral.location import Location # noqa: PLC0415
latitude = hass.config.latitude
longitude = hass.config.longitude
timezone = str(hass.config.time_zone)
elevation = hass.config.elevation
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,
event: str,
utc_point_in_time: datetime.datetime | None = None,
offset: datetime.timedelta | None = None,
) -> datetime.datetime:
"""Calculate the next specified solar event."""
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,
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 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()
if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()
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 = (
event_func(
observer,
dt_util.as_local(utc_point_in_time).date()
+ datetime.timedelta(days=mod),
**kwargs,
)
+ offset
)
if next_dt > utc_point_in_time:
return next_dt
except ValueError as err:
if not first_err:
first_err = err
mod += 1
raise ValueError(
f"Unable to find event after one year, initial ValueError: {first_err}"
) from first_err
@callback
def get_astral_event_date(
hass: HomeAssistant,
event: str,
date: datetime.date | datetime.datetime | None = None,
) -> datetime.datetime | None:
"""Calculate the astral event time for the specified date."""
import astral.sun # noqa: PLC0415
observer = get_astral_observer(hass)
if date is None:
date = dt_util.now().date()
if isinstance(date, datetime.datetime):
date = dt_util.as_local(date).date()
event_func = cast(_AstralSunEventCallable, getattr(astral.sun, event))
try:
return event_func(observer, date)
except ValueError:
# Event never occurs for specified date.
return None
@callback
def is_up(
hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None
) -> bool:
"""Calculate if the sun is currently up."""
if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()
next_sunrise = get_astral_event_next(hass, SUN_EVENT_SUNRISE, utc_point_in_time)
next_sunset = get_astral_event_next(hass, SUN_EVENT_SUNSET, utc_point_in_time)
return next_sunrise > next_sunset