Skip to content

Commit

Permalink
Merge pull request #1 from zubir2k/beta
Browse files Browse the repository at this point in the history
0.2.0
  • Loading branch information
zubir2k authored Dec 19, 2024
2 parents 588022a + eed9522 commit e17b3d0
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 142 deletions.
71 changes: 71 additions & 0 deletions MARKDOWN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
## ⌚ Dynamic Prayer Time Card

![image](https://github.com/zubir2k/HomeAssistantEsolatGPS/assets/1905339/3e894bd2-7982-44b9-adbd-024e12a4c3c8)

1. Edit your current dashboard.
2. Add a new Markdown card. ![What is Markdown card?](https://www.home-assistant.io/dashboards/markdown)
3. Copy below markdown.
4. Save

```markdown
> Today is</b><br /><font size=7>{{ now().strftime('%I:%M %p') }}<br />{{ now().strftime('%A') }}</font>
<font size=2>{{ now().strftime('%d %B %Y') }} | {{ state_attr('sensor.esolatnow','hijri') }}</font>
{% set esolat = namespace(array=state_attr('sensor.esolatnow', 'array')) %}
{% for person in states.person | selectattr('name', 'eq', user) %}
{% set userid = person.entity_id | replace('person.', '') | replace(' ', '_') %}
{% set esolat_sensor = 'sensor.esolat_' ~ userid %}
{% if states(esolat_sensor) == 'unknown' or not esolat.array.get(userid) or person.state == 'home' %}
<ha-alert alert-type="info"><b>Waktu Sekarang: </b>{{ esolat.array.get('home').current }} - {{ as_timestamp(esolat.array.get('home').datetime) | timestamp_custom('%I:%M %p') }}<br /><b>Waktu Berikutnya: </b>{{ esolat.array.get('home').next }}</ha-alert>
<table align=center width=100%>
<tr align=center>
<td>Subuh</td>
<td>Zohor</td>
<td>Asar</td>
<td>Maghrib</td>
<td>Isyak</td>
</tr>
<tr align=center>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
</tr>
<tr align=center>
<td>{{ state_attr('sensor.esolat_home', 'subuh_12h') }}</td>
<td>{{ state_attr('sensor.esolat_home', 'zohor_12h') }}</td>
<td>{{ state_attr('sensor.esolat_home', 'asar_12h') }}</td>
<td>{{ state_attr('sensor.esolat_home', 'maghrib_12h') }}</td>
<td>{{ state_attr('sensor.esolat_home', 'isyak_12h') }}</td>
</tr>
<tr><ha-alert alert-type="info">Location: <b>Home</b> 🏠</ha-alert></tr>
{% else %}
<ha-alert alert-type="info"><b>Waktu Sekarang: </b>{{ esolat.array.get(userid).current }} - {{ as_timestamp(esolat.array.get(userid).datetime) | timestamp_custom('%I:%M %p') }}<br /><b>Waktu Berikutnya: </b>{{ esolat.array.get(userid).next }}</ha-alert>
<table align=center width=100%>
<tr align=center>
<td>Subuh</td>
<td>Zohor</td>
<td>Asar</td>
<td>Maghrib</td>
<td>Isyak</td>
</tr>
<tr align=center>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
<td><ha-icon icon="mdi:star-crescent"></ha-icon></td>
</tr>
<tr align=center>
<td>{{ state_attr(esolat_sensor, 'subuh_12h') }}</td>
<td>{{ state_attr(esolat_sensor, 'zohor_12h') }}</td>
<td>{{ state_attr(esolat_sensor, 'asar_12h') }}</td>
<td>{{ state_attr(esolat_sensor, 'maghrib_12h') }}</td>
<td>{{ state_attr(esolat_sensor, 'isyak_12h') }}</td>
</tr>
<tr><ha-alert alert-type="info"><b>Location: </b>{{ userid }}</ha-alert></tr>
{% endif %}
{% endfor %}
</table>

```
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
![esolatgps_banner](https://user-images.githubusercontent.com/1905339/223016758-1c0c8058-7375-43d9-bd65-9fc00f48809c.png)\
[![hacs_badge](https://img.shields.io/badge/HACS-Integration-41BDF5.svg)](https://github.com/hacs/integration)
![GitHub all releases](https://img.shields.io/badge/dynamic/json?color=41BDF5&logo=home-assistant&label=Download%20Count&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.esolatgps.total)
[![Buy](https://img.shields.io/badge/Belanja-Coffee-yellow.svg)](https://zubirco.de/buymecoffee)
![GitHub all releases](https://img.shields.io/github/downloads/zubir2k/homeassistant-esolatgps/total.svg)
![GitHub manifest version (path)](https://img.shields.io/github/manifest-json/v/zubir2k/homeassistant-esolatgps?filename=custom_components%2Fesolatgps%2Fmanifest.json)

Assalamu'alaikum
Expand All @@ -16,6 +16,10 @@ Prayer time information are made as sensor attributes with the following format:

This is a continuation of [HomeAssistantEsolatGPS](https://github.com/zubir2k/HomeAssistantEsolatGPS) (Appdaemon version)

## Whats New?
- Added Current Prayer Time (sensor.esolatnow)
- Dynamic Prayer Time dashboard using Markdown card

## Requirements
- Home Assistant 2021.x and above
- Person entity with GPS location (device tracker)
Expand Down Expand Up @@ -49,6 +53,15 @@ The sensors will be populated `sensor.esolat_` based on the person with GPS coor

![image](https://user-images.githubusercontent.com/1905339/223009818-6e8b483e-a86d-48f7-8f3d-b6fd2035bdae.png)

## Dynamic Prayer Time - Markdown Card
I have prepared a markdown card template that will:
- Automatically show Prayer time based on the logged in user
- If user state is at `home` the prayer time will automatically switched to Home prayer time

You may refer to the [Markdown.md](MARKDOWN.md) and copy the markdown codes.

![image](https://github.com/zubir2k/HomeAssistantEsolatGPS/assets/1905339/3e894bd2-7982-44b9-adbd-024e12a4c3c8)

## Special Thanks 🎉
- [HomeAssistantMalaysia](https://www.facebook.com/groups/homeassistantmalaysia)
- Saudara [Noorzaini Ilhami](https://github.com/i906) for his [MPT API](https://github.com/MalaysiaPrayerTimes)
Expand Down
9 changes: 7 additions & 2 deletions custom_components/esolatgps/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Constants for the EsolatGPS integration."""
"""
Constants for the EsolatGPS integration.
"""
from homeassistant.const import CONF_SCAN_INTERVAL

DOMAIN = "esolatgps"
Expand All @@ -10,4 +12,7 @@
MIN_SCAN_INTERVAL = 5

# Maximum scan interval in minutes (60 minutes = 1 hour)
MAX_SCAN_INTERVAL = 60
MAX_SCAN_INTERVAL = 60

# Prayer names
PRAYER_NAMES = ["Subuh", "Syuruk", "Zohor", "Asar", "Maghrib", "Isyak"]
198 changes: 198 additions & 0 deletions custom_components/esolatgps/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import logging
import aiohttp
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import entity_registry
from .const import PRAYER_NAMES

_LOGGER = logging.getLogger(__name__)

class EsolatGPSCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
"""Initialize the coordinator with the user-configured scan interval."""
super().__init__(
hass,
_LOGGER,
name="eSolat GPS",
update_interval=timedelta(minutes=entry.data.get("scan_interval", 15)),
)
self.url = "https://mpt.i906.my/api/prayer/"
self.geo = "https://nominatim.openstreetmap.org/reverse?format=json&"
self._tracked_entities = set()

async def _async_update_data(self):
"""Fetch data from API."""
async with aiohttp.ClientSession() as session:
data = {}
current_entities = set()

for entity_id in self.hass.states.async_entity_ids("person"):
entity = self.hass.states.get(entity_id)
latitude = entity.attributes.get("latitude")
longitude = entity.attributes.get("longitude")
if latitude and longitude:
current_entities.add(entity_id)
data[entity_id] = await self._fetch_prayer_times(session, latitude, longitude, entity_id)
else:
_LOGGER.info(f"Skipping {entity_id} as it lacks GPS coordinates.")

# Home zone processing
home = self.hass.states.get("zone.home")
home_latitude = home.attributes.get("latitude")
home_longitude = home.attributes.get("longitude")
data["zone.home"] = await self._fetch_prayer_times(session, home_latitude, home_longitude, "zone.home")
current_entities.add("zone.home")

# Cleanup stale entities
entities_to_remove = self._tracked_entities - current_entities
self._tracked_entities = current_entities

if entities_to_remove:
self.hass.async_create_task(self._remove_stale_entities(entities_to_remove))

return data

async def fetch_hijri_date(self):
"""Fetch Hijri date from API."""
url = "https://www.e-solat.gov.my/index.php"
params = {
"r": "esolatApi/tarikhtakwim",
"period": "today",
"datetype": "miladi"
}

try:
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params) as response:
if response.status == 200:
data = await response.json()
hijri_date = next(iter(data["takwim"].values()))
parts = hijri_date.split('-')
day = parts[2]
month_number = int(parts[1]) - 1
year = parts[0]

months = [
"Muharram", "Safar", "Rabi'ul Awwal", "Rabi'ul Akhir",
"Jamadil Awwal", "Jamadil Akhir", "Rejab", "Sha'aban",
"Ramadhan", "Syawal", "Zulkaedah", "Zulhijjah"
]

return f"{day} {months[month_number]} {year}"
else:
_LOGGER.error(f"Failed to fetch Hijri date: HTTP {response.status}")
return "unavailable"
except aiohttp.ClientError as e:
_LOGGER.error(f"Client error fetching Hijri date: {e}")
return "unavailable"
except Exception as e:
_LOGGER.error(f"Unexpected error fetching Hijri date: {e}")
return "unavailable"

async def _remove_stale_entities(self, entities_to_remove):
"""Remove entities that no longer have GPS coordinates."""
ent_reg = entity_registry.async_get(self.hass)
for entity_id in entities_to_remove:
entity_name = entity_id.split('.')[1]
sensor_id = f"sensor.esolat_{entity_name}"
if ent_reg.async_get(sensor_id): # Check if entity exists before removing
_LOGGER.info(f"Removing stale entity {sensor_id} due to missing GPS coordinates")
ent_reg.async_remove(sensor_id)

async def _fetch_prayer_times(self, session, latitude, longitude, entity_id):
"""Fetch prayer times from the API and handle out-of-country cases."""
today = datetime.now(ZoneInfo("Asia/Kuala_Lumpur")).date()
day_index = 0 if today.day == 1 else today.day - 1

try:
async with session.get(f"{self.url}{latitude},{longitude}") as response:
if response.status == 404:
async with session.get(f"{self.geo}lat={latitude}&lon={longitude}") as geo_response:
geodata = await geo_response.json()
geostate = geodata["address"]["state"]
geocountry = geodata["address"]["country_code"].upper()
return {
"state": "Outside Malaysia",
"attributes": {
"location": f"{geostate}, {geocountry}",
"gps": f"{latitude},{longitude}"
}
}
elif response.status == 200:
data = await response.json()
prayer_times_data = data["data"]["times"][day_index]
prayer_times = {}

for i, prayer_name in enumerate(PRAYER_NAMES):
prayer_time = prayer_times_data[i]
utc_prayer_time = self.timestamp_to_utc(prayer_time).astimezone(ZoneInfo("UTC"))
prayer_times[prayer_name.lower()] = utc_prayer_time.isoformat()
prayer_times[f"{prayer_name.lower()}_12h"] = self.convert_to_local_12time(prayer_time)
prayer_times[f"{prayer_name.lower()}_24h"] = self.convert_to_local_24time(prayer_time)

# Additional times: Imsak, Isyraq, Dhuha
if prayer_name.lower() == "subuh":
imsak_time = utc_prayer_time - timedelta(minutes=10)
prayer_times["imsak"] = imsak_time.isoformat()
prayer_times["imsak_12h"] = self.convert_to_local_12time(imsak_time.timestamp())
prayer_times["imsak_24h"] = self.convert_to_local_24time(imsak_time.timestamp())

if prayer_name.lower() == "syuruk":
isyraq_time = utc_prayer_time + timedelta(minutes=12)
dhuha_time = utc_prayer_time + timedelta(minutes=15)
prayer_times["isyraq"] = isyraq_time.isoformat()
prayer_times["isyraq_12h"] = self.convert_to_local_12time(isyraq_time.timestamp())
prayer_times["isyraq_24h"] = self.convert_to_local_24time(isyraq_time.timestamp())
prayer_times["dhuha"] = dhuha_time.isoformat()
prayer_times["dhuha_12h"] = self.convert_to_local_12time(dhuha_time.timestamp())
prayer_times["dhuha_24h"] = self.convert_to_local_24time(dhuha_time.timestamp())

return {
"state": data["data"]["place"],
"attributes": {"gps": f"{latitude},{longitude}", **prayer_times}
}
else:
_LOGGER.error(f"Error retrieving prayer times for {entity_id}: Status {response.status}")
return {
"state": "unavailable",
"attributes": {
"location": f"ERROR CODE:{response.status}",
"gps": f"{latitude},{longitude}"
}
}
except aiohttp.ClientError as e:
_LOGGER.error(f"Client error retrieving prayer times for {entity_id}: {e}")
return {
"state": "unavailable",
"attributes": {
"location": "Client error",
"gps": f"{latitude},{longitude}"
}
}
except Exception as e:
_LOGGER.error(f"Unexpected error retrieving prayer times for {entity_id}: {e}")
return {
"state": "unavailable",
"attributes": {
"location": "Unexpected error",
"gps": f"{latitude},{longitude}"
}
}

@staticmethod
def timestamp_to_utc(timestamp):
return datetime.fromtimestamp(timestamp, ZoneInfo("UTC"))

@staticmethod
def convert_to_local_12time(time):
dt = datetime.fromtimestamp(time, ZoneInfo("Asia/Kuala_Lumpur"))
return dt.strftime("%-I:%M %p")

@staticmethod
def convert_to_local_24time(time):
dt = datetime.fromtimestamp(time, ZoneInfo("Asia/Kuala_Lumpur"))
return dt.strftime("%H:%M:%S")

2 changes: 1 addition & 1 deletion custom_components/esolatgps/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/zubir2k/homeassistant-esolatgps/issues",
"requirements": ["pytz"],
"version": "0.1.1"
"version": "0.2.0"
}
Loading

0 comments on commit e17b3d0

Please sign in to comment.