Add new sensor platform to expose Islamic prayer times (#19444)
* added new sensor platform to expose Islamic prayer times * added new sensor platform to expose Islamic prayer times * updated tests according to feedback * make prayer_times_info a public attribute * remove stale comments
This commit is contained in:
parent
c15445159d
commit
71900ca719
3 changed files with 388 additions and 0 deletions
221
homeassistant/components/sensor/islamic_prayer_times.py
Normal file
221
homeassistant/components/sensor/islamic_prayer_times.py
Normal file
|
@ -0,0 +1,221 @@
|
|||
"""
|
||||
Platform to retrieve Islamic prayer times information for Home Assistant.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.islamic_prayer_times/
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
|
||||
|
||||
REQUIREMENTS = ['prayer_times_calculator==0.0.3']
|
||||
|
||||
_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
|
||||
|
||||
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_track_point_in_time(hass,
|
||||
update_sensors(hass, sensors, prayer_times_data),
|
||||
next_update_at)
|
||||
|
||||
|
||||
async def update_sensors(hass, sensors, prayer_times_data):
|
||||
"""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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
today = datetime.today().strftime('%Y-%m-%d')
|
||||
|
||||
calc = PrayerTimesCalculator(latitude=self.latitude,
|
||||
longitude=self.longitude,
|
||||
calculation_method=self.calc_method,
|
||||
date=str(today))
|
||||
|
||||
self.prayer_times_info = calc.fetch_prayer_times()
|
||||
return self.prayer_times_info
|
||||
|
||||
|
||||
class IslamicPrayerTimeSensor(Entity):
|
||||
"""Representation of an Islamic prayer time sensor."""
|
||||
|
||||
ENTITY_ID_FORMAT = 'sensor.islamic_prayer_time_{}'
|
||||
|
||||
def __init__(self, sensor_type, prayer_times_data):
|
||||
"""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._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()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to display in the front end."""
|
||||
return PRAYER_TIMES_ICON
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@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
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
prayer_time = self.prayer_times_data.prayer_times_info[self.name]
|
||||
pt_dt = self.get_prayer_time_as_dt(prayer_time)
|
||||
self._state = pt_dt.isoformat()
|
|
@ -798,6 +798,9 @@ pocketcasts==0.1
|
|||
# homeassistant.components.sensor.postnl
|
||||
postnl_api==1.0.2
|
||||
|
||||
# homeassistant.components.sensor.islamic_prayer_times
|
||||
prayer_times_calculator==0.0.3
|
||||
|
||||
# homeassistant.components.sensor.prezzibenzina
|
||||
prezzibenzina-py==1.1.4
|
||||
|
||||
|
|
164
tests/components/sensor/test_islamic_prayer_times.py
Normal file
164
tests/components/sensor/test_islamic_prayer_times.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
"""The tests for the Islamic prayer times sensor platform."""
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.sensor.islamic_prayer_times import \
|
||||
IslamicPrayerTimesData
|
||||
from tests.common import MockDependency
|
||||
import homeassistant.util.dt as dt_util
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
LATITUDE = 41
|
||||
LONGITUDE = -87
|
||||
CALC_METHOD = 'isna'
|
||||
PRAYER_TIMES = {"Fajr": "06:10", "Sunrise": "07:25", "Dhuhr": "12:30",
|
||||
"Asr": "15:32", "Maghrib": "17:35", "Isha": "18:53",
|
||||
"Midnight": "00:45"}
|
||||
ENTITY_ID_FORMAT = 'sensor.islamic_prayer_time_{}'
|
||||
|
||||
|
||||
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):
|
||||
"""Test minimum Islamic prayer times configuration."""
|
||||
min_config_sensors = ['fajr', 'dhuhr', 'asr', 'maghrib', 'isha']
|
||||
|
||||
with MockDependency('prayer_times_calculator') as mock_pt_calc:
|
||||
mock_pt_calc.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 = ['fajr', 'sunrise', 'dhuhr', 'asr', 'maghrib', 'isha',
|
||||
'midnight']
|
||||
|
||||
with MockDependency('prayer_times_calculator') as mock_pt_calc:
|
||||
mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times \
|
||||
.return_value = PRAYER_TIMES
|
||||
|
||||
config = {
|
||||
'sensor': {
|
||||
'platform': 'islamic_prayer_times',
|
||||
'sensors': multiple_sensors
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, 'sensor', config) is True
|
||||
|
||||
for sensor in multiple_sensors:
|
||||
entity_id = ENTITY_ID_FORMAT.format(sensor)
|
||||
entity_id_name = sensor.capitalize()
|
||||
pt_dt = get_prayer_time_as_dt(PRAYER_TIMES[entity_id_name])
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == pt_dt.isoformat()
|
||||
assert state.name == entity_id_name
|
||||
|
||||
|
||||
async def test_islamic_prayer_times_with_calculation_method(hass):
|
||||
"""Test Islamic prayer times configuration with calculation method."""
|
||||
sensors = ['fajr', 'maghrib']
|
||||
|
||||
with MockDependency('prayer_times_calculator') as mock_pt_calc:
|
||||
mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times \
|
||||
.return_value = PRAYER_TIMES
|
||||
|
||||
config = {
|
||||
'sensor': {
|
||||
'platform': 'islamic_prayer_times',
|
||||
'calculation_method': 'mwl',
|
||||
'sensors': sensors
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, 'sensor', config) is True
|
||||
|
||||
for sensor in sensors:
|
||||
entity_id = ENTITY_ID_FORMAT.format(sensor)
|
||||
entity_id_name = sensor.capitalize()
|
||||
pt_dt = get_prayer_time_as_dt(PRAYER_TIMES[entity_id_name])
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == pt_dt.isoformat()
|
||||
assert state.name == entity_id_name
|
||||
|
||||
|
||||
async def test_islamic_prayer_times_data_get_prayer_times(hass):
|
||||
"""Test Islamic prayer times data fetcher."""
|
||||
with MockDependency('prayer_times_calculator') as mock_pt_calc:
|
||||
mock_pt_calc.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 MockDependency('prayer_times_calculator') as mock_pt_calc:
|
||||
mock_pt_calc.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.sensor.islamic_prayer_times'
|
||||
'.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()
|
Loading…
Add table
Reference in a new issue