Home Assistant Unofficial Reference 2024.12.1
sun.py
Go to the documentation of this file.
1 """Helpers for sun events."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import datetime
7 from typing import TYPE_CHECKING, Any, cast
8 
9 from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
10 from homeassistant.core import HomeAssistant, callback
11 from homeassistant.loader import bind_hass
12 from homeassistant.util import dt as dt_util
13 from homeassistant.util.hass_dict import HassKey
14 
15 if TYPE_CHECKING:
16  import astral
17  import astral.location
18 
19 DATA_LOCATION_CACHE: HassKey[
20  dict[tuple[str, str, str, float, float], astral.location.Location]
21 ] = HassKey("astral_location_cache")
22 
23 ELEVATION_AGNOSTIC_EVENTS = ("noon", "midnight")
24 
25 type _AstralSunEventCallable = Callable[..., datetime.datetime]
26 
27 
28 @callback
29 @bind_hass
31  hass: HomeAssistant,
32 ) -> tuple[astral.location.Location, astral.Elevation]:
33  """Get an astral location for the current Home Assistant configuration."""
34  from astral import LocationInfo # pylint: disable=import-outside-toplevel
35  from astral.location import Location # pylint: disable=import-outside-toplevel
36 
37  latitude = hass.config.latitude
38  longitude = hass.config.longitude
39  timezone = str(hass.config.time_zone)
40  elevation = hass.config.elevation
41  info = ("", "", timezone, latitude, longitude)
42 
43  # Cache astral locations so they aren't recreated with the same args
44  if DATA_LOCATION_CACHE not in hass.data:
45  hass.data[DATA_LOCATION_CACHE] = {}
46 
47  if info not in hass.data[DATA_LOCATION_CACHE]:
48  hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info))
49 
50  return hass.data[DATA_LOCATION_CACHE][info], elevation
51 
52 
53 @callback
54 @bind_hass
56  hass: HomeAssistant,
57  event: str,
58  utc_point_in_time: datetime.datetime | None = None,
59  offset: datetime.timedelta | None = None,
60 ) -> datetime.datetime:
61  """Calculate the next specified solar event."""
62  location, elevation = get_astral_location(hass)
64  location, elevation, event, utc_point_in_time, offset
65  )
66 
67 
68 @callback
70  location: astral.location.Location,
71  elevation: astral.Elevation,
72  event: str,
73  utc_point_in_time: datetime.datetime | None = None,
74  offset: datetime.timedelta | None = None,
75 ) -> datetime.datetime:
76  """Calculate the next specified solar event."""
77 
78  if offset is None:
79  offset = datetime.timedelta()
80 
81  if utc_point_in_time is None:
82  utc_point_in_time = dt_util.utcnow()
83 
84  kwargs: dict[str, Any] = {"local": False}
85  if event not in ELEVATION_AGNOSTIC_EVENTS:
86  kwargs["observer_elevation"] = elevation
87 
88  mod = -1
89  first_err = None
90  while mod < 367:
91  try:
92  next_dt = (
93  cast(_AstralSunEventCallable, getattr(location, event))(
94  dt_util.as_local(utc_point_in_time).date()
95  + datetime.timedelta(days=mod),
96  **kwargs,
97  )
98  + offset
99  )
100  if next_dt > utc_point_in_time:
101  return next_dt
102  except ValueError as err:
103  if not first_err:
104  first_err = err
105  mod += 1
106  raise ValueError(
107  f"Unable to find event after one year, initial ValueError: {first_err}"
108  ) from first_err
109 
110 
111 @callback
112 @bind_hass
114  hass: HomeAssistant,
115  event: str,
116  date: datetime.date | datetime.datetime | None = None,
117 ) -> datetime.datetime | None:
118  """Calculate the astral event time for the specified date."""
119  location, elevation = get_astral_location(hass)
120 
121  if date is None:
122  date = dt_util.now().date()
123 
124  if isinstance(date, datetime.datetime):
125  date = dt_util.as_local(date).date()
126 
127  kwargs: dict[str, Any] = {"local": False}
128  if event not in ELEVATION_AGNOSTIC_EVENTS:
129  kwargs["observer_elevation"] = elevation
130 
131  try:
132  return cast(_AstralSunEventCallable, getattr(location, event))(date, **kwargs)
133  except ValueError:
134  # Event never occurs for specified date.
135  return None
136 
137 
138 @callback
139 @bind_hass
140 def is_up(
141  hass: HomeAssistant, utc_point_in_time: datetime.datetime | None = None
142 ) -> bool:
143  """Calculate if the sun is currently up."""
144  if utc_point_in_time is None:
145  utc_point_in_time = dt_util.utcnow()
146 
147  next_sunrise = get_astral_event_next(hass, SUN_EVENT_SUNRISE, utc_point_in_time)
148  next_sunset = get_astral_event_next(hass, SUN_EVENT_SUNSET, utc_point_in_time)
149 
150  return next_sunrise > next_sunset
datetime.datetime|None get_astral_event_date(HomeAssistant hass, str event, datetime.date|datetime.datetime|None date=None)
Definition: sun.py:117
tuple[astral.location.Location, astral.Elevation] get_astral_location(HomeAssistant hass)
Definition: sun.py:32
bool is_up(HomeAssistant hass, datetime.datetime|None utc_point_in_time=None)
Definition: sun.py:142
datetime.datetime get_astral_event_next(HomeAssistant hass, str event, datetime.datetime|None utc_point_in_time=None, datetime.timedelta|None offset=None)
Definition: sun.py:60
datetime.datetime get_location_astral_event_next(astral.location.Location location, astral.Elevation elevation, str event, datetime.datetime|None utc_point_in_time=None, datetime.timedelta|None offset=None)
Definition: sun.py:75