Skip to content

Commit ea942c1

Browse files
JohannJohann
authored andcommitted
feat: tarhim starttijd berekend op basis van MP3 duur + 5s buffer
- _get_mp3_duration() leest MP3 duur via Xing/Info VBR header of filesize/bitrate fallback — geen externe library nodig - check_tarhim() berekent: start = Fajr - duur - 5s - restore_delay automatisch ingesteld op duur + 10s - debug logging toont exact starttijdstip in HA logs
1 parent a1bac54 commit ea942c1

1 file changed

Lines changed: 123 additions & 34 deletions

File tree

nida/__init__.py

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
DOMAIN, CONF_CITY, CONF_COUNTRY, CONF_METHOD,
2020
CONF_PLAY_METHOD, CONF_FAJR_SPEAKER, CONF_FAJR_VOLUME, CONF_FAJR_SOUND,
2121
CONF_DAY_SPEAKER, CONF_DAY_VOLUME, CONF_DAY_SOUND,
22-
CONF_SALAWAT_ENABLED, CONF_SALAWAT_SPEAKER, CONF_SALAWAT_VOLUME, CONF_SALAWAT_SOUND,
22+
CONF_TARHIM_ENABLED, CONF_TARHIM_SPEAKER, CONF_TARHIM_VOLUME, CONF_TARHIM_SOUND,
2323
)
2424

2525
_LOGGER = logging.getLogger(__name__)
@@ -122,7 +122,7 @@ def check_prayer_time(now):
122122
_LOGGER.info("Playing adhan for %s", prayer)
123123
hass.async_create_task(play_adhan(hass, entry, prayer_key))
124124

125-
hass.async_create_task(check_salawat(hass, entry, coordinator, now_ts))
125+
hass.async_create_task(check_tarhim(hass, entry, coordinator, now_ts))
126126
hass.async_create_task(check_reminders(hass, entry, coordinator, now_ts, prayers))
127127

128128
entry.async_on_unload(
@@ -314,7 +314,7 @@ async def async_send_notification(
314314
):
315315
"""
316316
Stuur notificatie op basis van type.
317-
notify_type: "prayer" | "pre_adhan" | "salawat" | "suhoor"
317+
notify_type: "prayer" | "pre_adhan" | "tarhim" | "suhoor"
318318
319319
Kritische notificaties:
320320
- iOS: push.sound.critical=1 — doorbreekt Niet Storen + stil profiel
@@ -439,11 +439,70 @@ async def check_reminders(hass, entry, coordinator, now_ts, prayers):
439439
)
440440

441441

442-
async def check_salawat(hass: HomeAssistant, entry: ConfigEntry, coordinator, now_ts: float):
443-
"""Speel salawat voor Fajr tijdens Ramadan."""
442+
def _get_mp3_duration(path: str) -> float:
443+
"""
444+
Lees MP3 duur in seconden — pure Python, geen externe library nodig.
445+
Gebruikt Xing/Info VBR header als beschikbaar, anders filesize/bitrate schatting.
446+
"""
447+
import struct
448+
BITRATES = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]
449+
SAMPLERATES = [44100, 48000, 32000]
450+
451+
try:
452+
with open(path, "rb") as f:
453+
data = f.read()
454+
455+
# Skip ID3v2 tag indien aanwezig
456+
offset = 0
457+
if data[:3] == b"ID3":
458+
size = (
459+
(data[6] & 0x7f) << 21 | (data[7] & 0x7f) << 14 |
460+
(data[8] & 0x7f) << 7 | (data[9] & 0x7f)
461+
)
462+
offset = size + 10
463+
464+
# Zoek eerste geldige MPEG frame header
465+
for i in range(offset, min(offset + 10000, len(data) - 4)):
466+
if data[i] != 0xff or (data[i + 1] & 0xe0) != 0xe0:
467+
continue
468+
b2 = data[i + 2]
469+
bitrate_idx = (b2 >> 4) & 0xf
470+
samplerate_idx = (b2 >> 2) & 0x3
471+
if bitrate_idx in (0, 15) or samplerate_idx >= len(SAMPLERATES):
472+
continue
473+
474+
bitrate = BITRATES[bitrate_idx] * 1000
475+
samplerate = SAMPLERATES[samplerate_idx]
476+
477+
# Xing/Info header aanwezig? → nauwkeurige frame-telling
478+
xing_off = i + 36 # MPEG1 stereo offset
479+
if len(data) > xing_off + 12 and data[xing_off:xing_off + 4] in (b"Xing", b"Info"):
480+
flags = struct.unpack(">I", data[xing_off + 4:xing_off + 8])[0]
481+
if flags & 0x1:
482+
frames = struct.unpack(">I", data[xing_off + 8:xing_off + 12])[0]
483+
return round(frames * 1152 / samplerate, 1)
484+
485+
# Fallback: bestandsgrootte / bitrate
486+
frame_size = 144 * bitrate // samplerate
487+
total_frames = (len(data) - i) // frame_size if frame_size else 0
488+
return round(total_frames * 1152 / samplerate, 1)
489+
490+
except Exception as e:
491+
_LOGGER.warning("Kon MP3 duur niet lezen van %s: %s", path, e)
492+
493+
return 0.0
494+
495+
496+
async def check_tarhim(hass: HomeAssistant, entry: ConfigEntry, coordinator, now_ts: float):
497+
"""
498+
Speel tarhim voor Fajr tijdens Ramadan.
499+
500+
Starttijd = Fajr - duur_van_mp3 - 5 seconden buffer.
501+
De duur wordt eenmalig per run gelezen via _get_mp3_duration() in een executor job.
502+
"""
444503
options = entry.options if entry.options else entry.data
445504

446-
if not options.get(CONF_SALAWAT_ENABLED, True):
505+
if not options.get(CONF_TARHIM_ENABLED, True):
447506
return
448507

449508
try:
@@ -455,30 +514,60 @@ async def check_salawat(hass: HomeAssistant, entry: ConfigEntry, coordinator, no
455514

456515
try:
457516
timings = coordinator.data["data"]["timings"]
458-
today = datetime.now().strftime("%Y-%m-%d")
459-
fajr_ts = datetime.strptime(f"{today} {timings['Fajr']}", "%Y-%m-%d %H:%M").timestamp()
460-
salawat_ts = fajr_ts - (6.5 * 60)
461-
462-
if abs(now_ts - salawat_ts) < 30:
463-
speaker = options.get(CONF_SALAWAT_SPEAKER, ["media_player.adhan_speakers"])
464-
if isinstance(speaker, str): speaker = [speaker]
465-
volume = _get_volume(options, CONF_SALAWAT_VOLUME, 10)
466-
sound = options.get(CONF_SALAWAT_SOUND, "Ramadan [salawat] - Ustaz Hendra.mp3")
517+
today = datetime.now().strftime("%Y-%m-%d")
518+
fajr_ts = datetime.strptime(
519+
f"{today} {timings['Fajr']}", "%Y-%m-%d %H:%M"
520+
).timestamp()
521+
522+
sound = options.get(CONF_TARHIM_SOUND, "")
523+
if not sound:
524+
_LOGGER.warning("Geen tarhim sound geconfigureerd — overgeslagen")
525+
return
526+
527+
# MP3 duur ophalen in executor (blocking file I/O)
528+
sounds_path = hass.config.path("www/nida/sounds")
529+
mp3_path = os.path.join(sounds_path, sound)
530+
duration = await hass.async_add_executor_job(_get_mp3_duration, mp3_path)
531+
532+
if duration <= 0:
533+
_LOGGER.warning(
534+
"Kon duur van %s niet bepalen — tarhim overgeslagen", sound
535+
)
536+
return
537+
538+
# Starttijd = Fajr - duur - 5s buffer
539+
BUFFER_SECONDS = 5
540+
tarhim_ts = fajr_ts - duration - BUFFER_SECONDS
541+
542+
_LOGGER.debug(
543+
"Tarhim timing: Fajr=%s, duur=%.1fs, buffer=%ds → start om %s",
544+
timings["Fajr"], duration, BUFFER_SECONDS,
545+
datetime.fromtimestamp(tarhim_ts).strftime("%H:%M:%S"),
546+
)
547+
548+
if abs(now_ts - tarhim_ts) < 30:
549+
speaker = options.get(CONF_TARHIM_SPEAKER, ["media_player.adhan_speakers"])
550+
if isinstance(speaker, str):
551+
speaker = [speaker]
552+
volume = _get_volume(options, CONF_TARHIM_VOLUME, 10)
467553
media_url = await _get_media_url(hass, f"/local/nida/sounds/{sound}")
468554

469-
_LOGGER.info("Playing salawat: %s", sound)
555+
_LOGGER.info(
556+
"Tarhim afspelen: %s (%.1fs) — eindigt ~5s voor Fajr om %s",
557+
sound, duration, timings["Fajr"],
558+
)
470559
await _play_media_with_volume(
471560
hass, speaker, media_url, volume,
472561
cover_url=_get_logo_url(hass),
473-
restore_delay=float(options.get("salawat_restore_delay", 60)),
562+
restore_delay=duration + BUFFER_SECONDS + 5,
474563
)
475564
await async_send_notification(
476565
hass, entry,
477566
message="Tarhim — Fajr begint binnenkort 🌙",
478-
notify_type="salawat",
567+
notify_type="tarhim",
479568
)
480569
except Exception as e:
481-
_LOGGER.error("Salawat error: %s", e)
570+
_LOGGER.error("Tarhim error: %s", e)
482571

483572

484573
async def async_setup_services(hass: HomeAssistant, entry: ConfigEntry):
@@ -515,16 +604,16 @@ async def handle_test_prayer(call):
515604
prayer = call.data.get("prayer", "dhuhr")
516605
await play_adhan(hass, entry, prayer)
517606

518-
async def handle_test_salawat(call):
519-
"""Test salawat."""
607+
async def handle_test_tarhim(call):
608+
"""Test tarhim."""
520609
options = entry.options if entry.options else entry.data
521-
speaker = call.data.get("speaker", options.get(CONF_SALAWAT_SPEAKER, "media_player.adhan_speakers"))
610+
speaker = call.data.get("speaker", options.get(CONF_TARHIM_SPEAKER, "media_player.adhan_speakers"))
522611
if isinstance(speaker, str):
523612
speaker = [speaker]
524-
raw = call.data.get("volume", options.get(CONF_SALAWAT_VOLUME, 15))
613+
raw = call.data.get("volume", options.get(CONF_TARHIM_VOLUME, 15))
525614
volume = raw / 100 if isinstance(raw, (int, float)) and raw > 1 else float(raw)
526615
volume = max(0.0, min(1.0, volume))
527-
sound = call.data.get("sound", options.get(CONF_SALAWAT_SOUND, "Ramadan [salawat] - Ustaz Hendra.mp3"))
616+
sound = call.data.get("sound", options.get(CONF_TARHIM_SOUND, "Ramadan [salawat] - Ustaz Hendra.mp3"))
528617
media_url = await _get_media_url(hass, f"/local/nida/sounds/{sound}")
529618
await _play_media_with_volume(
530619
hass, speaker, media_url, volume,
@@ -594,7 +683,7 @@ async def handle_test_notification(call):
594683
)
595684

596685
hass.services.async_register(
597-
DOMAIN, "test_salawat", handle_test_salawat,
686+
DOMAIN, "test_tarhim", handle_test_tarhim,
598687
schema=vol.Schema({
599688
vol.Optional("sound"): str,
600689
vol.Optional("speaker"): str,
@@ -679,7 +768,7 @@ def _build_and_write():
679768
sounds_path = os.path.join(os.path.dirname(__file__), "sounds")
680769
fajr_options = []
681770
day_options = []
682-
salawat_options = []
771+
tarhim_options = []
683772

684773
def _label(f):
685774
import re
@@ -695,9 +784,9 @@ def _label(f):
695784
label = _label(f)
696785
if "[fajr]" in fl or ("fajr" in fl and "[" not in fl):
697786
fajr_options.append({"label": label, "value": f})
698-
elif "[salawat]" in fl or "salawat" in fl:
699-
salawat_options.append({"label": label, "value": f})
700-
elif "[day]" in fl or ("adhan" in fl and "fajr" not in fl and "salawat" not in fl):
787+
elif "[tarhim]" in fl or "tarhim" in fl:
788+
tarhim_options.append({"label": label, "value": f})
789+
elif "[day]" in fl or ("adhan" in fl and "fajr" not in fl and "tarhim" not in fl):
701790
day_options.append({"label": label, "value": f})
702791

703792
services = {
@@ -747,15 +836,15 @@ def _label(f):
747836
}
748837
}
749838
},
750-
"test_salawat": {
751-
"name": "Test Salawat",
752-
"description": "Test de Salawat recitatie voor Fajr.",
839+
"test_tarhim": {
840+
"name": "Test Tarhim",
841+
"description": "Test de Tarhim recitatie voor Fajr.",
753842
"fields": {
754843
"sound": {
755844
"name": "Sound",
756-
"description": "Welk salawat wil je afspelen?",
845+
"description": "Welk tarhim wil je afspelen?",
757846
"required": False,
758-
"selector": {"select": {"options": salawat_options}}
847+
"selector": {"select": {"options": tarhim_options}}
759848
},
760849
"speaker": {
761850
"name": "Speaker",

0 commit comments

Comments
 (0)