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
484573async 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