2017-05-09 07:03:34 +00:00
|
|
|
"""Helpers for sun events."""
|
2021-02-12 09:58:20 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2017-05-09 07:03:34 +00:00
|
|
|
import datetime
|
2021-03-17 17:34:19 +00:00
|
|
|
from typing import TYPE_CHECKING
|
2017-05-09 07:03:34 +00:00
|
|
|
|
2018-10-31 08:10:28 +00:00
|
|
|
from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
2021-03-27 11:55:24 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2017-10-08 15:17:54 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
2018-11-04 21:46:42 +00:00
|
|
|
if TYPE_CHECKING:
|
2021-03-02 08:02:04 +00:00
|
|
|
import astral
|
2017-05-09 07:03:34 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_LOCATION_CACHE = "astral_location_cache"
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2021-03-27 11:55:24 +00:00
|
|
|
def get_astral_location(hass: HomeAssistant) -> astral.Location:
|
2017-06-08 13:53:12 +00:00
|
|
|
"""Get an astral location for the current Home Assistant configuration."""
|
2020-04-04 15:07:36 +00:00
|
|
|
from astral import Location # pylint: disable=import-outside-toplevel
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
latitude = hass.config.latitude
|
|
|
|
longitude = hass.config.longitude
|
2018-11-04 21:46:42 +00:00
|
|
|
timezone = str(hass.config.time_zone)
|
2017-05-09 07:03:34 +00:00
|
|
|
elevation = hass.config.elevation
|
2019-07-31 19:25:30 +00:00
|
|
|
info = ("", "", latitude, longitude, timezone, elevation)
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
return hass.data[DATA_LOCATION_CACHE][info]
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2018-11-04 21:46:42 +00:00
|
|
|
def get_astral_event_next(
|
2021-03-27 11:55:24 +00:00
|
|
|
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:
|
2017-05-09 07:03:34 +00:00
|
|
|
"""Calculate the next specified solar event."""
|
|
|
|
location = get_astral_location(hass)
|
2019-07-31 19:25:30 +00:00
|
|
|
return get_location_astral_event_next(location, event, utc_point_in_time, offset)
|
2019-05-15 07:02:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def get_location_astral_event_next(
|
2021-03-22 12:29:39 +00:00
|
|
|
location: astral.Location,
|
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:
|
2019-05-15 07:02:29 +00:00
|
|
|
"""Calculate the next specified solar event."""
|
2020-04-04 15:07:36 +00:00
|
|
|
from astral import AstralError # pylint: disable=import-outside-toplevel
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
if offset is None:
|
|
|
|
offset = datetime.timedelta()
|
|
|
|
|
|
|
|
if utc_point_in_time is None:
|
|
|
|
utc_point_in_time = dt_util.utcnow()
|
|
|
|
|
|
|
|
mod = -1
|
|
|
|
while True:
|
|
|
|
try:
|
2019-09-04 03:36:04 +00:00
|
|
|
next_dt: datetime.datetime = (
|
2019-07-31 19:25:30 +00:00
|
|
|
getattr(location, event)(
|
|
|
|
dt_util.as_local(utc_point_in_time).date()
|
|
|
|
+ datetime.timedelta(days=mod),
|
|
|
|
local=False,
|
|
|
|
)
|
|
|
|
+ offset
|
2019-09-04 03:36:04 +00:00
|
|
|
)
|
2017-05-09 07:03:34 +00:00
|
|
|
if next_dt > utc_point_in_time:
|
|
|
|
return next_dt
|
2018-11-04 21:46:42 +00:00
|
|
|
except AstralError:
|
2017-05-09 07:03:34 +00:00
|
|
|
pass
|
|
|
|
mod += 1
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2018-11-04 21:46:42 +00:00
|
|
|
def get_astral_event_date(
|
2021-03-27 11:55:24 +00:00
|
|
|
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:
|
2017-05-09 07:03:34 +00:00
|
|
|
"""Calculate the astral event time for the specified date."""
|
2020-04-04 15:07:36 +00:00
|
|
|
from astral import AstralError # pylint: disable=import-outside-toplevel
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
location = 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()
|
|
|
|
|
|
|
|
try:
|
2018-11-04 21:46:42 +00:00
|
|
|
return getattr(location, event)(date, local=False) # type: ignore
|
|
|
|
except AstralError:
|
2017-05-09 07:03:34 +00:00
|
|
|
# Event never occurs for specified date.
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2017-10-08 15:17:54 +00:00
|
|
|
@bind_hass
|
2019-07-31 19:25:30 +00:00
|
|
|
def is_up(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> bool:
|
2017-05-09 07:03:34 +00:00
|
|
|
"""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)
|
2017-05-09 07:03:34 +00:00
|
|
|
|
|
|
|
return next_sunrise > next_sunset
|