Skip to content

Commit 2a6948e

Browse files
authored
Add Islamic Prayer Times config flow (#31590)
* Add Islamic Prayer Times config_flow * Add Islamic Prayer Times config_flow * handle options update and fix tests * fix sensor update handling * fix pylint * fix scheduled update and add test * update test_init * update flow options to show drop list * clean up code * async scheduling and revert state to timestamp * fix update retry method * update strings * keep title as root key
1 parent 1b4851f commit 2a6948e

File tree

13 files changed

+631
-352
lines changed

13 files changed

+631
-352
lines changed

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ homeassistant/components/ipma/* @dgomes @abmantis
193193
homeassistant/components/ipp/* @ctalkington
194194
homeassistant/components/iqvia/* @bachya
195195
homeassistant/components/irish_rail_transport/* @ttroy50
196+
homeassistant/components/islamic_prayer_times/* @engrbm87
196197
homeassistant/components/izone/* @Swamp-Ig
197198
homeassistant/components/jewish_calendar/* @tsvi
198199
homeassistant/components/juicenet/* @jesserockz
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Islamic Prayer Times",
3+
"config": {
4+
"step": {
5+
"user": {
6+
"title": "Set up Islamic Prayer Times",
7+
"description": "Do you want to set up Islamic Prayer Times?"
8+
}
9+
},
10+
"abort": {
11+
"one_instance_allowed": "Only a single instance is necessary."
12+
}
13+
},
14+
"options": {
15+
"step": {
16+
"init": {
17+
"data": {
18+
"calc_method": "Prayer calculation method"
19+
}
20+
}
21+
}
22+
}
23+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,206 @@
11
"""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()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Config flow for Islamic Prayer Times integration."""
2+
import voluptuous as vol
3+
4+
from homeassistant import config_entries
5+
from homeassistant.core import callback
6+
7+
# pylint: disable=unused-import
8+
from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME
9+
10+
11+
class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
12+
"""Handle the Islamic Prayer config flow."""
13+
14+
VERSION = 1
15+
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
16+
17+
@staticmethod
18+
@callback
19+
def async_get_options_flow(config_entry):
20+
"""Get the options flow for this handler."""
21+
return IslamicPrayerOptionsFlowHandler(config_entry)
22+
23+
async def async_step_user(self, user_input=None):
24+
"""Handle a flow initialized by the user."""
25+
if self._async_current_entries():
26+
return self.async_abort(reason="one_instance_allowed")
27+
28+
if user_input is None:
29+
return self.async_show_form(step_id="user")
30+
31+
return self.async_create_entry(title=NAME, data=user_input)
32+
33+
async def async_step_import(self, import_config):
34+
"""Import from config."""
35+
return await self.async_step_user(user_input=import_config)
36+
37+
38+
class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow):
39+
"""Handle Islamic Prayer client options."""
40+
41+
def __init__(self, config_entry):
42+
"""Initialize options flow."""
43+
self.config_entry = config_entry
44+
45+
async def async_step_init(self, user_input=None):
46+
"""Manage options."""
47+
if user_input is not None:
48+
return self.async_create_entry(title="", data=user_input)
49+
50+
options = {
51+
vol.Optional(
52+
CONF_CALC_METHOD,
53+
default=self.config_entry.options.get(
54+
CONF_CALC_METHOD, DEFAULT_CALC_METHOD
55+
),
56+
): vol.In(CALC_METHODS)
57+
}
58+
59+
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Constants for the Islamic Prayer component."""
2+
DOMAIN = "islamic_prayer_times"
3+
NAME = "Islamic Prayer Times"
4+
SENSOR_SUFFIX = "Prayer"
5+
PRAYER_TIMES_ICON = "mdi:calendar-clock"
6+
7+
SENSOR_TYPES = ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha", "Midnight"]
8+
9+
CONF_CALC_METHOD = "calc_method"
10+
11+
CALC_METHODS = ["isna", "karachi", "mwl", "makkah"]
12+
DEFAULT_CALC_METHOD = "isna"
13+
14+
DATA_UPDATED = "Islamic_prayer_data_updated"

homeassistant/components/islamic_prayer_times/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"name": "Islamic Prayer Times",
44
"documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times",
55
"requirements": ["prayer_times_calculator==0.0.3"],
6-
"codeowners": []
6+
"codeowners": ["@engrbm87"],
7+
"config_flow": true
78
}

0 commit comments

Comments
 (0)