Design Pattern : Expire Binding based Countdown timer

designpattern
Tags: #<Tag:0x00007f01533d7d80>

(Rossko57) #1

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


[SOLVED] How to assign a value to global variable/phantom item in a rule?
Basic Motion Timer With Additional Force ON And Force OFF
Only an openhab restart shows any change!
FYI - .state is not always valid in rules
Help me - Turn off light after 5 or 10 minutes
(Rossko57) #2

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.