Islamic Prayers - Time calculations and rules with audio and video

Islamic Prayer Times

Purpose

One of the Five Pillars of Islam is the recitation of prayers 5 times a day at more or less predefined times.
These times depend on the position of the sun in the sky at your location and on the different methods used to calculate these times according to different conventions

There are 5 main prayers in the day:

Prayer Description
Fajr When the sky begins to lighten (dawn).
Dhuhr or Zuhr When the Sun begins to decline after reaching its highest point in the sky.
Asr The time when the length of any object's shadow reaches a factor (usually 1 or 2) of the length of the object itself plus the length of that object's shadow at noon.
Maghrib Soon after sunset.
Isha The time at which darkness falls and there is no scattered light in the sky.

Some of these times could be obtained from the Astro binding. However Asr is not available in the Astro binding and it’s value depends according to different Islamic schools of thought.
The times of Fajr and Isha are also not available reliably in the Astro binding at high latitudes and special calculations are required to obtain these times.

At the required times a call to prayer is performed. This is the call from the Muezzin at the top of the minaret. However this is now usually done with speakers!!

The times for the day are calculated using a python script every day after midnight.
This python script is from http://praytimes.org/

Every minute openHAB will check if one of these times is reached and perform different actions depending on your choice.

Options

In my case, I wanted the call to prayer to be played in the audio sink if the TV is off and a special video played on the TV if the TV is on. I used a spare raspberry pi with a python script mqtt client to play the video on the TV.

Note that the call to prayer played at Fajr is different than the call to prayer played at the other prayer times.

It is also a tradition in some countries to close the blinds and windows and to turn on the lights in the house a few minutes before Maghrib (sunset). The lights are then turned off a few minutes after sunset to save electricity.

I also added an option to play the early prayer (Fajr) only after a predefined time in order to avoid waking up the kids in the middle of the night.

Prerequisites

  1. Python is installed (The scripts are compatible with Python2 and Python3)

  2. Exec binding

  3. JSONPATH transform

  4. Options

Configuration

The files are available from:

Main Python script

Place prayertimes.py in your conf/scripts folder.

Unfortunately, the python script is a little buggy and I do not have the Python skills to fix it.
However there is a way around it. The bug is related to the settings for the prayer calculations.
There are many options but the main ones are the angle of the sun for the calculation of Fajr and Isha. The school or method used for the calculations and the method used for high latitudes.

I have included several examples in the script.
The settings are done at the bottom of the script.

You will need to input your latitude and longitude. (Altitude optional.)

The script will generate a JSON string with the prayer times.

Items

// *****************************
// **       Prayer Times      **
// *****************************

Group  PrayerTimes "Prayer Times" <islam>

// Prayer Times
String PrayerTime_Fajr                         "Fajr [%s]"                            <fajr>     (PrayerTimes)
String PrayerTime_Zuhr                         "Zuhr [%s]"                            <zuhr>     (PrayerTimes)
String PrayerTime_Asr                          "Asr [%s]"                             <asr>      (PrayerTimes)
String PrayerTime_Isha                         "Isha [%s]"                            <isha>     (PrayerTimes)
String PrayerTime_Maghrib                      "Maghrib [%s]"                         <maghrib>  (PrayerTimes)

// Trigger items for prayer times (MQTT binding optional)
Switch PrayerTime_PrayerTrigger     { mqtt=">[mybroker:Misc/PrayerTrigger:command:ON:ON]" }
Switch PrayerTime_PrayerTriggerFajr { mqtt=">[mybroker:Misc/PrayerTriggerFajr:command:ON:ON]" }

// Optional settings and times for Lights on and off before and after Maghrib
Number PrayerTime_MinutesLightsONBeforeMaghrib "Lights ON Before Maghrib [%02d] mins" <clock>    (PrayerTimes)
Number PrayerTime_MinutesLightsOFFAfterMaghrib "Lights OFF After Maghrib [%02d] mins" <clock>    (PrayerTimes)
String PrayerTime_MaghribLightsON              "Maghrib Lights ON [%s]"               <lighton>  (PrayerTimes)
String PrayerTime_MaghribLightsOFF             "Maghrib Lights OFF [%s]"              <lightoff> (PrayerTimes)
Switch PrayerTime_MaghribLightsTrigger
Switch PrayerTime_MaghribPeriod

// Optional Settings for player Fajr
Number PrayerTime_EarlyAdhanTime               "Earliest Adhan [%02d] o'clock"        <clock>    (PrayerTimes)
Switch PrayerTime_PlayEarlyAdhan               "Play Fajr"                            <sound>    (PrayerTimes)

Rules

The first rule executes the python script and retrieves the prayer times:

rule "prayer times"
when
    Time cron "0 2 0 * * ?"
then
    var String PT = executeCommandLine("python /etc/openhab2/scripts/prayertimes.py", 5000)
    PrayerTime_Fajr.postUpdate(transform("JSONPATH", "$.fajr", PT))
    PrayerTime_Zuhr.postUpdate(transform("JSONPATH", "$.dhuhr", PT))
    PrayerTime_Asr.postUpdate(transform("JSONPATH", "$.asr", PT))
    PrayerTime_Maghrib.postUpdate(transform("JSONPATH", "$.maghrib", PT))
    PrayerTime_Isha.postUpdate(transform("JSONPATH", "$.isha", PT))
end

The second rule checks if the current time matches one of the prayer times:

rule "Prayer Time Check"
when
    Time cron "0 * * * * ?" or
    System started
then
    var String currentTime = now.toString("HH:mm")
    var Number currentHour = now.getHourOfDay() //Optional
    if (currentTime == PrayerTime_Fajr.state.toString) {
        if (PrayerTime_PlayEarlyAdhan.state == ON) {
            if (currentHour >= (PrayerTime_EarlyAdhanTime.state as Number)) { //Optional
                PrayerTime_PrayerTriggerFajr.sendCommand(ON)
            } // Optional
        }
    }
    if (currentTime == PrayerTime_Zuhr.state.toString) PrayerTime_PrayerTrigger.sendCommand(ON)
    if (currentTime == PrayerTime_Asr.state.toString) PrayerTime_PrayerTrigger.sendCommand(ON)
    if (currentTime == PrayerTime_Maghrib.state.toString) PrayerTime_PrayerTrigger.sendCommand(ON)
    if (currentTime == PrayerTime_Isha.state.toString) PrayerTime_PrayerTrigger.sendCommand(ON)

    //Optional checks for Maghrib lights routines
    if (currentTime == PrayerTime_MaghribLightsON.state.toString) {
        PrayerTime_MaghribPeriod.postUpdate(ON)
        PrayerTime_MaghribLightsTrigger.sendCommand(ON)
    }
    if (currentTime == PrayerTime_MaghribLightsOFF.state.toString) {
        PrayerTime_MaghribPeriod.postUpdate(OFF)
        PrayerTime_MaghribLightsTrigger.sendCommand(OFF)
    }
end

Optional rules

Rule to calculate the times when the lights should turn on before Maghrib and off after Magrhib
This relies on a Javascript transformation

The rule:

rule "Maghrib lights time change"
when
    Item PrayerTime_Maghrib changed or
    Item PrayerTime_MinutesLightsONBeforeMaghrib changed or
    Item PrayerTime_MinutesLightsOFFAfterMaghrib changed
then
    var String PT = PrayerTime_Maghrib.state.toString + "#-" + PrayerTime_MinutesLightsONBeforeMaghrib.state.toString
    PrayerTime_MaghribLightsON.postUpdate(transform("JS", "maghriblights.js", PT))
    PT = PrayerTime_Maghrib.state.toString + "#" + PrayerTime_MinutesLightsOFFAfterMaghrib.state.toString
    PrayerTime_MaghribLightsOFF.postUpdate(transform("JS", "maghriblights.js", PT))
end

The Javascript transform: (Put in maghriblights.js in the transform folder)
maghriblight.js:

(function(i) {
    if (i == 'NULL') { return i; }
    if (i == '-') { return 'Undef'; }
    var maghribtime = i.split('#')[0];
    var minutes = parseInt(i.split('#')[1]);
    var MM = parseInt(maghribtime.split(':')[1]);
    var HH = parseInt(maghribtime.split(':')[0]);

    if (minutes < 0) {
        minutes = -minutes;
        if ((MM - minutes) < 0) {
            MM = MM + (60 - minutes);
            HH = HH - 1;
        } else {
            MM = MM - minutes;
        }
    } else {
        if ((MM + minutes) > 59) {
            MM = MM - (60 - minutes);
            HH = HH + 1;
        } else {
            MM = MM + minutes;
        }
    }

    var StMM = '';
    if (MM < 10) {
        StMM = StMM.concat('0').concat(MM);
        StMM = '0' + MM;
    } else {
        StMM = StMM.concat(MM);
    }

    var StHH = '';
    StHH = StHH.concat(HH);

    var returnString =  StHH + ':' + StMM;
    return returnString
})(input)

This first rule turns the lights on and off if we are at home

rule "Maghrib Lights"
when
    Item PrayerTime_MaghribLightsTrigger received command
then
    if (House_HomeAway.state.toString == "home") { //House_HomeAway item for presence
        MaghribLights.sendCommand(receivedCommand)
    }
end

Rule for edge case when we come home during the period around Maghrib when the lights should be on but are not because we were away.
This would have to be integrated in your own presence rules

rule "House Presence"
when
    Item House_HomeAway changed
then
    //General
    // ... STUFF DO DO

    //Coming home
    if (triggeringItem.state.toString == "home") {

        // Coming home during Maghrib Lights Period
        if (PrayerTime_MaghribPeriod.state == ON) {
            MaghribLights.sendCommand(ON)
        }
    //OTHER STUFF DO DO
end

Options for sound and video

Raspberry PI, python and MQTT

The main interest for me was to have the call to prayer played out at the correct times. As a bonus, when the television is on, the current program is paused, the TV input switched to the AV input to which I have connected a RaspberryPi One B. The raspberry is loaded with the Raspian Lite image. I have used the AV output but you could use the HDMI output.

A small python script in the home folder is used to receive an mqtt message and play a video.

To try the script do:

python adhan.py

Some dependencies for python such as paho-mqtt may have to be installed depending on your set-up.

The script is available at:

You will need to input your mqtt broker settings in the script

To run the script automatically on start-up the following line is added at the end of the rc.local file in the /etc directory of the Raspberry PI

sudo -H -u pi python /home/pi/adhan.py > /tmp/adhan.out > /tmp/adhan.err &

The script will output to the file /tmp/adhan.out and errors will be logged in /tmp/adhan.err. This is useful for debugging.

!! DO NOT FORGET THE & at the end of the line or your Raspberry PI will not come out of boot! The & will instruct the script to run in the background.

Video Files

The video files:

adhan.mp4 - https://drive.google.com/open?id=1bd6NP9qYwwuFixFHZclIu8EYHw7lfyA4
adhanfajr.mp4 - https://drive.google.com/open?id=1bd6NP9qYwwuFixFHZclIu8EYHw7lfyA4

Will need to be placed in a /share folder in the home directory of the PI. Or anywhere else. Make sure your change the Python script to reflect the location of the files.

Rules in openHAB

This the rule that make it happen.
Please adapt to your set-up:

rule "Prayer Trigger"
when
    Item PrayerTime_PrayerTrigger received command or
    Item PrayerTime_PrayerTriggerFajr received command
then
    if (House_HomeAway.state.toString == "home") { #If we are at home
        var prayerDuration = 0
        var String soundFile = "adhan.mp3" #Default audio
        if (triggeringItem.name.toString == "PrayerTime_PrayerTrigger") {
            prayerDuration = 100 # Duration of adhan.mp4
        } else {
            prayerDuration = 180 # Duration of adhanfajr.mp4
            soundFile = "adhanfajr.mp3" # Audio file for Fajr
        }
        if (LivingRoom_TVON.state == ON) { # If the TV is on
            if ((LivingRoom_TVSource.state.toString == "HDMI1") || (LivingRoom_TVSource.state.toString == "HDMI-CEC")) { #if the TV is playing from satellite or chromecast
                if (timer === null) {
                    val tvVolume = LivingRoom_TVVolume.state as Number # Save current TV volume
                    val tvLightColour = (LivingRoom_TVLightLeft_Colour.state as HSBType) # Save the current TV Lights colour settings
                    val tvLightLevel = LivingRoom_TVLightLeft_Level.state as Number # Save the current TV Lights level
                    SkyBox.sendCommand("pause") # Send pause to satellite box
                    LivingRoom_TVRemoteKey.sendCommand("KEY_PAUSE") # Send pause to TV
                    LivingRoom_TVVolume.sendCommand(65) # Increase TV volume to 65
                    Thread::sleep(1300) # Wait for Raspberry pi to run video
                    LivingRoom_TVSource.sendCommand("SCART") # Change TV source
                    LivingRoom_TVLightLeft_Colour.sendCommand(new HSBType(new DecimalType(25), new PercentType(100), new PercentType(80))) # Change TV Lights colour
                    LivingRoom_TVLightLeft_Level.sendCommand(100) # Change TV Lights level
                    timer = createTimer(now.plusSeconds(prayerDuration), [ |
                        LivingRoom_TVSource.sendCommand("HDMI1") # Restore TV source
                        LivingRoom_TVVolume.sendCommand(tvVolume) # Restore TV Volume
                        LivingRoom_TVLightLeft_Colour.sendCommand(tvLightColour) # Restore TV Lights colour
                        LivingRoom_TVLightLeft_Level.sendCommand(tvLightLevel) # Restore TV Lights level
                        SkyBox.sendCommand("play") # Play the satellite box
                        LivingRoom_TVRemoteKey.sendCommand("KEY_PLAY") # Play the TV
                        timer = null // reset the timer
                    ] )
                }
            }
        } else {
            playSound(soundFile) # Play audio on the default audio sink
        }
    }
end

Audio Files

Your will require the two audio files:

adhan.mp3 - https://drive.google.com/open?id=1AgQQfvYerchEhArLVEhkCLPbF7D1r8uZ
adhanfajr.mp3 - https://drive.google.com/open?id=1lSVZM8KiRpWxS_2XsqEwyiUdWWKCqb_y

Place the two files in the conf/sounds folder of openHAB

Eid Mubarak!

7 Likes

Excellent write up. Thanks for posting!

Some thought I had while going through this:

  • I wouldn’t take on the task of rewriting the Python script, but the Astro binding does provide the sun angle in a channel so I beleive the times could theoretically be implemented in Rules using the Astro binding. If someone wanted a challenge that could be a fun experiment.
  • I don’t know if this is still a problem. It used to be the case that OH would change the permissions of all of its files, including those files in the scripts folder. The real purpose of the scripts folder is for OH scripts, not shell or Python scripts so it used to strip the execute permissions off of the scripts. I think this might still happen on upgrade of OH. So I usually recommend putting scripts like this in ~openhab/bin which if you have an installed OH ends up in /var/lib/openhab2/bin.

  • Polling works and sometimes it is the only way around things. However, in this case one could use Timers. With Timers you can have the notification arrive to the second and you avoid unnecessary activity. First switch your Items to a DateTime Item.

import org.eclipse.xtext.xbase.lib.Functions
import java.util.Map

val Map<String, Timer> timers = createHashMap

val Functions$Function1<String, DateTimeType> toDateTimeType = [ hhmm |
    val hours = Integer::parseInt(hhmm.split(":").get(0))
    val mins = Integer::parseInt(hhmm.split(":").get(1))

    // Jump to tomorrow midnight and subtract to avoid problems at daylight savings changes
    val timerTime = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-hours).plusMinues(mins)

    new DateTimeType(timerTime.toString)
]

rule "prayer times"
when
    System started or
    Time cron "0 2 0 * * ?"
then
    var String PT = executeCommandLine("python /etc/openhab2/scripts/prayertimes.py", 5000)
    PrayerTime_Fajr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.fajr", PT)))
    PrayerTime_Zuhr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.dhuhr", PT)))
    PrayerTime_Asr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.asr", PT)))
    PrayerTime_Maghrib.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.maghrib", PT)))
    PrayerTime_Isha.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.isha", PT)))
end

rule "Prayer time updated"
when
    Member of PrayerTimes receives update // put all the times into a Group
then
    val prayer = triggeringItem.name.split("_").get(1)

    timers.get(prayer)?.cancel
    timers.put(prayer, createTimer(timerTime, [ |
        PrayerTime_PrayerTrigger.sendCommand(ON)
        timers.put(prayer, null)
    ])

    if(prayer == "Maghrib"){
        val lightsTimer = prayer + "Lights"

        val time = new DateTime((triggeringItem.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli)

        val onTime = time.minusMinutes((PrayerTime_MinutesLightsONBeforeMaghrib.state as Number))
        val offTime = time.plusMinutes((PrayerTime_MinuesLightsOFFAfterMaghrib.state as Number))

        // ON timer
        timers.get(lightsTimer+"ON")?.cancel
        timers.put(lightsTimer+"ON", createTimer(onTime, [ |
            PrayerTime_MaghribPeriod.postUpdate(ON)
            PrayerTime_MaghribLightsTrigger.sendCommand(ON)            
        ])  

        // OFF timer
        timers.get(lightsTimer+"OFF")?.cancel
        timers.put(lightsTimer+"OFF", createTimer(offTime, [ |
            PrayerTime_MaghribPeriod.postUpdate(OFF)
            PrayerTime_MaghribLightsTrigger.sendCommand(OFF)        
        ])      
    }
end  

Great post. I love to see these sorts of tutporials in particular. I’m inspired. Maybe I’ll bump up exploring integrating OH with Dexcom. Thanks for posting!

Rich
Theory of operation:

We convert the HH:MM returned from the script to an absolute time for today and store in a DateTimeType. When the DateTime Type Items are update we trigger a Rule and create the timer to trigger that prayer at that time. For the Maghrib time we also set ON and OFF lights timers as well.

Because the script is called again at System start and at 02:00, you will always have Timers running to go off for the right times today. Now there is no need for polling.

The Maghrib lights time change rule will need to be updated to reschedule the Timer if the light minutes changes. But you may not need the js transform any longer since you will already have DateTime Items and can use the % formatting to convert the time to display as desired.

1 Like

I think that will work for the re-schedule of the lights
Can you check, please?
I shamelessly copied your code…

rule "Maghrib lights time change"
when
    Item PrayerTime_MinutesLightsONBeforeMaghrib changed or
    Item PrayerTime_MinutesLightsOFFAfterMaghrib changed
then
    val time = new DateTime((PrayerTime_Maghrib.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli)

    val onTime = time.minusMinutes((PrayerTime_MinutesLightsONBeforeMaghrib.state as Number))
    val offTime = time.plusMinutes((PrayerTime_MinutesLightsOFFAfterMaghrib.state as Number))

    // ON timer
    timers.get(lightsTimer+"ON")?.cancel
    timers.put(lightsTimer+"ON", createTimer(onTime, [ |
        PrayerTime_MaghribPeriod.postUpdate(ON)
        PrayerTime_MaghribLightsTrigger.sendCommand(ON)            
    ])  

    // OFF timer
    timers.get(lightsTimer+"OFF")?.cancel
    timers.put(lightsTimer+"OFF", createTimer(offTime, [ |
        PrayerTime_MaghribPeriod.postUpdate(OFF)
        PrayerTime_MaghribLightsTrigger.sendCommand(OFF)        
    ])      
end

I see what you are doing here, but I wanted to keep the option of doing different action at the different times. There are many traditions that will required certain things to happen depend on what prayer is to be performed.

You might need to call intValue in the call to minusMinutes and plusMinutes. I’ve had mixed results in the Rules DSL being able to convert Numbers to primitives and the primitive is needed by those two methods.

I forgot to add a line at the end of those two timers to timers.put(lightsTimer+"ON", null) when the Timer body is done. It doesn’t really matter but I like to clean up.

Thank you, I will try all that after the end of Ramadan. Everything works at the moment and I don’t want to start experimenting in the Holy Month. I’ll let you know soon after the 15th of June.

In that case I would do something like the following to make it more clear to users who come to adopt this later:

import org.eclipse.xtext.xbase.lib.Functions
import java.util.Map

val Map<String, Timer> timers = createHashMap

val Functions$Function1<String, DateTimeType> toDateTimeType = [ hhmm |
    val hours = Integer::parseInt(hhmm.split(":").get(0))
    val mins = Integer::parseInt(hhmm.split(":").get(1))

    // Jump to tomorrow midnight and subtract to avoid problems at daylight savings changes
    val timerTime = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-hours).plusMinues(mins)

    new DateTimeType(timerTime.toString)
]

rule "prayer times"
when
    System started or
    Time cron "0 2 0 * * ?"
then
    var String PT = executeCommandLine("python /etc/openhab2/scripts/prayertimes.py", 5000)
    PrayerTime_Fajr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.fajr", PT)))
    PrayerTime_Zuhr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.dhuhr", PT)))
    PrayerTime_Asr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.asr", PT)))
    PrayerTime_Maghrib.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.maghrib", PT)))
    PrayerTime_Isha.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.isha", PT)))
end

rule "Prayer time updated"
when
    Member of PrayerTimes receives update // put all the times into a Group
then
    val prayer = triggeringItem.name.split("_").get(1)

    timers.get(prayer)?.cancel
    timers.put(prayer, createTimer(timerTime, [ |
        PrayerTime_PrayerTrigger.sendCommand(prayer) // change this to a String Item
        timers.put(prayer, null)
    ])

    if(prayer == "Maghrib"){
        val lightsTimer = prayer + "Lights"

        val time = new DateTime((triggeringItem.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli)

        val onTime = time.minusMinutes((PrayerTime_MinutesLightsONBeforeMaghrib.state as Number))
        val offTime = time.plusMinutes((PrayerTime_MinuesLightsOFFAfterMaghrib.state as Number))

        // ON timer
        timers.get(lightsTimer+"ON")?.cancel
        timers.put(lightsTimer+"ON", createTimer(onTime, [ |
            PrayerTime_MaghribPeriod.postUpdate(ON)
            PrayerTime_MaghribLightsTrigger.sendCommand(ON)            
        ])  

        // OFF timer
        timers.get(lightsTimer+"OFF")?.cancel
        timers.put(lightsTimer+"OFF", createTimer(offTime, [ |
            PrayerTime_MaghribPeriod.postUpdate(OFF)
            PrayerTime_MaghribLightsTrigger.sendCommand(OFF)        
        ])      
    }
end 

rule "Fajr"
when
    Item PrayerTime_PrayerTrigger received command "Fajr" // only works in 2.3 release
then
    // perform actions for Fajr
end

rule "Zuhr"
when
    Item PrayerTime_PrayerTrigger received command "Zahr"
then
    // perform actions for Zahr
end

// and so on

This makes it clear to the users attempting to adopt this exactly where they would need to edit to implement the actions to take place at the start of that prayer time.

It isn’t more efficient or anything like that but I do think it is easier to read for a beginner programmer. They never have to touch to part that handles the calculations and they have a nice and clear Rule dedicated to each time.

I’ve been thinking on this one some more. I don’t know how variable the before/after timers would be but it occured to me to make the code completely generic so you can have activities that occur before and after all the prayers you can do something like the following (note, I noticed a couple of bugs in the above so rewriting the full set of Rules):

Group:DateTime PrayerTimes "Prayer Times" <islam>

DateTime PrayerTime_Fajr "Fajr [%1$tH:%1$tM]" (PrayerTimes)
DateTime PrayerTime_Zuhr "Zuhr [%1$tH:%1$tM]" (PrayerTimes)
DateTime PrayerTime_Asr "Asr [%1$tH:%1$tM]" (PrayerTimes)
DateTime PrayerTime_Isha "Isha [%1$tH:%1$tM]" (PrayerTimes)
DateTime PrayerTime_Maghrib "Maghrib [%1$tH:%1$tM]" (PrayerTimes)

String PrayerTime_PrayerTrigger
String PrayerTime_BeforePrayerTrigger 
String PrayerTime_AfterPrayerTrigger 

Group:Number BeforePrayerMinutes
Group:Number AfterPrayerMinutes

Number PrayerTime_MinutesBeforeMaghrib (BeforePrayerMinutes)
Number PrayerTime_MinutesAfterMaghrib (AfterPrayerMinutes)

// Optional settings and times for Lights on and off before and after Maghrib
String PrayerTime_MaghribLightsON              "Maghrib Lights ON [%s]"               <lighton>  (PrayerTimes)
String PrayerTime_MaghribLightsOFF             "Maghrib Lights OFF [%s]"              <lightoff> (PrayerTimes)
Switch PrayerTime_MaghribLightsTrigger
Switch PrayerTime_MaghribPeriod
import org.eclipse.xtext.xbase.lib.Functions
import java.util.Map

val Map<String, Timer> timers = createHashMap

// Converts HH:MM to a DateTimeType for today
val Functions$Function1<String, DateTimeType> toDateTimeType = [ hhmm |
    val hours = Integer::parseInt(hhmm.split(":").get(0))
    val mins = Integer::parseInt(hhmm.split(":").get(1))

    // Jump to tomorrow midnight and subtract to avoid problems at daylight savings changes
    val timerTime = now.withTimeAtStartOfDay.plusDays(1).minusHours(24-hours).plusMinues(mins)

    new DateTimeType(timerTime.toString)
]

// Create a Timer that sendCommand to trigger with cmd  at the passed in time
val Procedures$Procedure5<Map<String, Timer>, String, DateTimeType, String, String> createPrayerTimer = [ timers, key, time, trigger, cmd |
    val timerTime = new DateTime(time.getZonedDateTime.toInstant.toEpochMilli)
    timers.get(key)?.cancel
    timers.put(key, createTimer(timerTime, [ |
        sendCommand(trigger, cmd)
        timers.put(key, null)
    ])
]

val Procedures$Procedure2<Map<String, Timer>, String> prayerTrigger = [ timers, prayer |
    if(House_HomeAway.state.toString == "home") { // If we are at home
        var prayerDuration = if(prayer == "Fajr") 180 else 100
        var String soundFile = if(prayer == "Fajr") "adhanfajr.mp3" else "adhan.mp3" 
        var String topic = if(prayer == "Fajr") "Misc/PrayerTriggerFajr" else "Misc/PrayerTrigger"

        if(LivingRoom_TVSource.state.toString == "HDMI1" || LivingRoom_TV_Source.state.toString == "HDMI-CEC") {
            if(timers.get("prayerDuration") === null) {
                // save the current states of the TV and Lights Items
                val tvStates = storeStates(LivingRoom_TVSource, LivingRoom_TVVolume, LivingRoom_TVLightLeft_Colour, LivingRoom_TVLightLeft_Level)

                // Change the TV
                SkyBox.sendCommand("pause")
                LivingRoom_TVRemoteKey.sendCommand("KEY_PAUSE")
                LivingRoom_TVVolume.sendCommand(65)
                publish("mybroker", topic, "ON") // trigger the video on the RPI 
                Thread::sleep(1300) // Where do you start playing the video?
                LivingRoom_TVSource.sendCommand("SCART")
                LivingRoom_TVLightLeft_Colour.sendCommand(new HSBType(new DecimalType(25), new PercentType(100), new PercentType(80)))
                LivingRoom_TVLightLeft_Level.sendCommand(100)

                // Restore the TV
                timers.put("prayerDuration", createTimer(now.plusSeconds(prayerDuration), [ |
                    restoreStates(tvStates)
                    SkyBox.sendCommand("play")
                    LivingRoom_TVRemoteKey.sendCommand("KEY_PLAY")
                    timers.put("prayerDuration", null)
                ]
            }
        } 
    }
    else {
        playSound(soundFile)
    }
]

// Update the prayer start times based on the results of the Python script
rule "prayer times"
when
    System started or
    Time cron "0 2 0 * * ?"
then
    var String PT = executeCommandLine("python /etc/openhab2/scripts/prayertimes.py", 5000)
    PrayerTime_Fajr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.fajr", PT)))
    PrayerTime_Zuhr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.dhuhr", PT)))
    PrayerTime_Asr.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.asr", PT)))
    PrayerTime_Maghrib.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.maghrib", PT)))
    PrayerTime_Isha.postUpdate(toDateTimeType.apply(transform("JSONPATH", "$.isha", PT)))
end

// Reset the timers when a prayer time is updated
rule "Prayer time updated"
when
    Member of PrayerTimes receives update // put all the times into a Group
then
    val prayer = triggeringItem.name.split("_").get(1)
    createPrayerTimer.apply(timers, prayer, triggeringItem.state, "PrayerTime_PrayerTrigger", prayer)

    val prayerTime = new DateTime((triggeringItem.state as DateTimeType).getZonedDateTime.toInstant.toEpochMilli)
    val beforePrayerMins = BeforePrayerMinutes.members.findFirst[ min | min.name == "PrayerTime_MinutesBefore"+prayer ]
    val afterPrayerMins = AfterPrayerMinutes.members.findFirst[ min | min.name == "PrayerTime_MinutesAfter"+prayer ]

    if(beforePrayerMins !== null) {
        val time = new DateTimeType(prayerTime.minusMinutes((beforePrayerMins.state as Number).intValue).toString)
        createPrayerTimer.apply(timers, "before"+prayer, time, "PrayerTime_BeforePrayerTrigger", prayer)
    }
    if(afterPrayerMins !== null){
        val time = new DateTimeType(prayerTime.plusMinutes((afterPrayerMins.state as Number).intValue).toString)
        createPrayerTimer.apply(timers, "after"+prayer, time, "PrayerTime_AfterPrayerTrigger", prayer)
    }

end 


// Fajr is handled slight differently 
rule "Fajr"
when
    Item PrayerTime_PrayerTrigger received command "Fajr"
then
    if(PrayerTime_PlayEarlyAdhan.state == ON) {
        if(now.getHourOfDay() >= (PrayerTime_EarlyAdhanTime.state as Number)) {
            prayerTrigger.apply(timers, receivedCommand)
        }
    }
end

rule "Prayer Trigger other times"
when
    Item PrayerTime_PrayerTrigger received command "Zuhr" or
    Item PrayerTime_PrayerTrigger received command "Asr" or
    Item PrayerTime_PrayerTrigger received command "Maghrib" or
    Item PrayerTime_PrayerTrigger received command "Isha"
then
    prayerTrigger.apply(timers, receivedCommand)
end

rule "Before Maghrib"
when
    Item PrayerTime_BeforePrayerTrigger received command "Maghrib"
then
    PrayerTime_MaghribPeriod.postUpdate(ON)
    PrayerTime_MaghribLightsTrigger.sendCommand(ON)                
end

rule "After Maghrib"
when
    Item PrayerTime_AfterPrayerTrigger received command "Maghrib"
then
    PrayerTime_MaghribPeriod.postUpdate(OFF)
    PrayerTime_MaghribLightsTrigger.sendCommand(OFF)        
end

Theory of Operation:

At system start and at 02:00 the Python script is executed and the ParyerTime Items are updated with the proper times for today.

When any member of ParyerTimes is updated a Rule triggers to create a Timer to go off at that time. If there exists an Item in the BeforePrayerMinutes group or the AfterPrayerMinutes group for that specific payer, timers are created for those minus/plus that number of minutes of the Item.

At the start of a prayer PrayerTime_PrayerTrigger receives a command with the prayer that is starting as a String. This triggers one or more Rules that perform the activities to take place when a prayer starts.

The given number of minutes before a prayer starts PrayerTime_BeforePrayerTrigger receives a command with the prayer that is about to start as a String. This performs the activities to take place N minutes before that prayer starts.

The given number of minutes after a prayer starts PrayerTime_AfterPrayerTrigger receives a command with the prayer that has just completed as a String. This performes the activities to take place N minutes after the prayer ends.

All of the logic for what to do when a prayer starts is centralized in a lambda though it could all be in a single rule. I made some changes to what to do when the prayer starts. Instead of having separate Items for the MQTT message, I use the publish action. I also use storeStates and restoreStates to save and restore the states of the TV.

Except for the elimination of the polling, I don’t think the above is any better or worse than any other approach. But it was a fun exercise to figure it out.

Thank you. More food for thought…
I may not take everything on board but we never know.
I like the savestate/restorestate trick.

Hm, fun task indeed. But it seems that the astro binding has some kind of “bug” when it comes to the event times. I might be wrong, but have a look at the following:

The calculation of noon (green frames) is off by a minute in the astro binding compared to the results of the playtimes.py script as well as some randomly picked website for calculation of the same. Well, even the results for sunrise (blue frames) and sunset (red frames) differ between all three calculations.

The calculation method in the astro binding seems to be derived from http://www.suncalc.net as stated in the code:

https://github.com/eclipse/smarthome/blob/master/extensions/binding/org.eclipse.smarthome.binding.astro/src/main/java/org/eclipse/smarthome/binding/astro/internal/calc/SunCalc.java

But even some results from this website are off by a minute compared with the astro binding:

suncalc

Interesting survey of the results. It certainly looks like a rounding error in the Astro binding or something like that.

It might be worth going an issue on the astro binding. For something like lighting it probably doesn’t matter but for some uses that minute could be important.

In this case, the minute IS important.

I live in Watford approx 20 miles NE of London. The London central mosque hours are of by a minute or two depending on the time of the year.

But I get what you are saying. It should only depend on latitude and longitude. There are many website that provide sunrise and sunset and they are all within a minute or two of each other.

I am not saying that the python script gives the most accurate answer but I have stood outsite at sunset several time and the sunset occurred within that minute.

@rlkoshak

Hi Rich,

I typed in your rules (Better typing it in to learn rather than copy and paste)
Couple of bits:

I get this:

2018-06-21 11:09:49.761 [WARN ] [me.internal.engine.RuleContextHelper] - Variable 'timers' on rule file 'prayertimes.rules' cannot be initialized with value 'createHashMap': The name 'createHashMap' cannot be resolved to an item or type; line 4, column 33, length 13

And the same warning about it in VScode…

I think there are a couple of brackets missing in your code:

                // Restore the TV
                timers.put("prayerDuration", createTimer(now.plusSeconds(prayerDuration), [ |
                    restoreStates(tvStates)
                    SkyBox.sendCommand("play")
                    LivingRoom_TVRemoteKey.sendCommand("KEY_PLAY")
                    timers.put("prayerDuration", null)
                ]))

And

val Procedures$Procedure5<Map<String, Timer>, String, DateTimeType, String, String> createPrayerTimer = [ timers, key, time, trigger, cmd |
    val timerTime = new DateTime(time.getZonedDateTime.toInstant.toEpochMilli)
    timers.get(key)?.cancel
    timers.put(key, createTimer(timerTime, [ |
        sendCommand(trigger, cmd)
        timers.put(key, null)
    ]))
]

Missing closing brackets after the ]

@rlkoshak

This seems to have done the trick!!

import java.util.Map
import java.util.HashMap
import org.eclipse.xtext.xbase.lib.Functions

val Map<String, Timer> timers = new HashMap<String, Timer> //createHashMap

Doh!, stupid mistake. It’s newHashMap.

That’s likely. I typed it up on my phone.

That works too.

Ha, good decision! I tried and partially failed. Not that it’s not possible but I found that developing such a rule is soooo time consuming. With each step I added it extended the time to compile into byte code. With my machine (an intel NUC with OH2.3 in a docker container) it finally took almost 10min each time I saved the code. :frowning:

Anyways, I want to share the code and maybe someone else can pick it up and improve the performance significantly.

But be warned: it’s incomplete in a way that currently one can only calculate the times for ISNA due to some missing null checks. Basically if the angles used in the calculation process are larger then 16° there will be inputs to var Double T = Math.acos() that is outside the -1...1 range.

Let me first start with the results:

ptweety@nuc:~
$ python /opt/docker/oh-python/conf/scripts/prayertimes.py
{'isha': '23:37', 'asr': '17:49', 'sunset': '21:44', 'dhuhr': '13:29', 'maghrib': '21:44', 'imsak': '03:11', 'midnight': '01:29', 'sunrise': '05:14', 'fajr': '03:21'}
2018-06-26 22:00:00.007 [INFO ] [script.pray.rules] - Using method: 'ISNA' with parameters [10.0, 1, 15.0, 0.0, 1, 0.0, 1, 15.0, 0, 0]
2018-06-26 22:00:00.054 [INFO ] [script.pray.rules] - Imsak -> 03:11
2018-06-26 22:00:00.055 [INFO ] [script.pray.rules] - Fajr -> 03:21
2018-06-26 22:00:00.057 [INFO ] [script.pray.rules] - Sunrise -> 05:14
2018-06-26 22:00:00.058 [INFO ] [script.pray.rules] - Dhuhr -> 13:29
2018-06-26 22:00:00.059 [INFO ] [script.pray.rules] - Asr -> 17:49
2018-06-26 22:00:00.060 [INFO ] [script.pray.rules] - Sunset -> 21:44
2018-06-26 22:00:00.061 [INFO ] [script.pray.rules] - Maghrib -> 21:44
2018-06-26 22:00:00.062 [INFO ] [script.pray.rules] - Isha -> 23:37
2018-06-26 22:00:00.063 [INFO ] [script.pray.rules] - Midnight -> 01:29

This is the configuration of prayertimes.py. (Replace x.xxxxxx,y.yyyyyy,zzz with your own coordinates in the some way you provide the data to the astro binding):

    prayTimes = PrayTimes('ISNA');
    prayTimes.adjust( {'highLats':'AngleBased', 'fajr': 15, 'isha': 15 } );

    #Enter here your latitude and longitude (and your altitude (optional))
    times = prayTimes.getTimes(date.today(), (xx.xxxxxx,y.yyyyyy,zzz), 2);
    print times;

This is the complete pray.rules file:

import org.eclipse.xtext.xbase.lib.Functions
import java.util.Calendar
import java.util.TimeZone
import java.util.Map
import java.util.List

val logName = "pray.rules"

// START USER SETTINGS
val Double LATITUDE = xx.xxxxxx // same as for astro
val Double LONGITUDE = y.yyyyyy // same as for astro
val Double ALTITUDE = zzz.0 // same as for astro
val Integer METHOD = 1 // 0-7 with default: 0 -> MWL and 7 -> is your own adjusted method (see below)
val Integer HIGHLAT = 1 // 0-3 with defaul: 0 -> NONE (see below)
val List<Integer> TUNE = newArrayList(0, 0, 0, 0, 0, 0, 0, 0, 0) // defined in minutes (optional)
// END USER SETTINGS

// Calculation Methods
val Integer METHOD_MWL = 0 // Muslim World League
val Integer METHOD_ISNA = 1 // Islamic Society of North America (ISNA)
val Integer METHOD_EGYPT = 2 // Egyptian General Authority of Survey
val Integer METHOD_MAKKAH = 3 // Umm Al-Qura University, Makkah
val Integer METHOD_KARACHI = 4 // University of Islamic Sciences, Karachi
val Integer METHOD_TEHRAN = 5 // Institute of Geophysics, University of Tehran
val Integer METHOD_JAFARI = 6 // Shia Ithna-Ashari, Leva Institute, Qum
val Integer METHOD_ADJUST = 7 // Define your own method

val List<String> METHOD_NAMES = newArrayList("MWL", "ISNA", "Egypt", "Makkah", "Karachi", "Tehran", "Jafari", "Self Adjusted") // 0 - 7

// Adjust Method for Higher Latitudes
val Integer HIGHLAT_NONE = 0 // no adjustment
/**
 * HIGHLAT_ANGLEBASED
 * This is an intermediate solution, used by some recent prayer time calculators.
 * Let α be the twilight angle for Isha, and let t = α/60. The period between
 * sunset and sunrise is divided into t parts. Isha begins after the first part.
 * For example, if the twilight angle for Isha is 15, then Isha begins at the end
 * of the first quarter (15/60) of the night. Time for Fajr is calculated similarly.
 */
val Integer HIGHLAT_ANGLEBASED = 1 // angle/60th of night
/**
 * HIGHLAT_ONESEVENTH
 * In this method, the period between sunset and sunrise is divided into seven parts.
 * Isha begins after the first one-seventh part, and Fajr is at the beginning of the
 * seventh part.
 */
val Integer HIGHLAT_ONESEVENTH = 2 // 1/7th of night
/**
 * HIGHLAT_NIGHTMIDDLE
 * In this method, the period from sunset to sunrise is divided into two halves.
 * The first half is considered to be the "night" and the other half as "day break".
 * Fajr and Isha in this method are assumed to be at mid-night during the abnormal
 * periods.
 */
val Integer HIGHLAT_NIGHTMIDDLE = 3 // middle of night

// Asr Juristic Method
val Integer JURISTIC_STANDARD = 1 // Shafii, Maliki, Jafari and Hanbali (shadow factor = 1)
val Integer JURISTIC_HANAFI = 2 // Hanafi school of tought (shadow factor = 2)

// Midnight Modes
val Integer MIDNIGHT_STANDARD = 0 // Mid Sunset to Sunrise
val Integer MIDNIGHT_JAFARI = 1 // Mid Sunset to Fajr

// Value defined as degrees or minutes
val Integer VALUE_DEGREES = 0
val Integer VALUE_MINUTES = 1

val Integer TIMES_IMSAK = 0 // Stop eating Sahur (for fasting), slightly before Fajr
val Integer TIMES_FAJR = 1 // Sky begins to lighten (dawn)
val Integer TIMES_SUNRISE = 2 // First part of the Sun appears above the horizon
//val Integer TIMES_ZAWAL = 3 // Zawal (Solar Noon): Sun reaches its highest point in the sky
val Integer TIMES_DHUHR = 3 // Sun begins to decline after reaching its highest point in the sky, slightly after solar noon
val Integer TIMES_ASR = 4 // When the length of any object's shadow reaches a factor (usually 1 or 2) of the length of the object itself plus the length of that object's shadow at noon
val Integer TIMES_SUNSET = 5 // Sun disappears below the horizon
val Integer TIMES_MAGHRIB = 6 // Soon after sunset (dusk)
val Integer TIMES_ISHA = 7 // Darkness falls and there is no scattered light in the sky
val Integer TIMES_MIDNIGHT = 8 // Mean time from sunset to sunrise (or sometimes from Maghrib to Fajr)

val List<String> TIME_NAMES = newArrayList("Imsak", "Fajr", "Sunrise", "Dhuhr", "Asr", "Sunset", "Maghrib", "Isha", "Midnight") // 0 - 8

val Functions$Function3<Number, Number, Number, Double> julian = [ year, month, day |
    var y = year.intValue var m = month.intValue var d = day.intValue
    if (m <= 2) { y -= 1 m += 12 }
    var Double a = Math.floor((y / 100.0).doubleValue)
    var Double b = 2 - a + Math.floor((a / 4.0).doubleValue)

    return Math.floor((365.25 * (y + 4716)).doubleValue) + Math.floor((30.6001 * (m + 1)).doubleValue) + d + b - 1524.5 ]

val Functions$Function1<Double, String> modifyFormats = [ time | 
    return String::format("%02.0f:%02.0f",
            Math.floor(time.doubleValue), (Math.round((time * 60).doubleValue) % 60).doubleValue ) ]

rule "SunCalc"
when
    System started or
    Time cron "0 0/20 * * * ?"  // Every 2 mins
then
    // PLEASE NOTE: Fajr is always in degrees; Dhuhr is always in minutes
    val Map<Integer, List<Double>> METHODS = newHashMap()
    //                                       Imsak            , Fajr, Dhur, Asr              , Maghrib           , Isha               , Midnight
    METHODS.put(METHOD_MWL,     newArrayList(10, VALUE_MINUTES, 18,   0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 17,   VALUE_DEGREES, MIDNIGHT_STANDARD))
    METHODS.put(METHOD_ISNA,    newArrayList(10, VALUE_MINUTES, 15,   0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 15,   VALUE_DEGREES, MIDNIGHT_STANDARD))
    METHODS.put(METHOD_EGYPT,   newArrayList(10, VALUE_MINUTES, 19.5, 0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 17.5, VALUE_DEGREES, MIDNIGHT_STANDARD))
    METHODS.put(METHOD_MAKKAH,  newArrayList(10, VALUE_MINUTES, 18.5, 0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 90,   VALUE_MINUTES, MIDNIGHT_STANDARD))
    METHODS.put(METHOD_KARACHI, newArrayList(10, VALUE_MINUTES, 18,   0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 18,   VALUE_DEGREES, MIDNIGHT_STANDARD))
    METHODS.put(METHOD_TEHRAN,  newArrayList(10, VALUE_MINUTES, 17.7, 0,    JURISTIC_STANDARD, 4.5, VALUE_DEGREES, 14,   VALUE_DEGREES, MIDNIGHT_JAFARI))
    METHODS.put(METHOD_JAFARI,  newArrayList(10, VALUE_MINUTES, 16,   0,    JURISTIC_STANDARD, 4,   VALUE_DEGREES, 14,   VALUE_DEGREES, MIDNIGHT_JAFARI))
    METHODS.put(METHOD_ADJUST,  newArrayList(10, VALUE_MINUTES, 13.6, 0,    JURISTIC_STANDARD, 0,   VALUE_MINUTES, 13.4, VALUE_DEGREES, MIDNIGHT_STANDARD))

    logInfo(logName, "Using method: '" + METHOD_NAMES.get(METHOD) + "' with parameters " + METHODS.get(METHOD).toString())

    // Some Constants
    val Double J2000 = 2451545.0  // julian date for year 2000
    val Double DEG2RAD = Math.PI / 180.0
    val Double RAD2DEG = 180.0 / Math.PI

    val Double G0 = 357.529
    val Double G1 = 0.98560028
    val Double Q0 = 280.459
    val Double Q1 = 0.98564736
    val Double E0 = 23.439
    val Double E1 = 0.00000036
    val Double L0 = 1.915
    val Double L1 = 0.020

    val Double phi = LATITUDE.doubleValue * DEG2RAD

    // midnightDate
    var Calendar date = Calendar.getInstance()
    date.set(Calendar.YEAR, now.getYear)
    date.set(Calendar.DAY_OF_YEAR, now.getDayOfYear)
    date.set(Calendar.HOUR_OF_DAY, 0)
    date.set(Calendar.MINUTE, 0)
    date.set(Calendar.SECOND, 0)
    logDebug(logName, "midnightDate: " + date.getTime().toString())

    // timeZoneOffset
    val Double timeZoneOffset = TimeZone.getDefault().getOffset(date.getTimeInMillis())
                                / 1000.0 / 60 / 60 - LONGITUDE.doubleValue / 15.0

    // julianDate
    val Double jDate = julian.apply(now.getYear, now.getMonthOfYear, now.getDayOfMonth)
                                - LONGITUDE.doubleValue / 360.0 // <- (15.0 * 24.0)

    // riseSetAngle
    val Double sunRiseSetAngle = 0.833 + 0.0347 * Math.sqrt(ALTITUDE.doubleValue) // an approximation

    var List<Double> angles = newArrayList(METHODS.get(METHOD).get(0), METHODS.get(METHOD).get(2),
            sunRiseSetAngle, METHODS.get(METHOD).get(3), 0.0, sunRiseSetAngle,
            METHODS.get(METHOD).get(5), METHODS.get(METHOD).get(7))

    /**
     * computeTimes
     */

    var List<Double> times = newArrayList(5, 5, 6, 12, 13, 18, 18, 18, 0)

    // convert hours to day portions
    times.subList(0,8).replaceAll([ i | i.doubleValue / 24.0 ])

    // convert deegres to radials
    angles.replaceAll([ i | i.doubleValue * DEG2RAD ])

    // computePrayerTimes (all but midnight)
    for ( var i = 0; i < times.subList(0,8).size(); i++ ) {
        logDebug(logName, String::format("computePrayerTimes: %s (%02d)", TIME_NAMES.get(i), i))
        logDebug(logName, String::format("< jDate: %04.12f", jDate.doubleValue))
        logDebug(logName, String::format("<  time: %04.12f", times.get(i).doubleValue))
        var Double D = (jDate + times.get(i) - J2000) // jDate is the given Julian date
        var Double G = (G0 + G1 * D).doubleValue % 360.0
        var Double Q = (Q0 + Q1 * D).doubleValue % 360.0
        var Double L = ( (Q + L0 * Math.sin((G * DEG2RAD).doubleValue)
                            + L1 * Math.sin((2 * G * DEG2RAD).doubleValue) + 360.0).doubleValue % 360.0
                       ) * DEG2RAD
        var Double E = ( E0 - E1 * D ) * DEG2RAD
        var Double decl = Math.asin(Math.sin(E.doubleValue) * Math.sin(L.doubleValue)) // * RAD2DEG
        logDebug(logName, String::format(">  decl: %04.12f", decl.doubleValue))
        if (i == TIMES_ASR) angles.set(i,
                    -1 * Math.atan((1 / ( METHODS.get(METHOD).get(4)
                                        + Math.tan((Math.abs((LATITUDE - decl * RAD2DEG).doubleValue) * DEG2RAD).doubleValue)
                                        )).doubleValue))
        var Double RA = ( Math.atan2(Math.cos(E.doubleValue) * Math.sin(L.doubleValue),
                                     Math.cos(L.doubleValue)) / 15 * RAD2DEG + 24.0 ).doubleValue % 24.0
        var Double eqt = Q / 15.0 - RA
        logDebug(logName, String::format(">   eqt: %04.12f", (eqt * RAD2DEG).doubleValue))
        var Double N = (12 - eqt).doubleValue % 24 // noon
        logDebug(logName, String::format(">  noon: %04.12f", N.doubleValue))
        var Double T = Math.acos( ( -1 * Math.sin(angles.get(i).doubleValue)
                                    - Math.sin(decl.doubleValue) * Math.sin(phi.doubleValue) )
                                    / ( Math.cos(decl.doubleValue) * Math.cos(phi.doubleValue) )
                                ) / 15 * RAD2DEG
        logDebug(logName, String::format("> tdiff: %04.12f", T.doubleValue))
        switch i {
            case i < TIMES_DHUHR: times.set(i, N - T)
            case i > TIMES_DHUHR: times.set(i, N + T)
            default             : times.set(i, N) }
        logDebug(logName, String::format(">  time: %04.12f", times.get(i).doubleValue))
    }

    // adjustTimeZone (all but midnight)
    times.subList(0,8).replaceAll([ i | i.doubleValue + timeZoneOffset.doubleValue ])

    // adjustHighLats (all but midnight)
    if (HIGHLAT != HIGHLAT_NONE) {
        val Double nightTime = ( times.get(TIMES_SUNRISE) - times.get(TIMES_SUNSET) + 24.0 ).doubleValue % 24.0 // sunset to sunrise
        logDebug(logName, String::format("nightTime: %04.12f", nightTime.doubleValue))

        var Double portion = 1.0 / 2.0 * nightTime // HIGHLAT_NIGHTMIDDLE
        if (HIGHLAT == HIGHLAT_ONESEVENTH) portion = 1.0 / 7.0 * nightTime

        for ( var i = 0; i < times.subList(0,2).size(); i++ ) { // Imsak and Fajr
            logDebug(logName, String::format("adjustHighLats: %s (%02d)", TIME_NAMES.get(i), i))
            logDebug(logName, String::format("<  time: %04.12f", times.get(i).doubleValue))
            logDebug(logName, String::format("< angle: %04.12f", ( angles.get(i).doubleValue * RAD2DEG ).doubleValue))
            if (HIGHLAT == HIGHLAT_ANGLEBASED) portion = 1.0 / 60.0 * ( angles.get(i).doubleValue * RAD2DEG ) * nightTime
            logDebug(logName, String::format(">  port: %04.12f", portion.doubleValue))
            if ( (( times.get(TIMES_SUNRISE) - times.get(i) + 24.0 ).doubleValue % 24.0) > portion )
                times.set(i, times.get(TIMES_SUNRISE) - portion)
            logDebug(logName, String::format(">  time: %04.12f", times.get(i).doubleValue))
        }
        for ( var i = 0; i < times.subList(6,8).size(); i++ ) { // Maghrib and Isha
            logDebug(logName, String::format("adjustHighLats: %s (%02d)", TIME_NAMES.get(i+6), i+6))
            logDebug(logName, String::format("<  time: %04.12f", times.get(i+6).doubleValue))
            logDebug(logName, String::format("< angle: %04.12f", ( angles.get(i+6).doubleValue * RAD2DEG ).doubleValue))
            if (HIGHLAT == HIGHLAT_ANGLEBASED) portion = 1.0 / 60.0 * ( angles.get(i+6).doubleValue * RAD2DEG ) * nightTime
            logDebug(logName, String::format(">  port: %04.12f", portion.doubleValue))
            if ( (( times.get(i+6) - times.get(TIMES_SUNSET) + 24.0 ).doubleValue % 24.0) > portion )
                times.set(i+6, times.get(TIMES_SUNSET) + portion)
            logDebug(logName, String::format(">  time: %04.12f", times.get(i+6).doubleValue))
        }
    }

    // adjustTimes (all but midnight)
    if ( METHODS.get(METHOD).get(1) == VALUE_MINUTES ) { // Imsak in minutes
        logDebug(logName, String::format("adjustTimes: %s", TIME_NAMES.get(TIMES_IMSAK)))
        logDebug(logName, String::format("<  time: %04.12f", times.get(TIMES_IMSAK).doubleValue))
        times.set( TIMES_IMSAK.intValue, times.get(TIMES_FAJR) - METHODS.get(METHOD).get(0).doubleValue / 60.0 )
        logDebug(logName, String::format(">  time: %04.12f", times.get(TIMES_IMSAK).doubleValue))
    }

    if ( METHODS.get(METHOD).get(6) == VALUE_MINUTES ) { // Maghrib in minutes
        logDebug(logName, String::format("adjustTimes: %s", TIME_NAMES.get(TIMES_MAGHRIB)))
        logDebug(logName, String::format("<  time: %04.12f", times.get(TIMES_MAGHRIB).doubleValue))
        times.set( TIMES_MAGHRIB.intValue, times.get(TIMES_SUNSET) - METHODS.get(METHOD).get(5).doubleValue / 60.0 )
        logDebug(logName, String::format(">  time: %04.12f", times.get(TIMES_MAGHRIB).doubleValue))
    }

    if ( METHODS.get(METHOD).get(8) == VALUE_MINUTES ) { // Isha in minutes
        logDebug(logName, String::format("adjustTimes: %s", TIME_NAMES.get(TIMES_ISHA)))
        logDebug(logName, String::format("<  time: %04.12f", times.get(TIMES_ISHA).doubleValue))
        times.set( TIMES_ISHA.intValue, times.get(TIMES_MAGHRIB) - METHODS.get(METHOD).get(7).doubleValue / 60.0 )
        logDebug(logName, String::format(">  time: %04.12f", times.get(TIMES_ISHA).doubleValue))
    }

    times.set( TIMES_DHUHR.intValue, times.get(TIMES_DHUHR) - METHODS.get(METHOD).get(3).doubleValue / 60.0 )

    // add midnight time
    if ( METHODS.get(METHOD).get(9) == MIDNIGHT_JAFARI )
        times.set( TIMES_MIDNIGHT.intValue,
            times.get(TIMES_SUNSET) + ( times.get(TIMES_FAJR) - times.get(TIMES_SUNSET) + 24.0 ).doubleValue % 24.0 / 2.0 )
    else
        times.set( TIMES_MIDNIGHT.intValue,
            times.get(TIMES_SUNSET) + ( times.get(TIMES_SUNRISE) - times.get(TIMES_SUNSET) + 24.0 ).doubleValue % 24.0 / 2.0 )

    // tuneTimes
    //times.replaceAll([ i | i.doubleValue + TUNE.get(i) / 60.0 ])

    // convert double time to HH:MM
    times.replaceAll([ i | i.doubleValue % 24.0 ])
    
    for ( var i = 0; i < times.size(); i++ ) {
        //logInfo( logName, String::format("%s -> %02.0f:%02.0f", TIME_NAMES.get(i).toString(),
        //    Math.floor(times.get(i).doubleValue), (Math.round((times.get(i) * 60).doubleValue) % 60).doubleValue ) )
        logInfo( logName, TIME_NAMES.get(i).toString() + " -> " + modifyFormats.apply(times.get(i)) )
    }

    PrayerTime_Fajr.postUpdate( modifyFormats.apply(times.get(TIMES_FAJR)) )
    PrayerTime_Zuhr.postUpdate( modifyFormats.apply(times.get(TIMES_DHUHR)) )
    PrayerTime_Asr.postUpdate( modifyFormats.apply(times.get(TIMES_ASR)) )
    PrayerTime_Maghrib.postUpdate( modifyFormats.apply(times.get(TIMES_MAGHRIB)) )
    PrayerTime_Isha.postUpdate( modifyFormats.apply(times.get(TIMES_ISHA)) )
end
3 Likes

Wow, now THAT is amazing! I will read that carefully and hopefully learn a lot from it. Thanks

The Javascript version is not buggy
From http://praytimes.org/

I used to use it successfully with node-red before migrating most of my functionalities back to OH

I just didn’t know of a way to call a js file and return a value. I knew how to do that with Python.

The bugs in the Python script are not the accuracy of the results but the settings of the different methods, angles etc…

That doesn’t sound right. I have OH running in a less capable VM than a NUC also in Docker and all of my rules are loaded and compile almost instantly.

Make these globals so they only need to calculated/saved once.

Beyond that it seems to be pretty straight forward code. Set up a bunch of variables then do a bunch of calculations.

It really doesn’t make sense to me that it takes that long to parse the file. Maybe it has something to do with all the imports or something.

I bet it would make a nice binding though if someone wanted a relatively simple one to cut their teeth on.

Thank you for your code, very impressive.

However, as I told you that pray times are not accurate (especially maghreb, around 3 minutes, and maybe you know maghreb time is important for ramadan).

Also maybe it is better to check if the script brought the current date (day), if no then call the script again (this is to resolve the case if the system shut down at the specified time of getting pray times)

Maghrib is accurate for me. Again you can adjust the times in the settings.

Good idea about the day, I’ll have a look.