Show Remaining Time of a Timer

So I have been trying this for hours and I cant wrap my head around it.
I am setting up an automated irrigation, which is working very well, especially through the help of @rlkoshak’s design patterns. It takes my hours to understand them (no computer savvy background whatsoever) but I got there. A Post in the solutions area will follow, once everything works to my satisfaction.

I have a Number Item (Setpoint in Sitemap) where I can set the IrrigationDuration in Minutes.
When Automated Irrigation is started, I want to show the RemainingMinutes of the Duration.

I found:

rule "Countdown"
when
    Time cron "0 0 * * * ?"
then
    var Number remain = MinutesRemainingOnTimer.state as DecimalType
    if((myTimer == null || myTimer.hasTerminated) && remain != 0) {
        MinutesRemainingOnTimer.postUpdate(0)
    }
    MinutesRemainingOnTimer.postUpdate(remain - 1)
end
*/

But I don’t really like it, because that rule would fire every Minute, no matter if the Irrigation is running or not. So I want something like this in plain text:

rule "Remaining Time"
When 
AutoIrrigation changed to ON
Then:
IrrigationDuration=MinutesRemaining
Every Minute:
MinutesRemainin.postUpdate(-1)

How on earth do I implement this?
Another approach I thought of is to calculate the End time and then subtract currentTime from the EndTime

So something like:

rule "Remaining Time"
When AutoIrrigation changed to ON
then 
Endtime=TimeNow+IrrigationDuration
For Every minute calculate 
RemainingTime=Endtime-TimeNow
Stop When RemainingTime=0

Best regards from an eager student…

It would look something like

    Item AutoIrrigation changed to ON
then
    MinutesRemainingOnTimer.postUpdate(TotalRuntime.state)
    timer = createTimer(now.plusMinutes(1), [ |
        var remain = MinutesRemainingOnTimer.state - 1
        if( remain < 0 ) remain = 0
        MinutesRemainingOnTimer.postUpdate(remain)
        if( remain > 0) timer.reschedule(now.plusMinutes(1))
    ]
End
1 Like

Would the if need {} ?
and Would I have to use

var remain = MinutesRemainingOnTimer.state as Number -1

Or actually

var remain = MinutesRemainingOnDripTimer.state as Number  
countdownDripTimer = createTimer(now.plusMinutes(1), [ |
remain = remain - 1
if( remain < 0 ) {remain = 0}
if( remain > 0){ countdownDripTimer.reschedule(now.plusMinutes(1))
    }
    ])
end

something like that?

If there is only one statement that needs to run when the condition is true the {} is optional.

The formatting got a little mangled but as far as I can tell the cover you posted looks ok.

I fixed the formatting.
I implemented as above, but for some reason, it throws an error, when trying the timer expired (so when

remain = remain -1

comes up.
I`ll look into it again tonight and then post the complete look. Also I’ll do some reading into Xtend to better understand the syntax. Is there any good tutorial available?

What is the error that gets generated now?

Start with the Rules docs: https://www.openhab.org/docs/configuration/rules-dsl.html

Then move on to http://www.eclipse.org/xtend/documentation/203_xtend_expressions.html (Note only the expressions page fully applies to OH Rules)

Then review the Design Patterns on this forum.

Hey guys.
I want to share my working solution:

var Timer irrigationDripTimer = null
var Timer countdownDripTimer 

rule "Reset Irrigation"
when
    System started 

then
    createTimer(now.plusSeconds(80)) [|
    gDrip.members.forEach[ i | i.sendCommand(OFF)]
    Garden_Irrigation_DripSwitch.sendCommand(OFF) ]
end

Above code turns off Irrigation at restart of the system.

rule "Start Automatic Drip Irrigation"
when
    Item Garden_Irrigation_DripSwitch received command ON
then
 gDrip.members.forEach[ i | i.sendCommand(ON)]
   val currValveDripMins = Garden_Irrigation_DripDuration.state as Number
MinutesRemainingOnDripTimer.postUpdate(currValveDripMins)
irrigationDripTimer = createTimer(now.plusMinutes(currValveDripMins.intValue), [|
gDrip.members.forEach[ i | i.sendCommand(OFF)]
Garden_Irrigation_DripSwitch.postUpdate(OFF)])
end

Above code for the actual timer, next is defining a cancel action:

rule "Cancel Drip Irrigation"
when
    Item Garden_Irrigation_DripSwitch received command OFF    

then
    // Cancel the timer if there is one, the ? will cause the line to be skipped if timer is null
    irrigationDripTimer?.cancel
    irrigationDripTimer = null
    countdownDripTimer?.cancel
    countdownDripTimer = null

    gDrip.members.forEach[ i | i.sendCommand(OFF)]
   
end

Now comes the rule for the Timer which shows the remaining Timer.

rule "Remaining"
when Item Garden_Irrigation_DripSwitch changed to ON 
then 
val currValveDripMinutes = Garden_Irrigation_DripDuration.state as Number
var remain = currValveDripMinutes
countdownDripTimer = createTimer(now.plusMinutes(1), [|
remain = remain - 1
if( remain < 0 ) remain = 0
MinutesRemainingOnDripTimer.postUpdate(remain)
if( remain > 0){ countdownDripTimer.reschedule(now.plusMinutes(1))
    }    ])
end

By trial and error I learned some things about vals and vars and the declaratioion thereof.
When using timers I have to declare
var timer myTimer = null in the beginning?
Doing that for the timer used for the remain messed up the timer (errors came when I used Timer.reschedule(now.PlusMinutes(1)
Also I have some general questions Are var and val defined per Rule or globally? So can I just reuse them in different rules (that would make my rules a littler shorter and easier). Does it matter in which order the rules are in the rules file?

1 Like
var xx = 1      // a global, can be seen in all rules (in this rules file)
                      // but not by rules in other files

rule xxx
...
then
    var zz = 1   // only accessible in the context of this rule
    ...
    createTimer( ...  [  |
           // code executed here is in global context
           // globals cannot 'see' each other
           // code cannot see 'xx'
           // nor can code see 'zz' which was in rule context
       ])
end

I think you are beginning to run into the difficulties described in Rik’s post below, in particular
“lambdas do not have access to the global vals and vars in their context so all of those must be passed as arguments.”