|
1 | 1 | """The islamic_prayer_times component.""" |
| 2 | +from datetime import timedelta |
| 3 | +import logging |
| 4 | + |
| 5 | +from prayer_times_calculator import PrayerTimesCalculator, exceptions |
| 6 | +from requests.exceptions import ConnectionError as ConnError |
| 7 | +import voluptuous as vol |
| 8 | + |
| 9 | +from homeassistant.config_entries import SOURCE_IMPORT |
| 10 | +from homeassistant.exceptions import ConfigEntryNotReady |
| 11 | +from homeassistant.helpers.dispatcher import async_dispatcher_send |
| 12 | +from homeassistant.helpers.event import async_call_later, async_track_point_in_time |
| 13 | +import homeassistant.util.dt as dt_util |
| 14 | + |
| 15 | +from .const import ( |
| 16 | + CALC_METHODS, |
| 17 | + CONF_CALC_METHOD, |
| 18 | + DATA_UPDATED, |
| 19 | + DEFAULT_CALC_METHOD, |
| 20 | + DOMAIN, |
| 21 | +) |
| 22 | + |
| 23 | +_LOGGER = logging.getLogger(__name__) |
| 24 | + |
| 25 | + |
| 26 | +CONFIG_SCHEMA = vol.Schema( |
| 27 | + { |
| 28 | + DOMAIN: { |
| 29 | + vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( |
| 30 | + CALC_METHODS |
| 31 | + ), |
| 32 | + } |
| 33 | + }, |
| 34 | + extra=vol.ALLOW_EXTRA, |
| 35 | +) |
| 36 | + |
| 37 | + |
| 38 | +async def async_setup(hass, config): |
| 39 | + """Import the Islamic Prayer component from config.""" |
| 40 | + if DOMAIN in config: |
| 41 | + hass.async_create_task( |
| 42 | + hass.config_entries.flow.async_init( |
| 43 | + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] |
| 44 | + ) |
| 45 | + ) |
| 46 | + |
| 47 | + return True |
| 48 | + |
| 49 | + |
| 50 | +async def async_setup_entry(hass, config_entry): |
| 51 | + """Set up the Islamic Prayer Component.""" |
| 52 | + client = IslamicPrayerClient(hass, config_entry) |
| 53 | + |
| 54 | + if not await client.async_setup(): |
| 55 | + return False |
| 56 | + |
| 57 | + hass.data.setdefault(DOMAIN, client) |
| 58 | + return True |
| 59 | + |
| 60 | + |
| 61 | +async def async_unload_entry(hass, config_entry): |
| 62 | + """Unload Islamic Prayer entry from config_entry.""" |
| 63 | + if hass.data[DOMAIN].event_unsub: |
| 64 | + hass.data[DOMAIN].event_unsub() |
| 65 | + hass.data.pop(DOMAIN) |
| 66 | + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") |
| 67 | + |
| 68 | + return True |
| 69 | + |
| 70 | + |
| 71 | +class IslamicPrayerClient: |
| 72 | + """Islamic Prayer Client Object.""" |
| 73 | + |
| 74 | + def __init__(self, hass, config_entry): |
| 75 | + """Initialize the Islamic Prayer client.""" |
| 76 | + self.hass = hass |
| 77 | + self.config_entry = config_entry |
| 78 | + self.prayer_times_info = {} |
| 79 | + self.available = True |
| 80 | + self.event_unsub = None |
| 81 | + |
| 82 | + @property |
| 83 | + def calc_method(self): |
| 84 | + """Return the calculation method.""" |
| 85 | + return self.config_entry.options[CONF_CALC_METHOD] |
| 86 | + |
| 87 | + def get_new_prayer_times(self): |
| 88 | + """Fetch prayer times for today.""" |
| 89 | + calc = PrayerTimesCalculator( |
| 90 | + latitude=self.hass.config.latitude, |
| 91 | + longitude=self.hass.config.longitude, |
| 92 | + calculation_method=self.calc_method, |
| 93 | + date=str(dt_util.now().date()), |
| 94 | + ) |
| 95 | + return calc.fetch_prayer_times() |
| 96 | + |
| 97 | + async def async_schedule_future_update(self): |
| 98 | + """Schedule future update for sensors. |
| 99 | +
|
| 100 | + Midnight is a calculated time. The specifics of the calculation |
| 101 | + depends on the method of the prayer time calculation. This calculated |
| 102 | + midnight is the time at which the time to pray the Isha prayers have |
| 103 | + expired. |
| 104 | +
|
| 105 | + Calculated Midnight: The Islamic midnight. |
| 106 | + Traditional Midnight: 12:00AM |
| 107 | +
|
| 108 | + Update logic for prayer times: |
| 109 | +
|
| 110 | + If the Calculated Midnight is before the traditional midnight then wait |
| 111 | + until the traditional midnight to run the update. This way the day |
| 112 | + will have changed over and we don't need to do any fancy calculations. |
| 113 | +
|
| 114 | + If the Calculated Midnight is after the traditional midnight, then wait |
| 115 | + until after the calculated Midnight. We don't want to update the prayer |
| 116 | + times too early or else the timings might be incorrect. |
| 117 | +
|
| 118 | + Example: |
| 119 | + calculated midnight = 11:23PM (before traditional midnight) |
| 120 | + Update time: 12:00AM |
| 121 | +
|
| 122 | + calculated midnight = 1:35AM (after traditional midnight) |
| 123 | + update time: 1:36AM. |
| 124 | +
|
| 125 | + """ |
| 126 | + _LOGGER.debug("Scheduling next update for Islamic prayer times") |
| 127 | + |
| 128 | + now = dt_util.as_local(dt_util.now()) |
| 129 | + |
| 130 | + midnight_dt = self.prayer_times_info["Midnight"] |
| 131 | + |
| 132 | + if now > dt_util.as_local(midnight_dt): |
| 133 | + next_update_at = midnight_dt + timedelta(days=1, minutes=1) |
| 134 | + _LOGGER.debug( |
| 135 | + "Midnight is after day the changes so schedule update for after Midnight the next day" |
| 136 | + ) |
| 137 | + else: |
| 138 | + _LOGGER.debug( |
| 139 | + "Midnight is before the day changes so schedule update for the next start of day" |
| 140 | + ) |
| 141 | + next_update_at = dt_util.start_of_local_day(now + timedelta(days=1)) |
| 142 | + |
| 143 | + _LOGGER.info("Next update scheduled for: %s", next_update_at) |
| 144 | + |
| 145 | + self.event_unsub = async_track_point_in_time( |
| 146 | + self.hass, self.async_update, next_update_at |
| 147 | + ) |
| 148 | + |
| 149 | + async def async_update(self, *_): |
| 150 | + """Update sensors with new prayer times.""" |
| 151 | + try: |
| 152 | + prayer_times = await self.hass.async_add_executor_job( |
| 153 | + self.get_new_prayer_times |
| 154 | + ) |
| 155 | + self.available = True |
| 156 | + except (exceptions.InvalidResponseError, ConnError): |
| 157 | + self.available = False |
| 158 | + _LOGGER.debug("Error retrieving prayer times.") |
| 159 | + async_call_later(self.hass, 60, self.async_update) |
| 160 | + return |
| 161 | + |
| 162 | + for prayer, time in prayer_times.items(): |
| 163 | + self.prayer_times_info[prayer] = dt_util.parse_datetime( |
| 164 | + f"{dt_util.now().date()} {time}" |
| 165 | + ) |
| 166 | + await self.async_schedule_future_update() |
| 167 | + |
| 168 | + _LOGGER.debug("New prayer times retrieved. Updating sensors.") |
| 169 | + async_dispatcher_send(self.hass, DATA_UPDATED) |
| 170 | + |
| 171 | + async def async_setup(self): |
| 172 | + """Set up the Islamic prayer client.""" |
| 173 | + await self.async_add_options() |
| 174 | + |
| 175 | + try: |
| 176 | + await self.hass.async_add_executor_job(self.get_new_prayer_times) |
| 177 | + except (exceptions.InvalidResponseError, ConnError): |
| 178 | + raise ConfigEntryNotReady |
| 179 | + |
| 180 | + await self.async_update() |
| 181 | + self.config_entry.add_update_listener(self.async_options_updated) |
| 182 | + |
| 183 | + self.hass.async_create_task( |
| 184 | + self.hass.config_entries.async_forward_entry_setup( |
| 185 | + self.config_entry, "sensor" |
| 186 | + ) |
| 187 | + ) |
| 188 | + |
| 189 | + return True |
| 190 | + |
| 191 | + async def async_add_options(self): |
| 192 | + """Add options for entry.""" |
| 193 | + if not self.config_entry.options: |
| 194 | + data = dict(self.config_entry.data) |
| 195 | + calc_method = data.pop(CONF_CALC_METHOD, DEFAULT_CALC_METHOD) |
| 196 | + |
| 197 | + self.hass.config_entries.async_update_entry( |
| 198 | + self.config_entry, data=data, options={CONF_CALC_METHOD: calc_method} |
| 199 | + ) |
| 200 | + |
| 201 | + @staticmethod |
| 202 | + async def async_options_updated(hass, entry): |
| 203 | + """Triggered by config entry options updates.""" |
| 204 | + if hass.data[DOMAIN].event_unsub: |
| 205 | + hass.data[DOMAIN].event_unsub() |
| 206 | + await hass.data[DOMAIN].async_update() |
0 commit comments