From b41c5666a5d24bb7b9ea1390c66c8aa036420d0b Mon Sep 17 00:00:00 2001 From: Rami Date: Wed, 13 Nov 2019 11:05:19 +0200 Subject: [PATCH 01/15] Add Islamic Prayer Times config_flow --- CODEOWNERS | 1 + .../.translations/en.json | 27 +++ .../islamic_prayer_times/EXAMPLE___init__.py | 49 +++++ .../islamic_prayer_times/__init__.py | 201 ++++++++++++++++++ .../islamic_prayer_times/config_flow.py | 66 ++++++ .../components/islamic_prayer_times/const.py | 14 ++ .../islamic_prayer_times/manifest.json | 7 +- .../components/islamic_prayer_times/sensor.py | 195 +++-------------- .../islamic_prayer_times/strings.json | 27 +++ homeassistant/generated/config_flows.py | 1 + .../islamic_prayer_times/test_config_flow.py | 71 +++++++ .../islamic_prayer_times/test_init.py | 73 +++++++ 12 files changed, 565 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/islamic_prayer_times/.translations/en.json create mode 100644 homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py 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 879a1c8f55d01..5ebb3bfbd4e1c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes 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 0000000000000..f70d5126620a1 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Islamic Prayer Times", + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Are you sure you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "title": "Configure options for Islamic Prayer Times", + "step": { + "init": { + "data": { + "calculation_method": "Prayer calculation method" + } + } + }, + "error": { + "wrong_method": "possible values [karachi, isna, mwl, makkah]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py b/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py new file mode 100644 index 0000000000000..c8fbd4a9fcba7 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py @@ -0,0 +1,49 @@ +"""The Islamic prayer times integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Islamic prayer times component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Islamic prayer times from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 642c31118bd4f..a3646484dd8bd 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -1 +1,202 @@ """The islamic_prayer_times component.""" +from datetime import datetime, timedelta +import logging + +from prayer_times_calculator import PrayerTimesCalculator, exceptions + + +import voluptuous as vol +import homeassistant.util.dt as dt_util + +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_track_point_in_time + +from .const import ( + DOMAIN, + CONF_CALC_METHOD, + DEFAULT_CALC_METHOD, + CALC_METHODS, + DATA_UPDATED, +) + +_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) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN] = client + + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload Transmission Entry from config_entry.""" + + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + hass.data.pop(DOMAIN) + + 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 = None + self.available = None + + async 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.config_entry.options[CONF_CALC_METHOD], + date=str(dt_util.now().date()), + ) + self.prayer_times_info = calc.fetch_prayer_times() + + async def 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") + + midnight_time = self.prayer_times_info["Midnight"] + now = dt_util.as_local(dt_util.now()) + today = now.date() + + midnight_dt_str = "{}::{}".format(str(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_track_point_in_time(self.hass, self.async_update, next_update_at) + + async def async_update(self): + """Update sensors with new prayer times.""" + try: + await self.get_new_prayer_times() + await self.schedule_future_update() + self.available = True + _LOGGER.debug("New prayer times retrieved. Updating sensors.") + + except exceptions.InvalidResponseError: + self.available = False + + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Islamic prayer client.""" + + self.add_options() + + try: + await self.get_new_prayer_times() + except exceptions.InvalidResponseError: + raise ConfigEntryNotReady + + await self.async_update() + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + + return True + + def add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + calc_method = self.config_entry.data.get( + CONF_CALC_METHOD, DEFAULT_CALC_METHOD + ) + + self.hass.config_entries.async_update_entry( + self.config_entry, options={CONF_CALC_METHOD: calc_method} + ) + + @staticmethod + def get_prayer_time_as_dt(prayer_time): + """Create a datetime object for the respective prayer time.""" + today = dt_util.now().date() + date_time_str = "{} {}".format(str(today), prayer_time) + pt_dt = dt_util.parse_datetime(date_time_str) + return pt_dt 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 0000000000000..a7890fb7c17c0 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -0,0 +1,66 @@ +"""Config flow for Islamic Prayer Times integration.""" +import voluptuous as vol + +from homeassistant import config_entries + +from homeassistant.core import callback + +from .const import CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, CALC_METHODS, 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 Transmission client 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.""" + errors = {} + + if user_input is not None: + if user_input[CONF_CALC_METHOD] in CALC_METHODS: + return self.async_create_entry(title="", data=user_input) + else: + errors[CONF_CALC_METHOD] = "wrong_method" + + options = { + vol.Optional( + CONF_CALC_METHOD, + default=self.config_entry.options.get( + CONF_CALC_METHOD, DEFAULT_CALC_METHOD + ), + ): str + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options), errors=errors + ) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py new file mode 100644 index 0000000000000..a593352da3b0f --- /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" +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" + +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 035b61d0f2d1b..b24455d24ae76 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -6,5 +6,8 @@ "prayer_times_calculator==0.0.3" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 88cbd2cb4319e..56f0689d42a91 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,166 +1,32 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" import logging -from datetime import datetime, timedelta -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time +from .const import ( + DOMAIN, + PRAYER_TIMES_ICON, + 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): - """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 - - prayer_times_data = IslamicPrayerTimesData(latitude, longitude, calc_method) - - 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 + """Import config from configuration.yaml.""" + pass - 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 = "{}::{}".format(str(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.""" - from prayer_times_calculator import PrayerTimesCalculator +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Islamic prayer times sensor platform.""" - today = datetime.today().strftime("%Y-%m-%d") + client = hass.data[DOMAIN] - calc = PrayerTimesCalculator( - latitude=self.latitude, - longitude=self.longitude, - calculation_method=self.calc_method, - date=str(today), - ) + dev = [] + for sensor_type in SENSOR_TYPES: + dev.append(IslamicPrayerTimeSensor(sensor_type, client)) - self.prayer_times_info = calc.fetch_prayer_times() - return self.prayer_times_info + async_add_entities(dev, True) class IslamicPrayerTimeSensor(Entity): @@ -168,22 +34,24 @@ class IslamicPrayerTimeSensor(Entity): ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" - 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 = self.ENTITY_ID_FORMAT.format(self.sensor_type) - self.prayer_times_data = prayer_times_data + self.client = client 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._state = None @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the entity.""" + return self.entity_id + @property def icon(self): """Icon to display in the front end.""" @@ -194,6 +62,11 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.client.available + @property def should_poll(self): """Disable polling.""" @@ -202,18 +75,10 @@ def should_poll(self): @property def device_class(self): """Return the device class.""" - return self._device_class - - @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 = "{} {}".format(str(today), prayer_time) - pt_dt = dt_util.parse_datetime(date_time_str) - return pt_dt + return DEVICE_CLASS_TIMESTAMP 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) + prayer_time = self.client.prayer_times_info[self.name] + pt_dt = self.client.get_prayer_time_as_dt(prayer_time) self._state = pt_dt.isoformat() diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json new file mode 100644 index 0000000000000..3152f3a58dc6c --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Islamic Prayer Times", + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Are you sure you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "title": "Configure options for Islamic Prayer Times", + "step": { + "init": { + "data": { + "calculation_method": "Prayer calculation method" + } + } + }, + "error": { + "wrong_method": "possible values [karachi, isna, mwl, makkah]" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 519df86f5e9e8..d885f9b02d62f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -37,6 +37,7 @@ "ios", "ipma", "iqvia", + "islamic_prayer_times", "izone", "life360", "lifx", 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 0000000000000..2850e7fb52284 --- /dev/null +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -0,0 +1,71 @@ +"""Tests for Islamic Prayer Times config flow.""" +from tests.common import MockConfigEntry + +from homeassistant.components.islamic_prayer_times import config_flow +from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN + + +async def test_flow_works(hass): + """Test user config.""" + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form", result + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert 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"}, + ) + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await options_flow.async_step_init({CONF_CALC_METHOD: "makkah"}) + assert result["type"] == "create_entry" + assert result["data"][CONF_CALC_METHOD] == "makkah" + + # Test calc_method is wrong + + result = await options_flow.async_step_init({CONF_CALC_METHOD: "bla"}) + + assert result["type"] == "form" + assert result["errors"] == {CONF_CALC_METHOD: "wrong_method"} + + +async def test_import(hass): + """Test import step.""" + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({CONF_CALC_METHOD: "makkah"}) + assert 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) + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + + assert 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 0000000000000..0111d3e6d5b33 --- /dev/null +++ b/tests/components/islamic_prayer_times/test_init.py @@ -0,0 +1,73 @@ +"""Tests for Islamic Prayer Times init.""" + +from unittest.mock import patch + +import pytest +import prayer_times_calculator + +from tests.common import MockConfigEntry, mock_coro + +from homeassistant.components import islamic_prayer_times +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + + +MOCK_ENTRY = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Islamic Prayer Times.""" + assert await async_setup_component(hass, islamic_prayer_times.DOMAIN, {}) is True + assert islamic_prayer_times.DOMAIN not in hass.data + + +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"} + } + 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 = MOCK_ENTRY + entry.add_to_hass(hass) + + assert await islamic_prayer_times.async_setup_entry(hass, entry) is True + 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 = MOCK_ENTRY + entry.add_to_hass(hass) + + # test request error raising ConfigEntryNotReady + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + side_effect=prayer_times_calculator.exceptions.InvalidResponseError(), + ), pytest.raises(ConfigEntryNotReady): + + await islamic_prayer_times.async_setup_entry(hass, entry) + + +async def test_unload_entry(hass): + """Test removing Islamic Prayer Times.""" + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry: + assert await islamic_prayer_times.async_setup_entry(hass, entry) + + assert await islamic_prayer_times.async_unload_entry(hass, entry) + assert unload_entry.call_count == 1 + assert islamic_prayer_times.DOMAIN not in hass.data From 211b60a5b0b22ddbbb5408a80c44226cd47d93fe Mon Sep 17 00:00:00 2001 From: Rami Date: Wed, 13 Nov 2019 11:05:19 +0200 Subject: [PATCH 02/15] Add Islamic Prayer Times config_flow --- CODEOWNERS | 1 + .../.translations/en.json | 27 +++ .../islamic_prayer_times/EXAMPLE___init__.py | 49 +++++ .../islamic_prayer_times/__init__.py | 201 ++++++++++++++++++ .../islamic_prayer_times/config_flow.py | 66 ++++++ .../components/islamic_prayer_times/const.py | 14 ++ .../islamic_prayer_times/manifest.json | 7 +- .../components/islamic_prayer_times/sensor.py | 195 +++-------------- .../islamic_prayer_times/strings.json | 27 +++ homeassistant/generated/config_flows.py | 1 + .../islamic_prayer_times/test_config_flow.py | 71 +++++++ .../islamic_prayer_times/test_init.py | 73 +++++++ 12 files changed, 565 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/islamic_prayer_times/.translations/en.json create mode 100644 homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py 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 4df7f250d606b..bd0d68488cc83 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -174,6 +174,7 @@ homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes @abmantis 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 0000000000000..f70d5126620a1 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Islamic Prayer Times", + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Are you sure you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "title": "Configure options for Islamic Prayer Times", + "step": { + "init": { + "data": { + "calculation_method": "Prayer calculation method" + } + } + }, + "error": { + "wrong_method": "possible values [karachi, isna, mwl, makkah]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py b/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py new file mode 100644 index 0000000000000..c8fbd4a9fcba7 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py @@ -0,0 +1,49 @@ +"""The Islamic prayer times integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Islamic prayer times component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Islamic prayer times from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 642c31118bd4f..a3646484dd8bd 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -1 +1,202 @@ """The islamic_prayer_times component.""" +from datetime import datetime, timedelta +import logging + +from prayer_times_calculator import PrayerTimesCalculator, exceptions + + +import voluptuous as vol +import homeassistant.util.dt as dt_util + +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_track_point_in_time + +from .const import ( + DOMAIN, + CONF_CALC_METHOD, + DEFAULT_CALC_METHOD, + CALC_METHODS, + DATA_UPDATED, +) + +_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) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN] = client + + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload Transmission Entry from config_entry.""" + + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + + hass.data.pop(DOMAIN) + + 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 = None + self.available = None + + async 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.config_entry.options[CONF_CALC_METHOD], + date=str(dt_util.now().date()), + ) + self.prayer_times_info = calc.fetch_prayer_times() + + async def 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") + + midnight_time = self.prayer_times_info["Midnight"] + now = dt_util.as_local(dt_util.now()) + today = now.date() + + midnight_dt_str = "{}::{}".format(str(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_track_point_in_time(self.hass, self.async_update, next_update_at) + + async def async_update(self): + """Update sensors with new prayer times.""" + try: + await self.get_new_prayer_times() + await self.schedule_future_update() + self.available = True + _LOGGER.debug("New prayer times retrieved. Updating sensors.") + + except exceptions.InvalidResponseError: + self.available = False + + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Islamic prayer client.""" + + self.add_options() + + try: + await self.get_new_prayer_times() + except exceptions.InvalidResponseError: + raise ConfigEntryNotReady + + await self.async_update() + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + + return True + + def add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + calc_method = self.config_entry.data.get( + CONF_CALC_METHOD, DEFAULT_CALC_METHOD + ) + + self.hass.config_entries.async_update_entry( + self.config_entry, options={CONF_CALC_METHOD: calc_method} + ) + + @staticmethod + def get_prayer_time_as_dt(prayer_time): + """Create a datetime object for the respective prayer time.""" + today = dt_util.now().date() + date_time_str = "{} {}".format(str(today), prayer_time) + pt_dt = dt_util.parse_datetime(date_time_str) + return pt_dt 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 0000000000000..a7890fb7c17c0 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -0,0 +1,66 @@ +"""Config flow for Islamic Prayer Times integration.""" +import voluptuous as vol + +from homeassistant import config_entries + +from homeassistant.core import callback + +from .const import CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, CALC_METHODS, 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 Transmission client 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.""" + errors = {} + + if user_input is not None: + if user_input[CONF_CALC_METHOD] in CALC_METHODS: + return self.async_create_entry(title="", data=user_input) + else: + errors[CONF_CALC_METHOD] = "wrong_method" + + options = { + vol.Optional( + CONF_CALC_METHOD, + default=self.config_entry.options.get( + CONF_CALC_METHOD, DEFAULT_CALC_METHOD + ), + ): str + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options), errors=errors + ) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py new file mode 100644 index 0000000000000..a593352da3b0f --- /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" +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" + +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 b9245bf081256..c8c333088de5f 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -4,5 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": ["prayer_times_calculator==0.0.3"], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 3f7de535407bf..56f0689d42a91 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,166 +1,32 @@ """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.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time -import homeassistant.util.dt as dt_util +from .const import ( + DOMAIN, + PRAYER_TIMES_ICON, + 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): - """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 - - prayer_times_data = IslamicPrayerTimesData(latitude, longitude, calc_method) - - 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 + """Import config from configuration.yaml.""" + pass - 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 = "{}::{}".format(str(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.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Islamic prayer times sensor platform.""" - today = datetime.today().strftime("%Y-%m-%d") + client = hass.data[DOMAIN] - calc = PrayerTimesCalculator( - latitude=self.latitude, - longitude=self.longitude, - calculation_method=self.calc_method, - date=str(today), - ) + dev = [] + for sensor_type in SENSOR_TYPES: + dev.append(IslamicPrayerTimeSensor(sensor_type, client)) - self.prayer_times_info = calc.fetch_prayer_times() - return self.prayer_times_info + async_add_entities(dev, True) class IslamicPrayerTimeSensor(Entity): @@ -168,22 +34,24 @@ class IslamicPrayerTimeSensor(Entity): ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" - 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 = self.ENTITY_ID_FORMAT.format(self.sensor_type) - self.prayer_times_data = prayer_times_data + self.client = client 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._state = None @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the unique id of the entity.""" + return self.entity_id + @property def icon(self): """Icon to display in the front end.""" @@ -194,6 +62,11 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.client.available + @property def should_poll(self): """Disable polling.""" @@ -202,18 +75,10 @@ def should_poll(self): @property def device_class(self): """Return the device class.""" - return self._device_class - - @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 = "{} {}".format(str(today), prayer_time) - pt_dt = dt_util.parse_datetime(date_time_str) - return pt_dt + return DEVICE_CLASS_TIMESTAMP 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) + prayer_time = self.client.prayer_times_info[self.name] + pt_dt = self.client.get_prayer_time_as_dt(prayer_time) self._state = pt_dt.isoformat() diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json new file mode 100644 index 0000000000000..3152f3a58dc6c --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Islamic Prayer Times", + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Are you sure you want to set up Islamic Prayer Times?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "title": "Configure options for Islamic Prayer Times", + "step": { + "init": { + "data": { + "calculation_method": "Prayer calculation method" + } + } + }, + "error": { + "wrong_method": "possible values [karachi, isna, mwl, makkah]" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ea8a0a4e82d4c..a71570bda6e6e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -44,6 +44,7 @@ "ios", "ipma", "iqvia", + "islamic_prayer_times", "izone", "life360", "lifx", 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 0000000000000..2850e7fb52284 --- /dev/null +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -0,0 +1,71 @@ +"""Tests for Islamic Prayer Times config flow.""" +from tests.common import MockConfigEntry + +from homeassistant.components.islamic_prayer_times import config_flow +from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN + + +async def test_flow_works(hass): + """Test user config.""" + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form", result + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert 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"}, + ) + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await options_flow.async_step_init({CONF_CALC_METHOD: "makkah"}) + assert result["type"] == "create_entry" + assert result["data"][CONF_CALC_METHOD] == "makkah" + + # Test calc_method is wrong + + result = await options_flow.async_step_init({CONF_CALC_METHOD: "bla"}) + + assert result["type"] == "form" + assert result["errors"] == {CONF_CALC_METHOD: "wrong_method"} + + +async def test_import(hass): + """Test import step.""" + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({CONF_CALC_METHOD: "makkah"}) + assert 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) + flow = config_flow.IslamicPrayerFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + + assert 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 0000000000000..0111d3e6d5b33 --- /dev/null +++ b/tests/components/islamic_prayer_times/test_init.py @@ -0,0 +1,73 @@ +"""Tests for Islamic Prayer Times init.""" + +from unittest.mock import patch + +import pytest +import prayer_times_calculator + +from tests.common import MockConfigEntry, mock_coro + +from homeassistant.components import islamic_prayer_times +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + + +MOCK_ENTRY = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Islamic Prayer Times.""" + assert await async_setup_component(hass, islamic_prayer_times.DOMAIN, {}) is True + assert islamic_prayer_times.DOMAIN not in hass.data + + +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"} + } + 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 = MOCK_ENTRY + entry.add_to_hass(hass) + + assert await islamic_prayer_times.async_setup_entry(hass, entry) is True + 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 = MOCK_ENTRY + entry.add_to_hass(hass) + + # test request error raising ConfigEntryNotReady + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + side_effect=prayer_times_calculator.exceptions.InvalidResponseError(), + ), pytest.raises(ConfigEntryNotReady): + + await islamic_prayer_times.async_setup_entry(hass, entry) + + +async def test_unload_entry(hass): + """Test removing Islamic Prayer Times.""" + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry: + assert await islamic_prayer_times.async_setup_entry(hass, entry) + + assert await islamic_prayer_times.async_unload_entry(hass, entry) + assert unload_entry.call_count == 1 + assert islamic_prayer_times.DOMAIN not in hass.data From 4fd2cea29a8942219a5047bbdc3c41f4234785a2 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Fri, 7 Feb 2020 13:46:17 +0200 Subject: [PATCH 03/15] handle options update and fix tests --- .../islamic_prayer_times/EXAMPLE___init__.py | 49 ----- .../islamic_prayer_times/__init__.py | 22 ++- .../islamic_prayer_times/config_flow.py | 7 +- .../islamic_prayer_times/test_config_flow.py | 62 ++++--- .../islamic_prayer_times/test_init.py | 41 ++++- .../islamic_prayer_times/test_sensor.py | 169 +++--------------- 6 files changed, 105 insertions(+), 245 deletions(-) delete mode 100644 homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py diff --git a/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py b/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py deleted file mode 100644 index c8fbd4a9fcba7..0000000000000 --- a/homeassistant/components/islamic_prayer_times/EXAMPLE___init__.py +++ /dev/null @@ -1,49 +0,0 @@ -"""The Islamic prayer times integration.""" -import asyncio - -import voluptuous as vol - -from homeassistant.core import HomeAssistant -from homeassistant.config_entries import ConfigEntry - -from .const import DOMAIN - -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - -# TODO List the platforms that you want to support. -# For your initial PR, limit it to 1 platform. -PLATFORMS = ["light"] - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Islamic prayer times component.""" - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Islamic prayer times from a config entry.""" - # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(...) - - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index a3646484dd8bd..9b954bfe8bde5 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -3,22 +3,20 @@ import logging from prayer_times_calculator import PrayerTimesCalculator, exceptions - - import voluptuous as vol -import homeassistant.util.dt as dt_util 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_track_point_in_time +import homeassistant.util.dt as dt_util from .const import ( - DOMAIN, - CONF_CALC_METHOD, - DEFAULT_CALC_METHOD, CALC_METHODS, + CONF_CALC_METHOD, DATA_UPDATED, + DEFAULT_CALC_METHOD, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) @@ -51,12 +49,12 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the Islamic Prayer Component.""" client = IslamicPrayerClient(hass, config_entry) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN] = client if not await client.async_setup(): return False + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN] = client return True @@ -155,7 +153,7 @@ async def async_update(self): await self.get_new_prayer_times() await self.schedule_future_update() self.available = True - _LOGGER.debug("New prayer times retrieved. Updating sensors.") + _LOGGER.debug("New prayer times retrieved. Updating sensors.") except exceptions.InvalidResponseError: self.available = False @@ -173,6 +171,7 @@ async def async_setup(self): 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( @@ -200,3 +199,8 @@ def get_prayer_time_as_dt(prayer_time): date_time_str = "{} {}".format(str(today), prayer_time) pt_dt = dt_util.parse_datetime(date_time_str) return pt_dt + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + 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 index a7890fb7c17c0..e0fc788bf3fc0 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -2,10 +2,9 @@ import voluptuous as vol from homeassistant import config_entries - from homeassistant.core import callback -from .const import CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, CALC_METHODS, NAME +from .const import CALC_METHODS, CONF_CALC_METHOD, DEFAULT_CALC_METHOD, DOMAIN, NAME class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -49,8 +48,8 @@ async def async_step_init(self, user_input=None): if user_input is not None: if user_input[CONF_CALC_METHOD] in CALC_METHODS: return self.async_create_entry(title="", data=user_input) - else: - errors[CONF_CALC_METHOD] = "wrong_method" + + errors[CONF_CALC_METHOD] = "wrong_method" options = { vol.Optional( diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 2850e7fb52284..84d69344cc3ee 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,22 +1,23 @@ """Tests for Islamic Prayer Times config flow.""" -from tests.common import MockConfigEntry - -from homeassistant.components.islamic_prayer_times import config_flow +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 + async def test_flow_works(hass): """Test user config.""" - flow = config_flow.IslamicPrayerFlowHandler() - flow.hass = hass - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + islamic_prayer_times.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form", result + 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"], {}) - assert result["type"] == "create_entry" + 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" @@ -28,33 +29,40 @@ async def test_options(hass): data={}, options={CONF_CALC_METHOD: "isna"}, ) - flow = config_flow.IslamicPrayerFlowHandler() - flow.hass = hass - options_flow = flow.async_get_options_flow(entry) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) - result = await options_flow.async_step_init() - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await options_flow.async_step_init({CONF_CALC_METHOD: "makkah"}) - assert result["type"] == "create_entry" + 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" # Test calc_method is wrong - result = await options_flow.async_step_init({CONF_CALC_METHOD: "bla"}) + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_CALC_METHOD: "bla"} + ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_CALC_METHOD: "wrong_method"} async def test_import(hass): """Test import step.""" - flow = config_flow.IslamicPrayerFlowHandler() - flow.hass = hass + result = await hass.config_entries.flow.async_init( + islamic_prayer_times.DOMAIN, + context={"source": "import"}, + data={CONF_CALC_METHOD: "makkah"}, + ) - result = await flow.async_step_import({CONF_CALC_METHOD: "makkah"}) - assert result["type"] == "create_entry" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Islamic Prayer Times" assert result["data"][CONF_CALC_METHOD] == "makkah" @@ -63,9 +71,9 @@ async def test_integration_already_configured(hass): """Test integration is already configured.""" entry = MockConfigEntry(domain=DOMAIN, data={}, options={},) entry.add_to_hass(hass) - flow = config_flow.IslamicPrayerFlowHandler() - flow.hass = hass - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + islamic_prayer_times.DOMAIN, context={"source": "user"} + ) - assert result["type"] == "abort" + 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 index 0111d3e6d5b33..6626150e36d0c 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -2,17 +2,26 @@ from unittest.mock import patch -import pytest import prayer_times_calculator - -from tests.common import MockConfigEntry, mock_coro +import pytest from homeassistant.components import islamic_prayer_times from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + +MOCK_OPTIONS = {islamic_prayer_times.CONF_CALC_METHOD: "makkah"} -MOCK_ENTRY = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) +PRAYER_TIMES = { + "Fajr": "06:10", + "Sunrise": "07:25", + "Dhuhr": "12:30", + "Asr": "15:32", + "Maghrib": "17:35", + "Isha": "18:53", + "Midnight": "00:45", +} async def test_setup_with_no_config(hass): @@ -34,7 +43,7 @@ async def test_setup_with_config(hass): async def test_successful_config_entry(hass): """Test that Islamic Prayer Times is configured successfully.""" - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) entry.add_to_hass(hass) assert await islamic_prayer_times.async_setup_entry(hass, entry) is True @@ -46,7 +55,7 @@ async def test_successful_config_entry(hass): async def test_setup_failed(hass): """Test Islamic Prayer Times failed due to an error.""" - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) entry.add_to_hass(hass) # test request error raising ConfigEntryNotReady @@ -60,7 +69,7 @@ async def test_setup_failed(hass): async def test_unload_entry(hass): """Test removing Islamic Prayer Times.""" - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) entry.add_to_hass(hass) with patch.object( @@ -71,3 +80,21 @@ async def test_unload_entry(hass): assert await islamic_prayer_times.async_unload_entry(hass, entry) assert unload_entry.call_count == 1 assert islamic_prayer_times.DOMAIN not in hass.data + + +async def test_islamic_prayer_times_data_get_prayer_times(hass): + """Test Islamic prayer times data fetcher.""" + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ): + config_entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + pt_data = islamic_prayer_times.IslamicPrayerClient(hass, config_entry) + await pt_data.async_setup() + assert pt_data.prayer_times_info == PRAYER_TIMES diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 3151b030637b1..755d5aad5e58f 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,16 +1,17 @@ """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 tests.common import MockConfigEntry LATITUDE = 41 LONGITUDE = -87 CALC_METHOD = "isna" +ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" + +MOCK_OPTIONS = {islamic_prayer_times.CONF_CALC_METHOD: "makkah"} + PRAYER_TIMES = { "Fajr": "06:10", "Sunrise": "07:25", @@ -20,43 +21,11 @@ "Isha": "18:53", "Midnight": "00:45", } -ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" -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"] - - 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"}} - assert await async_setup_component(hass, "sensor", config) is True - - 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 = [ + min_config_sensors = [ "fajr", "sunrise", "dhuhr", @@ -67,120 +36,22 @@ async def test_islamic_prayer_times_multiple_sensors(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, + ): + config_entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() - 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: + 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]) + pt_dt = hass.data[islamic_prayer_times.DOMAIN].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() From 87460d075d9c5cb4bede0fa1c16abe221ba65a86 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Fri, 7 Feb 2020 14:17:07 +0200 Subject: [PATCH 04/15] fix sensor update handling --- .../components/islamic_prayer_times/sensor.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 56f0689d42a91..dd397bb9b188c 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -2,19 +2,13 @@ import logging from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import ( - DOMAIN, - PRAYER_TIMES_ICON, - SENSOR_TYPES, -) - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import config from configuration.yaml.""" - pass +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -41,6 +35,7 @@ def __init__(self, sensor_type, client): self.client = client self._name = self.sensor_type.capitalize() self._state = None + self.unsub_update = None @property def name(self): @@ -77,8 +72,25 @@ def device_class(self): """Return the device class.""" return DEVICE_CLASS_TIMESTAMP + async def async_added_to_hass(self): + """Handle entity which will be added.""" + self.unsub_update = async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + async def async_update(self): """Update the sensor.""" - prayer_time = self.client.prayer_times_info[self.name] - pt_dt = self.client.get_prayer_time_as_dt(prayer_time) - self._state = pt_dt.isoformat() + if self.client.prayer_times_info is not None: + prayer_time = self.client.prayer_times_info[self.name] + pt_dt = self.client.get_prayer_time_as_dt(prayer_time) + self._state = pt_dt.isoformat() + + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None From 0e544e566383a03d999a3c28e21ea786e4bbfec9 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Thu, 13 Feb 2020 09:59:25 +0200 Subject: [PATCH 05/15] fix pylint --- homeassistant/components/islamic_prayer_times/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index e0fc788bf3fc0..2ab43e7997851 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -4,6 +4,7 @@ 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 From d412176cda19761f42868f4027b0f0930be106a0 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Sat, 15 Feb 2020 10:39:04 +0200 Subject: [PATCH 06/15] fix scheduled update and add test --- .../islamic_prayer_times/__init__.py | 2 +- .../islamic_prayer_times/test_init.py | 87 ++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 9b954bfe8bde5..d45d0ad8be519 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -147,7 +147,7 @@ async def schedule_future_update(self): async_track_point_in_time(self.hass, self.async_update, next_update_at) - async def async_update(self): + async def async_update(self, *_): """Update sensors with new prayer times.""" try: await self.get_new_prayer_times() diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 6626150e36d0c..531d498b4cb6d 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,15 +1,17 @@ """Tests for Islamic Prayer Times init.""" +from datetime import datetime, timedelta from unittest.mock import patch -import prayer_times_calculator +from prayer_times_calculator.exceptions import InvalidResponseError import pytest from homeassistant.components import islamic_prayer_times from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro MOCK_OPTIONS = {islamic_prayer_times.CONF_CALC_METHOD: "makkah"} @@ -35,9 +37,14 @@ async def test_setup_with_config(hass): config = { islamic_prayer_times.DOMAIN: {islamic_prayer_times.CONF_CALC_METHOD: "isna"} } - assert ( - await async_setup_component(hass, islamic_prayer_times.DOMAIN, config) is True - ) + 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): @@ -46,10 +53,14 @@ async def test_successful_config_entry(hass): entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) entry.add_to_hass(hass) - assert await islamic_prayer_times.async_setup_entry(hass, entry) is True - assert entry.options == { - islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD - } + with patch( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ): + assert await islamic_prayer_times.async_setup_entry(hass, entry) is True + assert entry.options == { + islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD + } async def test_setup_failed(hass): @@ -61,7 +72,7 @@ async def test_setup_failed(hass): # test request error raising ConfigEntryNotReady with patch( "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - side_effect=prayer_times_calculator.exceptions.InvalidResponseError(), + side_effect=InvalidResponseError(), ), pytest.raises(ConfigEntryNotReady): await islamic_prayer_times.async_setup_entry(hass, entry) @@ -72,10 +83,15 @@ async def test_unload_entry(hass): 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, + ): + assert await islamic_prayer_times.async_setup_entry(hass, entry) is True + with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) ) as unload_entry: - assert await islamic_prayer_times.async_setup_entry(hass, entry) assert await islamic_prayer_times.async_unload_entry(hass, entry) assert unload_entry.call_count == 1 @@ -95,6 +111,51 @@ async def test_islamic_prayer_times_data_get_prayer_times(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - pt_data = islamic_prayer_times.IslamicPrayerClient(hass, config_entry) - await pt_data.async_setup() + pt_data = hass.data[islamic_prayer_times.DOMAIN] assert pt_data.prayer_times_info == PRAYER_TIMES + + +async def test_async_update_on_schedule(hass): + """Test that update is scheduled based on midnight calculation.""" + 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( + "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times" + ) as FetchPrayerTimes: + FetchPrayerTimes.side_effect = [ + PRAYER_TIMES, + PRAYER_TIMES, + new_prayer_times, + ] + config_entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_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 + + 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.util.dt.utcnow", return_value=future, + ): + + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert pt_data.prayer_times_info == new_prayer_times From 2a61a7db7065532c9dc28f252939739108924b9d Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Wed, 18 Mar 2020 08:27:06 +0200 Subject: [PATCH 07/15] update test_init --- tests/components/islamic_prayer_times/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 531d498b4cb6d..e68e3be53dba1 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -57,7 +57,8 @@ async def test_successful_config_entry(hass): "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): - assert await islamic_prayer_times.async_setup_entry(hass, entry) is True + assert await hass.config_entries.async_setup(entry.entry_id) is True + await hass.async_block_till_done() assert entry.options == { islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD } From d5e6dfb3838badfdc6525bae71885dce0a68d887 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Thu, 19 Mar 2020 11:47:32 +0200 Subject: [PATCH 08/15] update flow options to show drop list --- .../islamic_prayer_times/.translations/en.json | 3 --- .../components/islamic_prayer_times/config_flow.py | 13 +++---------- .../components/islamic_prayer_times/strings.json | 3 --- .../islamic_prayer_times/test_config_flow.py | 10 ---------- 4 files changed, 3 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json index f70d5126620a1..baeceb4b25839 100644 --- a/homeassistant/components/islamic_prayer_times/.translations/en.json +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -19,9 +19,6 @@ "calculation_method": "Prayer calculation method" } } - }, - "error": { - "wrong_method": "possible values [karachi, isna, mwl, makkah]" } } } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 2ab43e7997851..e0c9a3244ea05 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -44,13 +44,8 @@ def __init__(self, config_entry): async def async_step_init(self, user_input=None): """Manage options.""" - errors = {} - if user_input is not None: - if user_input[CONF_CALC_METHOD] in CALC_METHODS: - return self.async_create_entry(title="", data=user_input) - - errors[CONF_CALC_METHOD] = "wrong_method" + return self.async_create_entry(title="", data=user_input) options = { vol.Optional( @@ -58,9 +53,7 @@ async def async_step_init(self, user_input=None): default=self.config_entry.options.get( CONF_CALC_METHOD, DEFAULT_CALC_METHOD ), - ): str + ): vol.In(CALC_METHODS) } - return self.async_show_form( - step_id="init", data_schema=vol.Schema(options), errors=errors - ) + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 3152f3a58dc6c..408e90659f37d 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -19,9 +19,6 @@ "calculation_method": "Prayer calculation method" } } - }, - "error": { - "wrong_method": "possible values [karachi, isna, mwl, makkah]" } } } \ No newline at end of file diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 84d69344cc3ee..adcc72cd02e84 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -43,16 +43,6 @@ async def test_options(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" - # Test calc_method is wrong - - result = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_CALC_METHOD: "bla"} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_CALC_METHOD: "wrong_method"} - async def test_import(hass): """Test import step.""" From ee453f6fdc47775e4ed8f1cf8833028c9f6bcb13 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 13 Apr 2020 13:49:07 +0300 Subject: [PATCH 09/15] clean up code --- .../.translations/en.json | 40 +++--- .../islamic_prayer_times/__init__.py | 110 ++++++++------- .../islamic_prayer_times/config_flow.py | 2 +- .../components/islamic_prayer_times/const.py | 9 +- .../components/islamic_prayer_times/sensor.py | 65 ++++----- .../islamic_prayer_times/strings.json | 6 +- .../islamic_prayer_times/__init__.py | 44 ++++++ .../islamic_prayer_times/test_config_flow.py | 14 ++ .../islamic_prayer_times/test_init.py | 125 +++++++----------- .../islamic_prayer_times/test_sensor.py | 52 ++------ 10 files changed, 224 insertions(+), 243 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json index baeceb4b25839..ff9fddd0866de 100644 --- a/homeassistant/components/islamic_prayer_times/.translations/en.json +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -1,24 +1,24 @@ { - "config": { - "title": "Islamic Prayer Times", - "step": { - "user": { - "title": "Set up Islamic Prayer Times", - "description": "Are you sure you want to set up Islamic Prayer Times?" - } - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - } + "config": { + "title": "Islamic Prayer Times", + "step": { + "user": { + "title": "Set up Islamic Prayer Times", + "description": "Do you want to set up Islamic Prayer Times?" + } }, - "options": { - "title": "Configure options for Islamic Prayer Times", - "step": { - "init": { - "data": { - "calculation_method": "Prayer calculation method" - } - } + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "title": "Configure options for Islamic Prayer Times", + "step": { + "init": { + "data": { + "calc_method": "Prayer calculation method" } + } } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index d45d0ad8be519..2a491925fc529 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -1,14 +1,15 @@ """The islamic_prayer_times component.""" -from datetime import datetime, timedelta +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_track_point_in_time +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_point_in_time import homeassistant.util.dt as dt_util from .const import ( @@ -53,16 +54,15 @@ async def async_setup_entry(hass, config_entry): if not await client.async_setup(): return False - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN] = client + hass.data.setdefault(DOMAIN, client) return True async def async_unload_entry(hass, config_entry): - """Unload Transmission Entry from config_entry.""" - + """Unload Islamic Prayer entry from config_entry.""" + if hass.data[DOMAIN].event_unsub: + await hass.async_add_executor_job(hass.data[DOMAIN].event_unsub) await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - hass.data.pop(DOMAIN) return True @@ -75,21 +75,26 @@ def __init__(self, hass, config_entry): """Initialize the Islamic Prayer client.""" self.hass = hass self.config_entry = config_entry - self.prayer_times_info = None - self.available = None + self.prayer_times_info = {} + self.available = True + self.event_unsub = None - async def get_new_prayer_times(self): - """Fetch prayer times for today.""" + @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.config_entry.options[CONF_CALC_METHOD], + calculation_method=self.calc_method, date=str(dt_util.now().date()), ) - self.prayer_times_info = calc.fetch_prayer_times() + return calc.fetch_prayer_times() - async def schedule_future_update(self): + def schedule_future_update(self): """Schedule future update for sensors. Midnight is a calculated time. The specifics of the calculation @@ -120,57 +125,57 @@ async def schedule_future_update(self): """ _LOGGER.debug("Scheduling next update for Islamic prayer times") - midnight_time = self.prayer_times_info["Midnight"] now = dt_util.as_local(dt_util.now()) - today = now.date() - midnight_dt_str = "{}::{}".format(str(today), midnight_time) - midnight_dt = datetime.strptime(midnight_dt_str, "%Y-%m-%d::%H:%M") + 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" + "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" + "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)) - 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)) + _LOGGER.info("Next update scheduled for: %s", next_update_at) - async_track_point_in_time(self.hass, self.async_update, next_update_at) + self.event_unsub = track_point_in_time(self.hass, self.update, next_update_at) - async def async_update(self, *_): + def update(self, *_): """Update sensors with new prayer times.""" try: - await self.get_new_prayer_times() - await self.schedule_future_update() + prayer_times = self.get_new_prayer_times() self.available = True - _LOGGER.debug("New prayer times retrieved. Updating sensors.") - - except exceptions.InvalidResponseError: + except (exceptions.InvalidResponseError, ConnError): self.available = False + _LOGGER.debug("Error retrieving prayer times.") + track_point_in_time( + self.hass, self.update, dt_util.utcnow() + timedelta(minutes=1) + ) + return + + for prayer, time in prayer_times.items(): + self.prayer_times_info[prayer] = dt_util.parse_datetime( + f"{dt_util.now().date()} {time}" + ) + self.schedule_future_update() - async_dispatcher_send(self.hass, DATA_UPDATED) + _LOGGER.debug("New prayer times retrieved. Updating sensors.") + dispatcher_send(self.hass, DATA_UPDATED) async def async_setup(self): """Set up the Islamic prayer client.""" - - self.add_options() + await self.async_add_options() try: - await self.get_new_prayer_times() - except exceptions.InvalidResponseError: + await self.hass.async_add_executor_job(self.get_new_prayer_times) + except (exceptions.InvalidResponseError, ConnError): raise ConfigEntryNotReady - await self.async_update() + await self.hass.async_add_executor_job(self.update) self.config_entry.add_update_listener(self.async_options_updated) self.hass.async_create_task( @@ -181,26 +186,19 @@ async def async_setup(self): return True - def add_options(self): + async def async_add_options(self): """Add options for entry.""" if not self.config_entry.options: - calc_method = self.config_entry.data.get( - CONF_CALC_METHOD, DEFAULT_CALC_METHOD - ) + 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, options={CONF_CALC_METHOD: calc_method} + self.config_entry, data=data, options={CONF_CALC_METHOD: calc_method} ) - @staticmethod - def get_prayer_time_as_dt(prayer_time): - """Create a datetime object for the respective prayer time.""" - today = dt_util.now().date() - date_time_str = "{} {}".format(str(today), prayer_time) - pt_dt = dt_util.parse_datetime(date_time_str) - return pt_dt - @staticmethod async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" - await hass.data[DOMAIN].async_update() + if hass.data[DOMAIN].event_unsub: + await hass.async_add_executor_job(hass.data[DOMAIN].event_unsub) + await hass.async_add_executor_job(hass.data[DOMAIN].update) diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index e0c9a3244ea05..d45997af76f62 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -31,7 +31,7 @@ async def async_step_user(self, user_input=None): return self.async_create_entry(title=NAME, data=user_input) async def async_step_import(self, import_config): - """Import from Transmission client config.""" + """Import from config.""" return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index a593352da3b0f..bc40e04e25677 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -1,14 +1,17 @@ """Constants for the Islamic Prayer component.""" DOMAIN = "islamic_prayer_times" NAME = "Islamic Prayer Times" +SENSOR_SUFFIX = "Prayer" PRAYER_TIMES_ICON = "mdi:calendar-clock" +ATTR_TIMESTAMP = "timestamp" +TIME_STR_FORMAT = "%H:%M" -SENSOR_TYPES = ["fajr", "sunrise", "dhuhr", "asr", "maghrib", "isha", "midnight"] +SENSOR_TYPES = ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha", "Midnight"] -CONF_CALC_METHOD = "calculation_method" +CONF_CALC_METHOD = "calc_method" CONF_SENSORS = "sensors" -CALC_METHODS = ["karachi", "isna", "mwl", "makkah"] +CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] DEFAULT_CALC_METHOD = "isna" DATA_UPDATED = "Islamic_prayer_data_updated" diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index dd397bb9b188c..20ef52fa7b85a 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,12 +1,18 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" import logging -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES +from .const import ( + ATTR_TIMESTAMP, + DATA_UPDATED, + DOMAIN, + PRAYER_TIMES_ICON, + SENSOR_SUFFIX, + SENSOR_TYPES, + TIME_STR_FORMAT, +) _LOGGER = logging.getLogger(__name__) @@ -16,36 +22,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): client = hass.data[DOMAIN] - dev = [] + entities = [] for sensor_type in SENSOR_TYPES: - dev.append(IslamicPrayerTimeSensor(sensor_type, client)) + entities.append(IslamicPrayerTimeSensor(sensor_type, client)) - async_add_entities(dev, True) + async_add_entities(entities, True) class IslamicPrayerTimeSensor(Entity): """Representation of an Islamic prayer time sensor.""" - ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" - def __init__(self, sensor_type, client): """Initialize the Islamic prayer time sensor.""" self.sensor_type = sensor_type - self.entity_id = self.ENTITY_ID_FORMAT.format(self.sensor_type) self.client = client - self._name = self.sensor_type.capitalize() - self._state = None - self.unsub_update = None + self._state = self.client.prayer_times_info.get(self.sensor_type) @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.entity_id + return f"{DOMAIN}_{self.sensor_type}" @property def icon(self): @@ -55,12 +56,7 @@ def icon(self): @property def state(self): """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Could the device be accessed during the last update call.""" - return self.client.available + return self._state.time().strftime(TIME_STR_FORMAT) if self._state else None @property def should_poll(self): @@ -68,29 +64,14 @@ def should_poll(self): return False @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP + def state_attributes(self): + """Return datetime as attribute.""" + if self._state: + return {ATTR_TIMESTAMP: self._state} + return None async def async_added_to_hass(self): """Handle entity which will be added.""" - self.unsub_update = async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.async_on_remove( + async_dispatcher_connect(self.hass, DATA_UPDATED, self.async_write_ha_state) ) - - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) - - async def async_update(self): - """Update the sensor.""" - if self.client.prayer_times_info is not None: - prayer_time = self.client.prayer_times_info[self.name] - pt_dt = self.client.get_prayer_time_as_dt(prayer_time) - self._state = pt_dt.isoformat() - - async def will_remove_from_hass(self): - """Unsubscribe from update dispatcher.""" - if self.unsub_update: - self.unsub_update() - self.unsub_update = None diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 408e90659f37d..ff9fddd0866de 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -4,7 +4,7 @@ "step": { "user": { "title": "Set up Islamic Prayer Times", - "description": "Are you sure you want to set up Islamic Prayer Times?" + "description": "Do you want to set up Islamic Prayer Times?" } }, "abort": { @@ -16,9 +16,9 @@ "step": { "init": { "data": { - "calculation_method": "Prayer calculation method" + "calc_method": "Prayer calculation method" } } } } -} \ No newline at end of file +} diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 4a2f000251668..db25428d17acd 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 index adcc72cd02e84..a56178e5225fb 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,4 +1,8 @@ """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 @@ -6,6 +10,16 @@ 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( diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index e68e3be53dba1..740f6d62101f7 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,35 +1,23 @@ """Tests for Islamic Prayer Times init.""" -from datetime import datetime, timedelta +from datetime import timedelta from unittest.mock import patch from prayer_times_calculator.exceptions import InvalidResponseError -import pytest +from homeassistant import config_entries from homeassistant.components import islamic_prayer_times -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro +from . import ( + NEW_PRAYER_TIMES, + NEW_PRAYER_TIMES_TIMESTAMPS, + NOW, + PRAYER_TIMES, + PRAYER_TIMES_TIMESTAMPS, +) -MOCK_OPTIONS = {islamic_prayer_times.CONF_CALC_METHOD: "makkah"} - -PRAYER_TIMES = { - "Fajr": "06:10", - "Sunrise": "07:25", - "Dhuhr": "12:30", - "Asr": "15:32", - "Maghrib": "17:35", - "Isha": "18:53", - "Midnight": "00:45", -} - - -async def test_setup_with_no_config(hass): - """Test that we do not discover anything or try to set up a Islamic Prayer Times.""" - assert await async_setup_component(hass, islamic_prayer_times.DOMAIN, {}) is True - assert islamic_prayer_times.DOMAIN not in hass.data +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_with_config(hass): @@ -57,8 +45,9 @@ async def test_successful_config_entry(hass): "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): - assert await hass.config_entries.async_setup(entry.entry_id) is True - await hass.async_block_till_done() + 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 } @@ -74,9 +63,9 @@ async def test_setup_failed(hass): with patch( "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", side_effect=InvalidResponseError(), - ), pytest.raises(ConfigEntryNotReady): - - await islamic_prayer_times.async_setup_entry(hass, entry) + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY async def test_unload_entry(hass): @@ -88,75 +77,55 @@ async def test_unload_entry(hass): "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", return_value=PRAYER_TIMES, ): - assert await islamic_prayer_times.async_setup_entry(hass, entry) is True - - with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) - ) as unload_entry: + await hass.config_entries.async_setup(entry.entry_id) assert await islamic_prayer_times.async_unload_entry(hass, entry) - assert unload_entry.call_count == 1 assert islamic_prayer_times.DOMAIN not in hass.data -async def test_islamic_prayer_times_data_get_prayer_times(hass): - """Test Islamic prayer times data fetcher.""" +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, - ): - config_entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS + ), 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 ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_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 - - -async def test_async_update_on_schedule(hass): - """Test that update is scheduled based on midnight calculation.""" - new_prayer_times = { - "Fajr": "06:10", - "Sunrise": "07:25", - "Dhuhr": "12:30", - "Asr": "15:32", - "Maghrib": "17:45", - "Isha": "18:53", - "Midnight": "00:45", - } + +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: + ) as FetchPrayerTimes, patch("homeassistant.util.dt.now", return_value=NOW): FetchPrayerTimes.side_effect = [ PRAYER_TIMES, PRAYER_TIMES, - new_prayer_times, + NEW_PRAYER_TIMES, ] - config_entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) + + 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 - - 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) + assert pt_data.prayer_times_info == PRAYER_TIMES_TIMESTAMPS - with patch( - "homeassistant.util.dt.utcnow", return_value=future, - ): + 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 pt_data.prayer_times_info == new_prayer_times + 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 755d5aad5e58f..a37cfe02eb35b 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -3,55 +3,27 @@ from homeassistant.components import islamic_prayer_times -from tests.common import MockConfigEntry - -LATITUDE = 41 -LONGITUDE = -87 -CALC_METHOD = "isna" -ENTITY_ID_FORMAT = "sensor.islamic_prayer_time_{}" - -MOCK_OPTIONS = {islamic_prayer_times.CONF_CALC_METHOD: "makkah"} +from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS -PRAYER_TIMES = { - "Fajr": "06:10", - "Sunrise": "07:25", - "Dhuhr": "12:30", - "Asr": "15:32", - "Maghrib": "17:35", - "Isha": "18:53", - "Midnight": "00:45", -} +from tests.common import MockConfigEntry async def test_islamic_prayer_times_sensors(hass): """Test minimum Islamic prayer times configuration.""" - min_config_sensors = [ - "fajr", - "sunrise", - "dhuhr", - "asr", - "maghrib", - "isha", - "midnight", - ] + 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, - ): - config_entry = MockConfigEntry( - domain=islamic_prayer_times.DOMAIN, data={}, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) + ), patch("homeassistant.util.dt.now", return_value=NOW): + + 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 = hass.data[islamic_prayer_times.DOMAIN].get_prayer_time_as_dt( - PRAYER_TIMES[entity_id_name] + for prayer, time in PRAYER_TIMES.items(): + assert hass.states.get(f"sensor.{prayer}_prayer").state == time + assert ( + hass.states.get(f"sensor.{prayer}_prayer").attributes.get("timestamp") + == PRAYER_TIMES_TIMESTAMPS[prayer] ) - state = hass.states.get(entity_id) - assert state.state == pt_dt.isoformat() - assert state.name == entity_id_name From 92ca24c93de87f6927b9dc568574b08dc5be9c3d Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 13 Apr 2020 20:23:53 +0300 Subject: [PATCH 10/15] async scheduling and revert state to timestamp --- .../islamic_prayer_times/__init__.py | 34 +++++++++++-------- .../components/islamic_prayer_times/const.py | 3 -- .../components/islamic_prayer_times/sensor.py | 24 ++++--------- .../islamic_prayer_times/test_init.py | 4 ++- .../islamic_prayer_times/test_sensor.py | 7 ++-- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 2a491925fc529..60868a7324a4c 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_point_in_time import homeassistant.util.dt as dt_util from .const import ( @@ -61,9 +61,9 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload Islamic Prayer entry from config_entry.""" if hass.data[DOMAIN].event_unsub: - await hass.async_add_executor_job(hass.data[DOMAIN].event_unsub) - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + hass.data[DOMAIN].event_unsub() hass.data.pop(DOMAIN) + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") return True @@ -94,7 +94,7 @@ def get_new_prayer_times(self): ) return calc.fetch_prayer_times() - def schedule_future_update(self): + async def async_schedule_future_update(self): """Schedule future update for sensors. Midnight is a calculated time. The specifics of the calculation @@ -142,18 +142,22 @@ def schedule_future_update(self): _LOGGER.info("Next update scheduled for: %s", next_update_at) - self.event_unsub = track_point_in_time(self.hass, self.update, next_update_at) + self.event_unsub = async_track_point_in_time( + self.hass, self.async_update, next_update_at + ) - def update(self, *_): + async def async_update(self, *_): """Update sensors with new prayer times.""" try: - prayer_times = self.get_new_prayer_times() + 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.") - track_point_in_time( - self.hass, self.update, dt_util.utcnow() + timedelta(minutes=1) + async_track_point_in_time( + self.hass, self.async_update, dt_util.utcnow() + timedelta(minutes=1) ) return @@ -161,10 +165,10 @@ def update(self, *_): self.prayer_times_info[prayer] = dt_util.parse_datetime( f"{dt_util.now().date()} {time}" ) - self.schedule_future_update() + await self.async_schedule_future_update() _LOGGER.debug("New prayer times retrieved. Updating sensors.") - dispatcher_send(self.hass, DATA_UPDATED) + async_dispatcher_send(self.hass, DATA_UPDATED) async def async_setup(self): """Set up the Islamic prayer client.""" @@ -175,7 +179,7 @@ async def async_setup(self): except (exceptions.InvalidResponseError, ConnError): raise ConfigEntryNotReady - await self.hass.async_add_executor_job(self.update) + await self.async_update() self.config_entry.add_update_listener(self.async_options_updated) self.hass.async_create_task( @@ -200,5 +204,5 @@ async def async_add_options(self): async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" if hass.data[DOMAIN].event_unsub: - await hass.async_add_executor_job(hass.data[DOMAIN].event_unsub) - await hass.async_add_executor_job(hass.data[DOMAIN].update) + hass.data[DOMAIN].event_unsub() + await hass.data[DOMAIN].async_update() diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index bc40e04e25677..5a9007689d9c0 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -3,13 +3,10 @@ NAME = "Islamic Prayer Times" SENSOR_SUFFIX = "Prayer" PRAYER_TIMES_ICON = "mdi:calendar-clock" -ATTR_TIMESTAMP = "timestamp" -TIME_STR_FORMAT = "%H:%M" SENSOR_TYPES = ["Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha", "Midnight"] CONF_CALC_METHOD = "calc_method" -CONF_SENSORS = "sensors" CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] DEFAULT_CALC_METHOD = "isna" diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 20ef52fa7b85a..d1f4baa90bc96 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,18 +1,11 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" import logging +from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import ( - ATTR_TIMESTAMP, - DATA_UPDATED, - DOMAIN, - PRAYER_TIMES_ICON, - SENSOR_SUFFIX, - SENSOR_TYPES, - TIME_STR_FORMAT, -) +from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_SUFFIX, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -36,7 +29,6 @@ def __init__(self, sensor_type, client): """Initialize the Islamic prayer time sensor.""" self.sensor_type = sensor_type self.client = client - self._state = self.client.prayer_times_info.get(self.sensor_type) @property def name(self): @@ -46,7 +38,7 @@ def name(self): @property def unique_id(self): """Return the unique id of the entity.""" - return f"{DOMAIN}_{self.sensor_type}" + return self.sensor_type @property def icon(self): @@ -56,7 +48,7 @@ def icon(self): @property def state(self): """Return the state of the sensor.""" - return self._state.time().strftime(TIME_STR_FORMAT) if self._state else None + return self.client.prayer_times_info.get(self.sensor_type).isoformat() @property def should_poll(self): @@ -64,11 +56,9 @@ def should_poll(self): return False @property - def state_attributes(self): - """Return datetime as attribute.""" - if self._state: - return {ATTR_TIMESTAMP: self._state} - return None + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP async def async_added_to_hass(self): """Handle entity which will be added.""" diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 740f6d62101f7..e91e83b315e29 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -79,7 +79,9 @@ async def test_unload_entry(hass): ): await hass.config_entries.async_setup(entry.entry_id) - assert await islamic_prayer_times.async_unload_entry(hass, entry) + 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 diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index a37cfe02eb35b..4954287b86473 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -21,9 +21,8 @@ async def test_islamic_prayer_times_sensors(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - for prayer, time in PRAYER_TIMES.items(): - assert hass.states.get(f"sensor.{prayer}_prayer").state == time + for prayer in PRAYER_TIMES: assert ( - hass.states.get(f"sensor.{prayer}_prayer").attributes.get("timestamp") - == PRAYER_TIMES_TIMESTAMPS[prayer] + hass.states.get(f"sensor.{prayer}_prayer").state + == PRAYER_TIMES_TIMESTAMPS[prayer].isoformat() ) From d2b9782c7cd230ada55ff5f2b466122c415cf57a Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Wed, 15 Apr 2020 13:43:29 +0300 Subject: [PATCH 11/15] fix update retry method --- homeassistant/components/islamic_prayer_times/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 60868a7324a4c..fa676221ea3dd 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -9,7 +9,7 @@ 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_track_point_in_time +from homeassistant.helpers.event import async_call_later, async_track_point_in_time import homeassistant.util.dt as dt_util from .const import ( @@ -156,9 +156,7 @@ async def async_update(self, *_): except (exceptions.InvalidResponseError, ConnError): self.available = False _LOGGER.debug("Error retrieving prayer times.") - async_track_point_in_time( - self.hass, self.async_update, dt_util.utcnow() + timedelta(minutes=1) - ) + async_call_later(self.hass, 60, self.async_update) return for prayer, time in prayer_times.items(): From d8d3df0b72e178f76d556138c27bda94b68593a7 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 20 Apr 2020 10:03:59 +0300 Subject: [PATCH 12/15] update strings --- .../components/islamic_prayer_times/.translations/en.json | 2 -- homeassistant/components/islamic_prayer_times/strings.json | 2 -- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json index ff9fddd0866de..246a5b873d83f 100644 --- a/homeassistant/components/islamic_prayer_times/.translations/en.json +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -1,6 +1,5 @@ { "config": { - "title": "Islamic Prayer Times", "step": { "user": { "title": "Set up Islamic Prayer Times", @@ -12,7 +11,6 @@ } }, "options": { - "title": "Configure options for Islamic Prayer Times", "step": { "init": { "data": { diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index ff9fddd0866de..246a5b873d83f 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -1,6 +1,5 @@ { "config": { - "title": "Islamic Prayer Times", "step": { "user": { "title": "Set up Islamic Prayer Times", @@ -12,7 +11,6 @@ } }, "options": { - "title": "Configure options for Islamic Prayer Times", "step": { "init": { "data": { From f3dc0534b008f4764b3b6ad254696316f581c53e Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 20 Apr 2020 10:57:37 +0300 Subject: [PATCH 13/15] keep title as root key --- .../components/islamic_prayer_times/.translations/en.json | 1 + homeassistant/components/islamic_prayer_times/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json index 246a5b873d83f..ebbea482122ae 100644 --- a/homeassistant/components/islamic_prayer_times/.translations/en.json +++ b/homeassistant/components/islamic_prayer_times/.translations/en.json @@ -1,4 +1,5 @@ { + "title": "Islamic Prayer Times", "config": { "step": { "user": { diff --git a/homeassistant/components/islamic_prayer_times/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index 246a5b873d83f..ebbea482122ae 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -1,4 +1,5 @@ { + "title": "Islamic Prayer Times", "config": { "step": { "user": { From 300220daadc19362fe52fb3272f47a8d62287d9d Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 27 Apr 2020 20:34:11 +0300 Subject: [PATCH 14/15] fix sensor naming and calculation_method key --- .../components/islamic_prayer_times/const.py | 13 ++++++++++--- .../components/islamic_prayer_times/sensor.py | 4 ++-- .../components/islamic_prayer_times/test_sensor.py | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index 5a9007689d9c0..ee7512c2d7a84 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -1,12 +1,19 @@ """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"] +SENSOR_TYPES = { + "Fajr": "prayer", + "Sunrise": "time", + "Dhuhr": "prayer", + "Asr": "prayer", + "Maghrib": "prayer", + "Isha": "prayer", + "Midnight": "time", +} -CONF_CALC_METHOD = "calc_method" +CONF_CALC_METHOD = "calculation_method" CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] DEFAULT_CALC_METHOD = "isna" diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index d1f4baa90bc96..92a0a491d8d12 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -5,7 +5,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_SUFFIX, SENSOR_TYPES +from .const import DATA_UPDATED, DOMAIN, PRAYER_TIMES_ICON, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def __init__(self, sensor_type, client): @property def name(self): """Return the name of the sensor.""" - return f"{self.sensor_type} {SENSOR_SUFFIX}" + return f"{self.sensor_type} {SENSOR_TYPES[self.sensor_type]}" @property def unique_id(self): diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 4954287b86473..0579664ae7be6 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -23,6 +23,8 @@ async def test_islamic_prayer_times_sensors(hass): for prayer in PRAYER_TIMES: assert ( - hass.states.get(f"sensor.{prayer}_prayer").state + hass.states.get( + f"sensor.{prayer}_{islamic_prayer_times.const.SENSOR_TYPES[prayer]}" + ).state == PRAYER_TIMES_TIMESTAMPS[prayer].isoformat() ) From 41dc6f0a761b145cc3b41112b7e2e7cc767504ad Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Mon, 27 Apr 2020 20:39:53 +0300 Subject: [PATCH 15/15] fix strings --- .../.translations/en.json | 23 ----------- .../islamic_prayer_times/strings.json | 2 +- .../islamic_prayer_times/translations/en.json | 40 +++++++++---------- 3 files changed, 21 insertions(+), 44 deletions(-) delete mode 100644 homeassistant/components/islamic_prayer_times/.translations/en.json diff --git a/homeassistant/components/islamic_prayer_times/.translations/en.json b/homeassistant/components/islamic_prayer_times/.translations/en.json deleted file mode 100644 index ebbea482122ae..0000000000000 --- a/homeassistant/components/islamic_prayer_times/.translations/en.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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/strings.json b/homeassistant/components/islamic_prayer_times/strings.json index ebbea482122ae..857ce4c2dffb5 100644 --- a/homeassistant/components/islamic_prayer_times/strings.json +++ b/homeassistant/components/islamic_prayer_times/strings.json @@ -15,7 +15,7 @@ "step": { "init": { "data": { - "calc_method": "Prayer calculation method" + "calculation_method": "Prayer calculation method" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/en.json b/homeassistant/components/islamic_prayer_times/translations/en.json index 155a693ab1fba..4db928c0ede3f 100644 --- a/homeassistant/components/islamic_prayer_times/translations/en.json +++ b/homeassistant/components/islamic_prayer_times/translations/en.json @@ -1,23 +1,23 @@ { - "config": { - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - }, - "step": { - "user": { - "description": "Do you want to set up Islamic Prayer Times?", - "title": "Set up Islamic Prayer Times" - } - } + "config": { + "abort": { + "one_instance_allowed": "Only a single instance is necessary." }, - "options": { - "step": { - "init": { - "data": { - "calc_method": "Prayer calculation method" - } - } + "step": { + "user": { + "description": "Do you want to set up Islamic Prayer Times?", + "title": "Set up Islamic Prayer Times" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Prayer calculation method" } - }, - "title": "Islamic Prayer Times" -} \ No newline at end of file + } + } + }, + "title": "Islamic Prayer Times" +}