(I think there are still a few typos in above)
I’ve been playing with this recursive timer, to combine it with some countdown ideas stolen from here
My objective, to create a countdown timer for e.g. motion activated lights. Must be able to reschedule for that purpose. Cannot use “expire binding” methods as timer durations are variable depending on context – home/away etc.
Ordinary so far, but I also wanted to be able to inspect remaining time from rules for smarter behaviour - e.g. as my light zones may be triggered from different causes for different durations, I want a rule to able to see if an existing timer is longer or shorter than what is now proposed.
Using recursion, we create a one-minute timer that decrements a Number Item. Lambdas, closures, recursion - all gives me headaches! Eventually I figured out my major obstacle was that the actual timer in Rik’s version is anonymous, and so could not be cancelled or rescheduled from a rule. The uncancelled timer contains a snapshot of the ‘old’ counter, so trying to simply update counter value fails.
The trick is put both the recursive function in a hashmap (so it can call itself) and the timer it spawns into another (where rules can cancel it).
Here is my example for OH1;
test items
Number my_countdown "minutes counter [%s min]"
Switch test_countdown "Simulate trigger or cancel"
you can put those on a sitemap to play
rules file
// an array of timers for each lighting zone
val Map<String, Timer> lightTimers = newHashMap
// an array of countdown functions, a workaround for rescheduling
val Map<String, Object> lightTickers = newHashMap
// lambda function for recursive countdown timers i.e.
// a timer that can re-call itself upon expiry for a periodic effect
// param1 = Number Item counter
// param2 = the timer map array (so it can be cancelled)
// param3 = the function array, needed for recursion
// param4 = recursive function itself
// We host it in a Map because the Timer will not have access to change the global var itself
// but can make a call on the Map if we pass it into the lambda to update the contents.
val org.eclipse.xtext.xbase.lib.Functions$Function4 doCountdown = [
NumberItem myDelay,
Map<String, Timer> timersMap,
Map<String, Object> functionMap,
org.eclipse.xtext.xbase.lib.Functions$Function4 makeTicker |
val int ddelay = (myDelay.state as DecimalType).intValue
timersMap.put(myDelay.name, null) // destruct old
if(ddelay > 0) {
timersMap.put(myDelay.name, createTimer(now.plusMinutes(1)) [ |
myDelay.postUpdate(ddelay-1)
functionMap.put(myDelay.name, makeTicker.apply(myDelay, timersMap, functionMap, makeTicker))
]
)
} else {
functionMap.put(myDelay.name, null)
logInfo("testing", "countdown complete")
// if you want to update Items here, you will need to pass those into the lambda as well
// I chose instead to have rules triggered from counter Item changing to 0
}
]
rule "test trigger"
when
Item test_countdown changed
then
if (test_countdown.state == ON) {
my_countdown.sendCommand(5)
} else {
my_countdown.sendCommand(0)
}
end
// send an integer minute count for start or reschedule, or 0 for abort
rule "Timeout setting"
when
Item my_countdown received command
then
val oldTimer = lightTimers.get(my_countdown.name)
if (oldTimer != null) {
logInfo("testing", "cancel existing ticker")
oldTimer.cancel
lightTimers.put(my_countdown.name, null)
lightTickers.put(my_countdown.name, null)
}
if (my_countdown.state > 0) {
logInfo("testing", "ticker startup")
lightTickers.put(my_countdown.name, doCountdown.apply(my_countdown, lightTimers, lightTickers, doCountdown))
} else {
logInfo("testing", "countdown zeroed")
}
end
I do not think this is 100% bombproof - at the least there is a chance that testing for a null timer in the rule could happen in the instant where a previous function is between ‘ticks’ i.e. nulled the last timer and just about to set a new one. Not likely though; the result could be two countdown sequences fighting over one counter, but that should be a smaller problem than it sounds. Eventually it reaches zero!