Design Pattern : Expire Binding based Countdown timer

Problem Statement
Sometimes you need to run a timer, but want to able to inspect the remaining time. This is not directly possible with either createTimer() or the Expire binding.

Example - I have lighting-on timers that run for different duration depending on the trigger source or time of day. It is useful to have the timer behave “intelligently” - do not apply a new 5-minute request where there is still seven minutes of a previous trigger to run. But do apply it if there is only two minutes left.

Concept
Create a number Item to use as a minutes countdown. Set autoupdate off for this, so its state only gets set by rules, not by Commands.
Link it to the Expire binding (you may need to install that) so that a Command of minus-1 is sent after a minute. -1 is arbritrary really, but easy to understand!
The countdown is managed by a single rule.

  • To use the countdown, simply send it a command with the required number of minutes. It acts smart, and only ‘reschedules’ the countdown when the new target runs longer than the current count.
  • You can force the countdown to end early by commanding zero.
  • You can force the countdown to some new value by commanding it as a negative value, e.g. -10 This overrides the ‘smart’ behaviour, e.g sets it to 10 even if there is still 20 from a previous trigger.

You write into the rule what actions you want it to take upon starting or rescheduling, upon expiry, and upon cancellation (which might not be the same as normal expiry)

Demonstration Items

// example counter
Number myCounter "Minutes counter [%s]" <clock> {expire="1m,command=-1", autoupdate="false"}
Switch testLamp "example light [%s]" <light>
// test buttons for UI
Switch test6 "6 min run" <button> {expire="2s,state=OFF"}  // expire makes it like a pushbutton
Switch test3 "3 min run" <button> {expire="2s,state=OFF"}
Switch test2 "force 2 mins" <button> {expire="2s,state=OFF"}
Switch testabort "counter cancel" <button> {expire="2s,state=OFF"}

Demonstration rules

// Rule to manage countdown
rule "Countdown tick"
when
	Item myCounter received command
then
	var cmmd = (receivedCommand as Number).intValue // integers only
	var count = 0
	if (myCounter.state != NULL) {  // avoid 1st time run error
		count = (myCounter.state as Number).intValue
	}
	if (cmmd == -1 && count > 0) {  // decrement counter, do not go below zero
		if (count == 1) {
			// do actions for counter expiry, as we about to zero it now
			testLamp.sendCommand(OFF)
		}
		myCounter.postUpdate(count - 1)
	} else if (cmmd >= count || cmmd < -1) {  // new or refreshed target
		if (cmmd < -1) {  // force override
			cmmd = 0 - cmmd  // make it positive
		}
		myCounter.postUpdate(cmmd)  // nb we still update even if equal value - resets expire binding
		// do startup/continue actions
		if (testLamp.state != ON) {
			testLamp.sendCommand(ON)
		}
	} else if (cmmd == 0) {  // cancel countdown
		myCounter.postUpdate(0)
		// do optional cancel actions
		testLamp.sendCommand(OFF)
	}
end

// rules just for test simulation buttons
rule "test 6 mins"
when
	Item test6 received command
then
	myCounter.sendCommand(6)
end
rule "test 3 mins"
when
	Item test3 received command
then
	myCounter.sendCommand(3)
end
rule "test force"
when
	Item test2 received command
then
	myCounter.sendCommand(-2)
end
rule "test stoppit"
when
	Item testabort received command
then
	myCounter.sendCommand(0)
end

Demo sitemap

Frame label="Countdown testing" {
		Text item=myCounter
		Text item=testLamp
		Switch item=test3
		Switch item=test6
		Switch item=test2
		Switch item=testabort
	}

Inspired by @rikoshak post on one-shot timers

9 Likes

Complex example

A more practical use of the countdown technique where Groups are used in an ‘associated Items’ way, exploiting the Group features of rules in OH2.3 onwards.
One rule operates many counters, and derives the name of the ‘target’ light from the counter name being processed.
Control is as before, by sending a number to the counter in a Command eg. from a rule responding to a motion sensor.

// example counters
Group gCounters
Group gLights
Number LampAA_counter "Minutes counter A[%s]" <clock> (gCounters) {expire="1m,command=-1", autoupdate="false"}
Switch LampAA "example light A [%s]" <light> (gLights)
Number LampBB_counter "Minutes counter B[%s]" <clock> (gCounters) {expire="1m,command=-1", autoupdate="false"}
Switch LampBB "example light B [%s]" <light> (gLights)

.

// Rule to manage countdowns
rule "Countdown tick"
when
	Member of gCounters received command
then
	var cmmd = (receivedCommand as Number).intValue // integers only
	var count = 0
	if (triggeringItem.state != NULL) {  // avoid 1st time run error
		count = (triggeringItem.state as Number).intValue
	}
	val lightname = triggeringItem.name.split("_").get(0)  // derive light
	val myLight = gLights.members.filter[ i | i.name == lightname ].head // get light item
    
	if (cmmd == -1 && count > 0) {  // decrement counter, do not go below zero
		if (count == 1) {
			// do actions for counter expiry, as we about to zero it now
			myLight.sendCommand(OFF)
		}
		triggeringItem.postUpdate(count - 1)
	} else if (cmmd == 0) {  // cancel countdown
		triggeringItem.postUpdate(0)
		// do optional cancel actions
		myLight.sendCommand(OFF)
	} else if (cmmd >= count || cmmd < -1) {  // new or refreshed target
		if (cmmd < -1) {  // force override
			cmmd = 0 - cmmd  // make it positive
		}
		triggeringItem.postUpdate(cmmd)  // nb we still update even if equal value - resets expire binding
		// do startup/continue actions
		if (myLight.state != ON) {
			myLight.sendCommand(ON)
		}
	}
end

See Rik’s ‘Associated Items’ design pattern for group/item naming

EDIT - changed order of main ‘if’ evaluation, so that accidentally commanding 0/cancel when already zero will not trigger light.

7 Likes

I used the example code for a water-irrigation system. Its only on for 3 Minutes and needs to turn the Motor on and off inbetween so I changed the expiring item to seconds.
Now I encountered the problem that it only counts down every second second.
The 3 minutes in total are precisely 6 now.

Did anybody else encounter this or can reproduce this behaviour? I dont have access to the system right now but can deliver logs later.

Thanks in Advance

You’re going to have to tell us more about that, e.g. rule(s).
I wouldn’t expect this expire-counter method to be super accurate when used for just a few seconds, but nor should it get whole minutes out.
Bear in mind expire’s timer restarts every time you update the “counter” Item.

For trying it out I exactly copied your Example (first one) including the items. The only difference is that the item expires in seconds not in minutes.

Number myCounter “Seconds counter [%s]” {expire=“1s,command=-1”, autoupdate=“false”}
and I send it higher values
myCounter.sendCommand(180)

Okay, that should be making lots of records in your events.log

Again, I’ would not expect precision from this expire method. Each “tick” is extended by at least the runtime of the rule, so the smaller you make your time slice the greater the error will be.
I would not expect your rule to take another second to execute - but that rather depends on your host system and how much other timer based work it is doing (there are limited concurrent threads for time triggered activities).
On my system it “slips” by 3 or 4mS per minute tick.

How interesting - had a play with expire periods 1s, 2s, 60s
expire binding consistently adds a second to duration expressed in seconds.
(it still hits the correct milliseconds-past plus the expected 3-4mS slippage)

It doesn’t do that for durations in minutes.

Seems like a bug that’s been there forever.

2 Likes

ahhhhh…wow

1 Like

For those using Scripted Automation (AKA JSR223) there is now a Python library that implements a countdown timer at Initial submission of a Countdown Timer implementation by rkoshak · Pull Request #237 · openhab-scripters/openhab-helper-libraries · GitHub. Once it’s merged you can just copy the countdown folder from the Community folder to your automation folder and start using it in your Rules. GitHub - rkoshak/openhab-rules-tools: Library functions, classes, and examples to reuse in the development of new Rules..

Update: there is now a JavaScript version of this DP posted at that link too.

I have logged an issue for the one-second error at


It may never get fixed, but at least the issue should remind not to get reproduced in any later “expire-3” mechanism.

@MaxH , what are you trying to do exactly? Sounds like you need to pulse something on and off during a three minute period.
Can you now arrange this, once aware of the error? Or remake it with separate 3-minute enable and an on-off “flasher” routine?

2 Likes

Sorry for the late reply. My homeautomation only gets pulsed attentioned because of work and other hobbies. Im trying to time my watering-system for my plants. Im now using twice the amount of time to get to the right value. Since the timing can be under a minute sometimes I definitely need seconds :confused: