Fix stale prayer times from islamic-prayer-times
(#115683)
This commit is contained in:
parent
822646749d
commit
7184543f12
4 changed files with 131 additions and 55 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
|
@ -70,8 +70,8 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim
|
|||
"""Return the school."""
|
||||
return self.config_entry.options.get(CONF_SCHOOL, DEFAULT_SCHOOL)
|
||||
|
||||
def get_new_prayer_times(self) -> dict[str, Any]:
|
||||
"""Fetch prayer times for today."""
|
||||
def get_new_prayer_times(self, for_date: date) -> dict[str, Any]:
|
||||
"""Fetch prayer times for the specified date."""
|
||||
calc = PrayerTimesCalculator(
|
||||
latitude=self.latitude,
|
||||
longitude=self.longitude,
|
||||
|
@ -79,7 +79,7 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim
|
|||
latitudeAdjustmentMethod=self.lat_adj_method,
|
||||
midnightMode=self.midnight_mode,
|
||||
school=self.school,
|
||||
date=str(dt_util.now().date()),
|
||||
date=str(for_date),
|
||||
iso8601=True,
|
||||
)
|
||||
return cast(dict[str, Any], calc.fetch_prayer_times())
|
||||
|
@ -88,51 +88,18 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim
|
|||
def async_schedule_future_update(self, midnight_dt: datetime) -> None:
|
||||
"""Schedule future update for sensors.
|
||||
|
||||
Midnight is a calculated time. The specifics of the calculation
|
||||
depends on the method of the prayer time calculation. This calculated
|
||||
midnight is the time at which the time to pray the Isha prayers have
|
||||
expired.
|
||||
The least surprising behaviour is to load the next day's prayer times only
|
||||
after the current day's prayers are complete. We will take the fiqhi opinion
|
||||
that Isha should be prayed before Islamic midnight (which may be before or after 12:00 midnight),
|
||||
and thus we will switch to the next day's timings at Islamic midnight.
|
||||
|
||||
Calculated Midnight: The Islamic midnight.
|
||||
Traditional Midnight: 12:00AM
|
||||
|
||||
Update logic for prayer times:
|
||||
|
||||
If the Calculated Midnight is before the traditional midnight then wait
|
||||
until the traditional midnight to run the update. This way the day
|
||||
will have changed over and we don't need to do any fancy calculations.
|
||||
|
||||
If the Calculated Midnight is after the traditional midnight, then wait
|
||||
until after the calculated Midnight. We don't want to update the prayer
|
||||
times too early or else the timings might be incorrect.
|
||||
|
||||
Example:
|
||||
calculated midnight = 11:23PM (before traditional midnight)
|
||||
Update time: 12:00AM
|
||||
|
||||
calculated midnight = 1:35AM (after traditional midnight)
|
||||
update time: 1:36AM.
|
||||
The +1s is to ensure that any automations predicated on the arrival of Islamic midnight will run.
|
||||
|
||||
"""
|
||||
_LOGGER.debug("Scheduling next update for Islamic prayer times")
|
||||
|
||||
now = dt_util.utcnow()
|
||||
|
||||
if now > midnight_dt:
|
||||
next_update_at = midnight_dt + timedelta(days=1, minutes=1)
|
||||
_LOGGER.debug(
|
||||
"Midnight is after the day changes so schedule update for after Midnight the next day"
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Midnight is before the day changes so schedule update for the next start of day"
|
||||
)
|
||||
next_update_at = dt_util.start_of_local_day(now + timedelta(days=1))
|
||||
|
||||
_LOGGER.debug("Next update scheduled for: %s", next_update_at)
|
||||
|
||||
self.event_unsub = async_track_point_in_time(
|
||||
self.hass, self.async_request_update, next_update_at
|
||||
self.hass, self.async_request_update, midnight_dt + timedelta(seconds=1)
|
||||
)
|
||||
|
||||
async def async_request_update(self, _: datetime) -> None:
|
||||
|
@ -140,8 +107,34 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim
|
|||
await self.async_request_refresh()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, datetime]:
|
||||
"""Update sensors with new prayer times."""
|
||||
prayer_times = self.get_new_prayer_times()
|
||||
"""Update sensors with new prayer times.
|
||||
|
||||
Prayer time calculations "roll over" at 12:00 midnight - but this does not mean that all prayers
|
||||
occur within that Gregorian calendar day. For instance Jasper, Alta. sees Isha occur after 00:00 in the summer.
|
||||
It is similarly possible (albeit less likely) that Fajr occurs before 00:00.
|
||||
|
||||
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),
|
||||
we calculate 3 days' worth of times (-1, 0, +1 days) and select the appropriate set based on Islamic midnight.
|
||||
|
||||
The calculation is inexpensive, so there is no need to cache it.
|
||||
"""
|
||||
|
||||
# Zero out the us component to maintain consistent rollover at T+1s
|
||||
now = dt_util.now().replace(microsecond=0)
|
||||
yesterday_times = self.get_new_prayer_times((now - timedelta(days=1)).date())
|
||||
today_times = self.get_new_prayer_times(now.date())
|
||||
tomorrow_times = self.get_new_prayer_times((now + timedelta(days=1)).date())
|
||||
|
||||
if (
|
||||
yesterday_midnight := dt_util.parse_datetime(yesterday_times["Midnight"])
|
||||
) and now <= yesterday_midnight:
|
||||
prayer_times = yesterday_times
|
||||
elif (
|
||||
tomorrow_midnight := dt_util.parse_datetime(today_times["Midnight"])
|
||||
) and now > tomorrow_midnight:
|
||||
prayer_times = tomorrow_times
|
||||
else:
|
||||
prayer_times = today_times
|
||||
|
||||
# introduced in prayer-times-calculator 0.0.8
|
||||
prayer_times.pop("date", None)
|
||||
|
|
|
@ -12,6 +12,17 @@ MOCK_USER_INPUT = {
|
|||
|
||||
MOCK_CONFIG = {CONF_LATITUDE: 12.34, CONF_LONGITUDE: 23.45}
|
||||
|
||||
|
||||
PRAYER_TIMES_YESTERDAY = {
|
||||
"Fajr": "2019-12-31T06:09:00+00:00",
|
||||
"Sunrise": "2019-12-31T07:24:00+00:00",
|
||||
"Dhuhr": "2019-12-31T12:29:00+00:00",
|
||||
"Asr": "2019-12-31T15:31:00+00:00",
|
||||
"Maghrib": "2019-12-31T17:34:00+00:00",
|
||||
"Isha": "2019-12-31T18:52:00+00:00",
|
||||
"Midnight": "2020-01-01T00:44:00+00:00",
|
||||
}
|
||||
|
||||
PRAYER_TIMES = {
|
||||
"Fajr": "2020-01-01T06:10:00+00:00",
|
||||
"Sunrise": "2020-01-01T07:25:00+00:00",
|
||||
|
@ -19,7 +30,17 @@ PRAYER_TIMES = {
|
|||
"Asr": "2020-01-01T15:32:00+00:00",
|
||||
"Maghrib": "2020-01-01T17:35:00+00:00",
|
||||
"Isha": "2020-01-01T18:53:00+00:00",
|
||||
"Midnight": "2020-01-01T00:45:00+00:00",
|
||||
"Midnight": "2020-01-02T00:45:00+00:00",
|
||||
}
|
||||
|
||||
PRAYER_TIMES_TOMORROW = {
|
||||
"Fajr": "2020-01-02T06:11:00+00:00",
|
||||
"Sunrise": "2020-01-02T07:26:00+00:00",
|
||||
"Dhuhr": "2020-01-02T12:31:00+00:00",
|
||||
"Asr": "2020-01-02T15:33:00+00:00",
|
||||
"Maghrib": "2020-01-02T17:36:00+00:00",
|
||||
"Isha": "2020-01-02T18:54:00+00:00",
|
||||
"Midnight": "2020-01-03T00:46:00+00:00",
|
||||
}
|
||||
|
||||
NOW = datetime(2020, 1, 1, 00, 00, 0, tzinfo=dt_util.UTC)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for Islamic Prayer Times init."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
@ -12,10 +13,11 @@ from homeassistant.config_entries import ConfigEntryState
|
|||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import NOW, PRAYER_TIMES
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -76,13 +78,16 @@ async def test_options_listener(hass: HomeAssistant) -> None:
|
|||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_fetch_prayer_times.call_count == 1
|
||||
# Each scheduling run calls this 3 times (yesterday, today, tomorrow)
|
||||
assert mock_fetch_prayer_times.call_count == 3
|
||||
mock_fetch_prayer_times.reset_mock()
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, options={CONF_CALC_METHOD: "makkah"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_fetch_prayer_times.call_count == 2
|
||||
# Each scheduling run calls this 3 times (yesterday, today, tomorrow)
|
||||
assert mock_fetch_prayer_times.call_count == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -155,3 +160,41 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None:
|
|||
CONF_LONGITUDE: hass.config.longitude,
|
||||
}
|
||||
assert entry.minor_version == 2
|
||||
|
||||
|
||||
async def test_update_scheduling(hass: HomeAssistant) -> None:
|
||||
"""Test that integration schedules update immediately after Islamic midnight."""
|
||||
entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times",
|
||||
return_value=PRAYER_TIMES,
|
||||
),
|
||||
freeze_time(NOW),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch(
|
||||
"prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times",
|
||||
return_value=PRAYER_TIMES,
|
||||
) as mock_fetch_prayer_times:
|
||||
midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"])
|
||||
assert midnight_time
|
||||
with freeze_time(midnight_time):
|
||||
async_fire_time_changed(hass, midnight_time)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_fetch_prayer_times.assert_not_called()
|
||||
|
||||
midnight_time += timedelta(seconds=1)
|
||||
with freeze_time(midnight_time):
|
||||
async_fire_time_changed(hass, midnight_time)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Each scheduling run calls this 3 times (yesterday, today, tomorrow)
|
||||
assert mock_fetch_prayer_times.call_count == 3
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""The tests for the Islamic prayer times sensor platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
@ -8,7 +9,7 @@ import pytest
|
|||
from homeassistant.components.islamic_prayer_times.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import NOW, PRAYER_TIMES
|
||||
from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TOMORROW, PRAYER_TIMES_YESTERDAY
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -31,20 +32,38 @@ def set_utc(hass: HomeAssistant) -> None:
|
|||
("Midnight", "sensor.islamic_prayer_times_midnight_time"),
|
||||
],
|
||||
)
|
||||
# In our example data, Islamic midnight occurs at 00:44 (yesterday's times, occurs today) and 00:45 (today's times, occurs tomorrow),
|
||||
# hence we check that the times roll over at exactly the desired minute
|
||||
@pytest.mark.parametrize(
|
||||
("offset", "prayer_times"),
|
||||
[
|
||||
(timedelta(days=-1), PRAYER_TIMES_YESTERDAY),
|
||||
(timedelta(minutes=44), PRAYER_TIMES_YESTERDAY),
|
||||
(timedelta(minutes=44, seconds=1), PRAYER_TIMES), # Rolls over at 00:44 + 1 sec
|
||||
(timedelta(days=1, minutes=45), PRAYER_TIMES),
|
||||
(
|
||||
timedelta(days=1, minutes=45, seconds=1), # Rolls over at 00:45 + 1 sec
|
||||
PRAYER_TIMES_TOMORROW,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_islamic_prayer_times_sensors(
|
||||
hass: HomeAssistant, key: str, sensor_name: str
|
||||
hass: HomeAssistant,
|
||||
key: str,
|
||||
sensor_name: str,
|
||||
offset: timedelta,
|
||||
prayer_times: dict[str, str],
|
||||
) -> None:
|
||||
"""Test minimum Islamic prayer times configuration."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times",
|
||||
return_value=PRAYER_TIMES,
|
||||
side_effect=(PRAYER_TIMES_YESTERDAY, PRAYER_TIMES, PRAYER_TIMES_TOMORROW),
|
||||
),
|
||||
freeze_time(NOW),
|
||||
freeze_time(NOW + offset),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(sensor_name).state == PRAYER_TIMES[key]
|
||||
assert hass.states.get(sensor_name).state == prayer_times[key]
|
||||
|
|
Loading…
Add table
Reference in a new issue