From 2a6948e696b096cab6031febe9a0639483a57c92 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 20 Apr 2020 15:22:48 +0300 Subject: [PATCH] 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 --- CODEOWNERS | 1 + .../.translations/en.json | 23 ++ .../islamic_prayer_times/__init__.py | 205 ++++++++++++++++++ .../islamic_prayer_times/config_flow.py | 59 +++++ .../components/islamic_prayer_times/const.py | 14 ++ .../islamic_prayer_times/manifest.json | 3 +- .../components/islamic_prayer_times/sensor.py | 198 ++--------------- .../islamic_prayer_times/strings.json | 23 ++ homeassistant/generated/config_flows.py | 1 + .../islamic_prayer_times/__init__.py | 44 ++++ .../islamic_prayer_times/test_config_flow.py | 83 +++++++ .../islamic_prayer_times/test_init.py | 133 ++++++++++++ .../islamic_prayer_times/test_sensor.py | 190 ++-------------- 13 files changed, 628 insertions(+), 349 deletions(-) create mode 100644 homeassistant/components/islamic_prayer_times/.translations/en.json create mode 100644 homeassistant/components/islamic_prayer_times/config_flow.py create mode 100644 homeassistant/components/islamic_prayer_times/const.py create mode 100644 homeassistant/components/islamic_prayer_times/strings.json create mode 100644 tests/components/islamic_prayer_times/test_config_flow.py create mode 100644 tests/components/islamic_prayer_times/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 5ed043bf176..2c5749d9799 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -193,6 +193,7 @@ homeassistant/components/ipma/* @dgomes @abmantis homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 +homeassistant/components/islamic_prayer_times/* @engrbm87 homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json new file mode 100644 index 00000000000..ebbea482122 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -0,0 +1,23 @@ +{ + "title": "Islamic Prayer Times", + "config": { + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Do you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "data": { + "calc_method": "Prayer calculation method" + } + } + } + } +} diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 642c31118bd..fa676221ea3 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -1 +1,206 @@ """The islamic_prayer_times component.""" +from datetime import timedelta +import logging + +from prayer_times_calculator import PrayerTimesCalculator, exceptions +from requests.exceptions import ConnectionError as ConnError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later, async_track_point_in_time +import homeassistant.util.dt as dt_util + +from .const import ( + CALC_METHODS, + CONF_CALC_METHOD, + DATA_UPDATED, + DEFAULT_CALC_METHOD, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( + CALC_METHODS + ), + } + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Import the Islamic Prayer component from config.""" + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the Islamic Prayer Component.""" + client = IslamicPrayerClient(hass, config_entry) + + if not await client.async_setup(): + return False + + hass.data.setdefault(DOMAIN, client) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload Islamic Prayer entry from config_entry.""" + if hass.data[DOMAIN].event_unsub: + hass.data[DOMAIN].event_unsub() + hass.data.pop(DOMAIN) + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + return True + + +class IslamicPrayerClient: + """Islamic Prayer Client Object.""" + + def __init__(self, hass, config_entry): + """Initialize the Islamic Prayer client.""" + self.hass = hass + self.config_entry = config_entry + self.prayer_times_info = {} + self.available = True + self.event_unsub = None + + @property + def calc_method(self): + """Return the calculation method.""" + return self.config_entry.options[CONF_CALC_METHOD] + + def get_new_prayer_times(self): + """Fetch prayer times for today.""" + calc = PrayerTimesCalculator( + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, + calculation_method=self.calc_method, + date=str(dt_util.now().date()), + ) + return calc.fetch_prayer_times() + + async def async_schedule_future_update(self): + """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. + + 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. + + """ + _LOGGER.debug("Scheduling next update for Islamic prayer times") + + now = dt_util.as_local(dt_util.now()) + + midnight_dt = self.prayer_times_info["Midnight"] + + if now > dt_util.as_local(midnight_dt): + next_update_at = midnight_dt + timedelta(days=1, minutes=1) + _LOGGER.debug( + "Midnight is after day the 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.info("Next update scheduled for: %s", next_update_at) + + self.event_unsub = async_track_point_in_time( + self.hass, self.async_update, next_update_at + ) + + async def async_update(self, *_): + """Update sensors with new prayer times.""" + try: + prayer_times = await self.hass.async_add_executor_job( + self.get_new_prayer_times + ) + self.available = True + except (exceptions.InvalidResponseError, ConnError): + self.available = False + _LOGGER.debug("Error retrieving prayer times.") + async_call_later(self.hass, 60, self.async_update) + return + + for prayer, time in prayer_times.items(): + self.prayer_times_info[prayer] = dt_util.parse_datetime( + f"{dt_util.now().date()} {time}" + ) + await self.async_schedule_future_update() + + _LOGGER.debug("New prayer times retrieved. Updating sensors.") + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Islamic prayer client.""" + await self.async_add_options() + + try: + await self.hass.async_add_executor_job(self.get_new_prayer_times) + except (exceptions.InvalidResponseError, ConnError): + raise ConfigEntryNotReady + + await self.async_update() + self.config_entry.add_update_listener(self.async_options_updated) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + + return True + + async def async_add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + data = dict(self.config_entry.data) + calc_method = data.pop(CONF_CALC_METHOD, DEFAULT_CALC_METHOD) + + self.hass.config_entries.async_update_entry( + self.config_entry, data=data, options={CONF_CALC_METHOD: calc_method} + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + if hass.data[DOMAIN].event_unsub: + hass.data[DOMAIN].event_unsub() + await hass.data[DOMAIN].async_update() diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py new file mode 100644 index 00000000000..d45997af76f --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -0,0 +1,59 @@ +"""Config flow for Islamic Prayer Times integration.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback + +# pylint: disable=unused-import +from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME + + +class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle the Islamic Prayer config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return IslamicPrayerOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") + + if user_input is None: + return self.async_show_form(step_id="user") + + return self.async_create_entry(title=NAME, data=user_input) + + async def async_step_import(self, import_config): + """Import from config.""" + return await self.async_step_user(user_input=import_config) + + +class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Islamic Prayer client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_CALC_METHOD, + default=self.config_entry.options.get( + CONF_CALC_METHOD, DEFAULT_CALC_METHOD + ), + ): vol.In(CALC_METHODS) + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py new file mode 100644 index 00000000000..5a9007689d9 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -0,0 +1,14 @@ +"""Constants for the Islamic Prayer component.""" +DOMAIN = "islamic_prayer_times" +NAME = "Islamic Prayer Times" +SENSOR_SUFFIX = "Prayer" +PRAYER_TIMES_ICON = "mdi:calendar-clock" + +SENSOR_TYPES = ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha", "Midnight"] + +CONF_CALC_METHOD = "calc_method" + +CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] +DEFAULT_CALC_METHOD = "isna" + +DATA_UPDATED = "Islamic_prayer_data_updated" diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index da6318a0926..536e728e845 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -3,5 +3,6 @@ "name": "Islamic Prayer Times", "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": ["prayer_times_calculator==0.0.3"], - "codeowners": [] + "codeowners": ["@engrbm87"], + "config_flow": true } diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 076718e83a2..d1f4baa90bc 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,186 +1,44 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" -from datetime import datetime, timedelta import logging -from prayer_times_calculator import PrayerTimesCalculator -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time -import homeassistant.util.dt as dt_util + +from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_SUFFIX, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -PRAYER_TIMES_ICON = "mdi:calendar-clock" -SENSOR_TYPES = ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha", "midnight"] - -CONF_CALC_METHOD = "calculation_method" -CONF_SENSORS = "sensors" - -CALC_METHODS = ["karachi", "isna", "mwl", "makkah"] -DEFAULT_CALC_METHOD = "isna" -DEFAULT_SENSORS = ["fajr", "dhuhr", "asr", "maghrib", "isha"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( - CALC_METHODS - ), - vol.Optional(CONF_SENSORS, default=DEFAULT_SENSORS): vol.All( - cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)] - ), - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Islamic prayer times sensor platform.""" - latitude = hass.config.latitude - longitude = hass.config.longitude - calc_method = config.get(CONF_CALC_METHOD) - if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return + client = hass.data[DOMAIN] - prayer_times_data = IslamicPrayerTimesData(latitude, longitude, calc_method) + entities = [] + for sensor_type in SENSOR_TYPES: + entities.append(IslamicPrayerTimeSensor(sensor_type, client)) - prayer_times = prayer_times_data.get_new_prayer_times() - - sensors = [] - for sensor_type in config[CONF_SENSORS]: - sensors.append(IslamicPrayerTimeSensor(sensor_type, prayer_times_data)) - - async_add_entities(sensors, True) - - # schedule the next update for the sensors - await schedule_future_update( - hass, sensors, prayer_times["Midnight"], prayer_times_data - ) - - -async def schedule_future_update(hass, sensors, midnight_time, prayer_times_data): - """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. - - 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. - - """ - _LOGGER.debug("Scheduling next update for Islamic prayer times") - - now = dt_util.as_local(dt_util.now()) - today = now.date() - - midnight_dt_str = f"{today}::{midnight_time}" - midnight_dt = datetime.strptime(midnight_dt_str, "%Y-%m-%d::%H:%M") - - if now > dt_util.as_local(midnight_dt): - _LOGGER.debug( - "Midnight is after day the changes so schedule update " - "for after Midnight the next day" - ) - - next_update_at = midnight_dt + timedelta(days=1, minutes=1) - else: - _LOGGER.debug( - "Midnight is before the day changes so schedule update for the " - "next start of day" - ) - - tomorrow = now + timedelta(days=1) - next_update_at = dt_util.start_of_local_day(tomorrow) - - _LOGGER.debug("Next update scheduled for: %s", str(next_update_at)) - - async def update_sensors(_): - """Update sensors with new prayer times.""" - # Update prayer times - prayer_times = prayer_times_data.get_new_prayer_times() - - _LOGGER.debug("New prayer times retrieved. Updating sensors.") - - # Update all prayer times sensors - for sensor in sensors: - sensor.async_schedule_update_ha_state(True) - - # Schedule next update - await schedule_future_update( - hass, sensors, prayer_times["Midnight"], prayer_times_data - ) - - async_track_point_in_time(hass, update_sensors, next_update_at) - - -class IslamicPrayerTimesData: - """Data object for Islamic prayer times.""" - - def __init__(self, latitude, longitude, calc_method): - """Create object to hold data.""" - self.latitude = latitude - self.longitude = longitude - self.calc_method = calc_method - self.prayer_times_info = None - - def get_new_prayer_times(self): - """Fetch prayer times for today.""" - - today = datetime.today().strftime("%Y-%m-%d") - - calc = PrayerTimesCalculator( - latitude=self.latitude, - longitude=self.longitude, - calculation_method=self.calc_method, - date=str(today), - ) - - self.prayer_times_info = calc.fetch_prayer_times() - return self.prayer_times_info + async_add_entities(entities, True) class IslamicPrayerTimeSensor(Entity): """Representation of an Islamic prayer time sensor.""" - def __init__(self, sensor_type, prayer_times_data): + def __init__(self, sensor_type, client): """Initialize the Islamic prayer time sensor.""" self.sensor_type = sensor_type - self.entity_id = f"sensor.islamic_prayer_time_{self.sensor_type}" - self.prayer_times_data = prayer_times_data - self._name = self.sensor_type.capitalize() - self._device_class = DEVICE_CLASS_TIMESTAMP - prayer_time = self.prayer_times_data.prayer_times_info[self._name] - pt_dt = self.get_prayer_time_as_dt(prayer_time) - self._state = pt_dt.isoformat() + self.client = client @property def name(self): """Return the name of the sensor.""" - return self._name + return f"{self.sensor_type} {SENSOR_SUFFIX}" + + @property + def unique_id(self): + """Return the unique id of the entity.""" + return self.sensor_type @property def icon(self): @@ -190,7 +48,7 @@ class IslamicPrayerTimeSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + return self.client.prayer_times_info.get(self.sensor_type).isoformat() @property def should_poll(self): @@ -200,18 +58,10 @@ class IslamicPrayerTimeSensor(Entity): @property def device_class(self): """Return the device class.""" - return self._device_class + return DEVICE_CLASS_TIMESTAMP - @staticmethod - def get_prayer_time_as_dt(prayer_time): - """Create a datetime object for the respective prayer time.""" - today = datetime.today().strftime("%Y-%m-%d") - date_time_str = f"{today} {prayer_time}" - pt_dt = dt_util.parse_datetime(date_time_str) - return pt_dt - - async def async_update(self): - """Update the sensor.""" - prayer_time = self.prayer_times_data.prayer_times_info[self.name] - pt_dt = self.get_prayer_time_as_dt(prayer_time) - self._state = pt_dt.isoformat() + async def async_added_to_hass(self): + """Handle entity which will be added.""" + self.async_on_remove( + async_dispatcher_connect(self.hass, DATA_UPDATED, self.async_write_ha_state) + ) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json new file mode 100644 index 00000000000..ebbea482122 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -0,0 +1,23 @@ +{ + "title": "Islamic Prayer Times", + "config": { + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Do you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "data": { + "calc_method": "Prayer calculation method" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ca997c05fb5..71e9e626108 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -59,6 +59,7 @@ FLOWS = [ "ipma", "ipp", "iqvia", + "islamic_prayer_times", "izone", "konnected", "life360", diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 4a2f0002516..db25428d17a 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -1 +1,45 @@ """Tests for the islamic_prayer_times component.""" + +from datetime import datetime + +PRAYER_TIMES = { + "Fajr": "06:10", + "Sunrise": "07:25", + "Dhuhr": "12:30", + "Asr": "15:32", + "Maghrib": "17:35", + "Isha": "18:53", + "Midnight": "00:45", +} + +PRAYER_TIMES_TIMESTAMPS = { + "Fajr": datetime(2020, 1, 1, 6, 10, 0), + "Sunrise": datetime(2020, 1, 1, 7, 25, 0), + "Dhuhr": datetime(2020, 1, 1, 12, 30, 0), + "Asr": datetime(2020, 1, 1, 15, 32, 0), + "Maghrib": datetime(2020, 1, 1, 17, 35, 0), + "Isha": datetime(2020, 1, 1, 18, 53, 0), + "Midnight": datetime(2020, 1, 1, 00, 45, 0), +} + +NEW_PRAYER_TIMES = { + "Fajr": "06:00", + "Sunrise": "07:25", + "Dhuhr": "12:30", + "Asr": "15:32", + "Maghrib": "17:45", + "Isha": "18:53", + "Midnight": "00:43", +} + +NEW_PRAYER_TIMES_TIMESTAMPS = { + "Fajr": datetime(2020, 1, 1, 6, 00, 0), + "Sunrise": datetime(2020, 1, 1, 7, 25, 0), + "Dhuhr": datetime(2020, 1, 1, 12, 30, 0), + "Asr": datetime(2020, 1, 1, 15, 32, 0), + "Maghrib": datetime(2020, 1, 1, 17, 45, 0), + "Isha": datetime(2020, 1, 1, 18, 53, 0), + "Midnight": datetime(2020, 1, 1, 00, 43, 0), +} + +NOW = datetime(2020, 1, 1, 00, 00, 0) diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py new file mode 100644 index 00000000000..a56178e5225 --- /dev/null +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -0,0 +1,83 @@ +"""Tests for Islamic Prayer Times config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import islamic_prayer_times +from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_setup", autouse=True) +def mock_setup(): + """Mock entry setup.""" + with patch( + "homeassistant.components.islamic_prayer_times.async_setup_entry", + return_value=True, + ): + yield + + +async def test_flow_works(hass): + """Test user config.""" + result = await hass.config_entries.flow.async_init( + islamic_prayer_times.DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Islamic Prayer Times" + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Islamic Prayer Times", + data={}, + options={CONF_CALC_METHOD: "isna"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_CALC_METHOD: "makkah"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_CALC_METHOD] == "makkah" + + +async def test_import(hass): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + islamic_prayer_times.DOMAIN, + context={"source": "import"}, + data={CONF_CALC_METHOD: "makkah"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Islamic Prayer Times" + assert result["data"][CONF_CALC_METHOD] == "makkah" + + +async def test_integration_already_configured(hass): + """Test integration is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options={},) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + islamic_prayer_times.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py new file mode 100644 index 00000000000..e91e83b315e --- /dev/null +++ b/tests/components/islamic_prayer_times/test_init.py @@ -0,0 +1,133 @@ +"""Tests for Islamic Prayer Times init.""" + +from datetime import timedelta +from unittest.mock import patch + +from prayer_times_calculator.exceptions import InvalidResponseError + +from homeassistant import config_entries +from homeassistant.components import islamic_prayer_times +from homeassistant.setup import async_setup_component + +from . import ( + NEW_PRAYER_TIMES, + NEW_PRAYER_TIMES_TIMESTAMPS, + NOW, + PRAYER_TIMES, + PRAYER_TIMES_TIMESTAMPS, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_setup_with_config(hass): + """Test that we import the config and setup the client.""" + config = { + islamic_prayer_times.DOMAIN: {islamic_prayer_times.CONF_CALC_METHOD: "isna"} + } + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ): + assert ( + await async_setup_component(hass, islamic_prayer_times.DOMAIN, config) + is True + ) + + +async def test_successful_config_entry(hass): + """Test that Islamic Prayer Times is configured successfully.""" + + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry.add_to_hass(hass) + + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ): + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.options == { + islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD + } + + +async def test_setup_failed(hass): + """Test Islamic Prayer Times failed due to an error.""" + + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry.add_to_hass(hass) + + # test request error raising ConfigEntryNotReady + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + side_effect=InvalidResponseError(), + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass): + """Test removing Islamic Prayer Times.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry.add_to_hass(hass) + + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ): + await hass.config_entries.async_setup(entry.entry_id) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert islamic_prayer_times.DOMAIN not in hass.data + + +async def test_islamic_prayer_times_timestamp_format(hass): + """Test Islamic prayer times timestamp format.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) + + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), patch("homeassistant.util.dt.now", return_value=NOW): + + await hass.config_entries.async_setup(entry.entry_id) + + assert ( + hass.data[islamic_prayer_times.DOMAIN].prayer_times_info + == PRAYER_TIMES_TIMESTAMPS + ) + + +async def test_update(hass): + """Test sensors are updated with new prayer times.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) + + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times" + ) as FetchPrayerTimes, patch("homeassistant.util.dt.now", return_value=NOW): + FetchPrayerTimes.side_effect = [ + PRAYER_TIMES, + PRAYER_TIMES, + NEW_PRAYER_TIMES, + ] + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + pt_data = hass.data[islamic_prayer_times.DOMAIN] + assert pt_data.prayer_times_info == PRAYER_TIMES_TIMESTAMPS + + future = pt_data.prayer_times_info["Midnight"] + timedelta(days=1, minutes=1) + + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert ( + hass.data[islamic_prayer_times.DOMAIN].prayer_times_info + == NEW_PRAYER_TIMES_TIMESTAMPS + ) diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 3151b030637..4954287b864 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,186 +1,28 @@ """The tests for the Islamic prayer times sensor platform.""" -from datetime import datetime, timedelta from unittest.mock import patch -from homeassistant.components.islamic_prayer_times.sensor import IslamicPrayerTimesData -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.components import islamic_prayer_times -from tests.common import async_fire_time_changed +from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS -LATITUDE = 41 -LONGITUDE = -87 -CALC_METHOD = "isna" -PRAYER_TIMES = { - "Fajr": "06:10", - "Sunrise": "07:25", - "Dhuhr": "12:30", - "Asr": "15:32", - "Maghrib": "17:35", - "Isha": "18:53", - "Midnight": "00:45", -} -ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" +from tests.common import MockConfigEntry -def get_prayer_time_as_dt(prayer_time): - """Create a datetime object for the respective prayer time.""" - today = datetime.today().strftime("%Y-%m-%d") - date_time_str = "{} {}".format(str(today), prayer_time) - pt_dt = dt_util.parse_datetime(date_time_str) - return pt_dt - - -async def test_islamic_prayer_times_min_config(hass): +async def test_islamic_prayer_times_sensors(hass): """Test minimum Islamic prayer times configuration.""" - min_config_sensors = ["fajr", "dhuhr", "asr", "maghrib", "isha"] + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) with patch( - "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" - ) as PrayerTimesCalculator: - PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( - PRAYER_TIMES - ) + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), patch("homeassistant.util.dt.now", return_value=NOW): - config = {"sensor": {"platform": "islamic_prayer_times"}} - assert await async_setup_component(hass, "sensor", config) is True + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - for sensor in min_config_sensors: - entity_id = ENTITY_ID_FORMAT.format(sensor) - entity_id_name = sensor.capitalize() - pt_dt = get_prayer_time_as_dt(PRAYER_TIMES[entity_id_name]) - state = hass.states.get(entity_id) - assert state.state == pt_dt.isoformat() - assert state.name == entity_id_name - - -async def test_islamic_prayer_times_multiple_sensors(hass): - """Test Islamic prayer times sensor with multiple sensors setup.""" - multiple_sensors = [ - "fajr", - "sunrise", - "dhuhr", - "asr", - "maghrib", - "isha", - "midnight", - ] - - with patch( - "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" - ) as PrayerTimesCalculator: - PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( - PRAYER_TIMES - ) - - config = { - "sensor": {"platform": "islamic_prayer_times", "sensors": multiple_sensors} - } - - assert await async_setup_component(hass, "sensor", config) is True - - for sensor in multiple_sensors: - entity_id = ENTITY_ID_FORMAT.format(sensor) - entity_id_name = sensor.capitalize() - pt_dt = get_prayer_time_as_dt(PRAYER_TIMES[entity_id_name]) - state = hass.states.get(entity_id) - assert state.state == pt_dt.isoformat() - assert state.name == entity_id_name - - -async def test_islamic_prayer_times_with_calculation_method(hass): - """Test Islamic prayer times configuration with calculation method.""" - sensors = ["fajr", "maghrib"] - - with patch( - "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" - ) as PrayerTimesCalculator: - PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( - PRAYER_TIMES - ) - - config = { - "sensor": { - "platform": "islamic_prayer_times", - "calculation_method": "mwl", - "sensors": sensors, - } - } - - assert await async_setup_component(hass, "sensor", config) is True - - for sensor in sensors: - entity_id = ENTITY_ID_FORMAT.format(sensor) - entity_id_name = sensor.capitalize() - pt_dt = get_prayer_time_as_dt(PRAYER_TIMES[entity_id_name]) - state = hass.states.get(entity_id) - assert state.state == pt_dt.isoformat() - assert state.name == entity_id_name - - -async def test_islamic_prayer_times_data_get_prayer_times(hass): - """Test Islamic prayer times data fetcher.""" - with patch( - "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" - ) as PrayerTimesCalculator: - PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( - PRAYER_TIMES - ) - - pt_data = IslamicPrayerTimesData( - latitude=LATITUDE, longitude=LONGITUDE, calc_method=CALC_METHOD - ) - - assert pt_data.get_new_prayer_times() == PRAYER_TIMES - assert pt_data.prayer_times_info == PRAYER_TIMES - - -async def test_islamic_prayer_times_sensor_update(hass): - """Test Islamic prayer times sensor update.""" - new_prayer_times = { - "Fajr": "06:10", - "Sunrise": "07:25", - "Dhuhr": "12:30", - "Asr": "15:32", - "Maghrib": "17:45", - "Isha": "18:53", - "Midnight": "00:45", - } - - with patch( - "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" - ) as PrayerTimesCalculator: - PrayerTimesCalculator.return_value.fetch_prayer_times.side_effect = [ - PRAYER_TIMES, - new_prayer_times, - ] - - config = { - "sensor": {"platform": "islamic_prayer_times", "sensors": ["maghrib"]} - } - - assert await async_setup_component(hass, "sensor", config) - - entity_id = "sensor.islamic_prayer_time_maghrib" - pt_dt = get_prayer_time_as_dt(PRAYER_TIMES["Maghrib"]) - state = hass.states.get(entity_id) - assert state.state == pt_dt.isoformat() - - midnight = PRAYER_TIMES["Midnight"] - now = dt_util.as_local(dt_util.now()) - today = now.date() - - midnight_dt_str = "{}::{}".format(str(today), midnight) - midnight_dt = datetime.strptime(midnight_dt_str, "%Y-%m-%d::%H:%M") - future = midnight_dt + timedelta(days=1, minutes=1) - - with patch( - "homeassistant.components.islamic_prayer_times.sensor.dt_util.utcnow", - return_value=future, - ): - - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - pt_dt = get_prayer_time_as_dt(new_prayer_times["Maghrib"]) - assert state.state == pt_dt.isoformat() + for prayer in PRAYER_TIMES: + assert ( + hass.states.get(f"sensor.{prayer}_prayer").state + == PRAYER_TIMES_TIMESTAMPS[prayer].isoformat() + )