Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Coordinator for the Islamic prayer times integration."""
2 
3 from __future__ import annotations
4 
5 from datetime import date, datetime, timedelta
6 import logging
7 from typing import Any, cast
8 
9 from prayer_times_calculator_offline import PrayerTimesCalculator
10 
11 from homeassistant.config_entries import ConfigEntry
12 from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
13 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
14 from homeassistant.helpers.event import async_track_point_in_time
15 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
16 import homeassistant.util.dt as dt_util
17 
18 from .const import (
19  CONF_CALC_METHOD,
20  CONF_LAT_ADJ_METHOD,
21  CONF_MIDNIGHT_MODE,
22  CONF_SCHOOL,
23  DEFAULT_CALC_METHOD,
24  DEFAULT_LAT_ADJ_METHOD,
25  DEFAULT_MIDNIGHT_MODE,
26  DEFAULT_SCHOOL,
27  DOMAIN,
28 )
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 
34  """Islamic Prayer Client Object."""
35 
36  config_entry: ConfigEntry
37 
38  def __init__(self, hass: HomeAssistant) -> None:
39  """Initialize the Islamic Prayer client."""
40  super().__init__(
41  hass,
42  _LOGGER,
43  name=DOMAIN,
44  )
45  self.latitudelatitude = self.config_entryconfig_entry.data[CONF_LATITUDE]
46  self.longitudelongitude = self.config_entryconfig_entry.data[CONF_LONGITUDE]
47  self.event_unsubevent_unsub: CALLBACK_TYPE | None = None
48 
49  @property
50  def calc_method(self) -> str:
51  """Return the calculation method."""
52  return self.config_entryconfig_entry.options.get(CONF_CALC_METHOD, DEFAULT_CALC_METHOD)
53 
54  @property
55  def lat_adj_method(self) -> str:
56  """Return the latitude adjustment method."""
57  return str(
58  self.config_entryconfig_entry.options.get(
59  CONF_LAT_ADJ_METHOD, DEFAULT_LAT_ADJ_METHOD
60  ).replace("_", " ")
61  )
62 
63  @property
64  def midnight_mode(self) -> str:
65  """Return the midnight mode."""
66  return self.config_entryconfig_entry.options.get(CONF_MIDNIGHT_MODE, DEFAULT_MIDNIGHT_MODE)
67 
68  @property
69  def school(self) -> str:
70  """Return the school."""
71  return self.config_entryconfig_entry.options.get(CONF_SCHOOL, DEFAULT_SCHOOL)
72 
73  def get_new_prayer_times(self, for_date: date) -> dict[str, Any]:
74  """Fetch prayer times for the specified date."""
75  calc = PrayerTimesCalculator(
76  latitude=self.latitudelatitude,
77  longitude=self.longitudelongitude,
78  calculation_method=self.calc_methodcalc_method,
79  latitudeAdjustmentMethod=self.lat_adj_methodlat_adj_method,
80  midnightMode=self.midnight_modemidnight_mode,
81  school=self.schoolschool,
82  date=str(for_date),
83  iso8601=True,
84  )
85  return cast(dict[str, Any], calc.fetch_prayer_times())
86 
87  @callback
88  def async_schedule_future_update(self, midnight_dt: datetime) -> None:
89  """Schedule future update for sensors.
90 
91  The least surprising behaviour is to load the next day's prayer times only
92  after the current day's prayers are complete. We will take the fiqhi opinion
93  that Isha should be prayed before Islamic midnight (which may be before or after 12:00 midnight),
94  and thus we will switch to the next day's timings at Islamic midnight.
95 
96  The +1s is to ensure that any automations predicated on the arrival of Islamic midnight will run.
97 
98  """
99  _LOGGER.debug("Scheduling next update for Islamic prayer times")
100 
102  self.hasshass, self.async_request_updateasync_request_update, midnight_dt + timedelta(seconds=1)
103  )
104 
105  async def async_request_update(self, _: datetime) -> None:
106  """Request update from coordinator."""
107  await self.async_request_refreshasync_request_refresh()
108 
109  async def _async_update_data(self) -> dict[str, datetime]:
110  """Update sensors with new prayer times.
111 
112  Prayer time calculations "roll over" at 12:00 midnight - but this does not mean that all prayers
113  occur within that Gregorian calendar day. For instance Jasper, Alta. sees Isha occur after 00:00 in the summer.
114  It is similarly possible (albeit less likely) that Fajr occurs before 00:00.
115 
116  As such, to ensure that no prayer times are "unreachable" (e.g. we always see the Isha timestamp pass before loading the next day's times),
117  we calculate 3 days' worth of times (-1, 0, +1 days) and select the appropriate set based on Islamic midnight.
118 
119  The calculation is inexpensive, so there is no need to cache it.
120  """
121 
122  # Zero out the us component to maintain consistent rollover at T+1s
123  now = dt_util.now().replace(microsecond=0)
124  yesterday_times = self.get_new_prayer_timesget_new_prayer_times((now - timedelta(days=1)).date())
125  today_times = self.get_new_prayer_timesget_new_prayer_times(now.date())
126  tomorrow_times = self.get_new_prayer_timesget_new_prayer_times((now + timedelta(days=1)).date())
127 
128  if (
129  yesterday_midnight := dt_util.parse_datetime(yesterday_times["Midnight"])
130  ) and now <= yesterday_midnight:
131  prayer_times = yesterday_times
132  elif (
133  tomorrow_midnight := dt_util.parse_datetime(today_times["Midnight"])
134  ) and now > tomorrow_midnight:
135  prayer_times = tomorrow_times
136  else:
137  prayer_times = today_times
138 
139  # introduced in prayer-times-calculator 0.0.8
140  prayer_times.pop("date", None)
141 
142  prayer_times_info: dict[str, datetime] = {}
143  for prayer, time in prayer_times.items():
144  if prayer_time := dt_util.parse_datetime(time):
145  prayer_times_info[prayer] = dt_util.as_utc(prayer_time)
146 
147  self.async_schedule_future_updateasync_schedule_future_update(prayer_times_info["Midnight"])
148  return prayer_times_info
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1462