Design Pattern: Recursive Timers

(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!

1 Like