core/homeassistant/helpers/sun.py

148 lines
4.4 KiB
Python
Raw Normal View History

"""Helpers for sun events."""
from __future__ import annotations
2022-04-27 15:19:46 +00:00
from collections.abc import Callable
import datetime
2022-04-27 15:19:46 +00:00
from typing import TYPE_CHECKING, Any, cast
2018-10-31 08:10:28 +00:00
from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
from homeassistant.core import HomeAssistant, callback
from homeassistant.loader import bind_hass
from homeassistant.util import dt as dt_util
if TYPE_CHECKING:
import astral
2022-04-27 15:19:46 +00:00
import astral.location
2019-07-31 19:25:30 +00:00
DATA_LOCATION_CACHE = "astral_location_cache"
2021-04-01 22:29:08 +00:00
ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight")
2022-04-27 15:19:46 +00:00
_AstralSunEventCallable = Callable[..., datetime.datetime]
@callback
@bind_hass
2021-04-01 22:29:08 +00:00
def get_astral_location(
hass: HomeAssistant,
) -> tuple[astral.location.Location, astral.Elevation]:
"""Get an astral location for the current Home Assistant configuration."""
2021-04-01 22:29:08 +00:00
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
2021-04-01 22:29:08 +00:00
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]:
2021-04-01 22:29:08 +00:00
hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info))
2021-04-01 22:29:08 +00:00
return hass.data[DATA_LOCATION_CACHE][info], elevation
@callback
@bind_hass
def get_astral_event_next(
hass: HomeAssistant,
2019-07-31 19:25:30 +00:00
event: str,
2021-03-17 17:34:19 +00:00
utc_point_in_time: datetime.datetime | None = None,
offset: datetime.timedelta | None = None,
2019-07-31 19:25:30 +00:00
) -> datetime.datetime:
"""Calculate the next specified solar event."""
2021-04-01 22:29:08 +00:00
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(
2021-04-01 22:29:08 +00:00
location: astral.location.Location,
elevation: astral.Elevation,
2019-07-31 19:25:30 +00:00
event: str,
2021-03-17 17:34:19 +00:00
utc_point_in_time: datetime.datetime | None = None,
offset: datetime.timedelta | None = None,
2019-07-31 19:25:30 +00:00
) -> datetime.datetime:
"""Calculate the next specified solar event."""
if offset is None:
offset = datetime.timedelta()
if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()
2022-04-27 15:19:46 +00:00
kwargs: dict[str, Any] = {"local": False}
2021-04-01 22:29:08 +00:00
if event not in ELEVATION_AGNOSTIC_EVENTS:
kwargs["observer_elevation"] = elevation
mod = -1
2023-03-15 02:27:29 +00:00
first_err = None
while mod < 367:
try:
2022-04-27 15:19:46 +00:00
next_dt = (
cast(_AstralSunEventCallable, getattr(location, event))(
2019-07-31 19:25:30 +00:00
dt_util.as_local(utc_point_in_time).date()
+ datetime.timedelta(days=mod),
2021-04-01 22:29:08 +00:00
**kwargs,
2019-07-31 19:25:30 +00:00
)
+ offset
)
if next_dt > utc_point_in_time:
return next_dt
2023-03-15 02:27:29 +00:00
except ValueError as err:
if not first_err:
first_err = err
mod += 1
2023-03-15 02:27:29 +00:00
raise ValueError(
f"Unable to find event after one year, initial ValueError: {first_err}"
) from first_err
@callback
@bind_hass
def get_astral_event_date(
hass: HomeAssistant,
2019-07-31 19:25:30 +00:00
event: str,
2021-03-17 17:34:19 +00:00
date: datetime.date | datetime.datetime | None = None,
) -> datetime.datetime | None:
"""Calculate the astral event time for the specified date."""
2021-04-01 22:29:08 +00:00
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()
2022-04-27 15:19:46 +00:00
kwargs: dict[str, Any] = {"local": False}
2021-04-01 22:29:08 +00:00
if event not in ELEVATION_AGNOSTIC_EVENTS:
kwargs["observer_elevation"] = elevation
try:
2022-04-27 15:19:46 +00:00
return cast(_AstralSunEventCallable, getattr(location, event))(date, **kwargs)
2021-04-01 22:29:08 +00:00
except ValueError:
# Event never occurs for specified date.
return None
@callback
@bind_hass
2019-07-31 19:25:30 +00:00
def is_up(
hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None
2019-07-31 19:25:30 +00:00
) -> bool:
"""Calculate if the sun is currently up."""
if utc_point_in_time is None:
utc_point_in_time = dt_util.utcnow()
2019-07-31 19:25:30 +00:00
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