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

11 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.

8 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:

I have been looking for something like this for a long time, thank you very much!
Question, is there an option to turn the timer to + (to show how much time has passed and not left)

Hi,
My programming skills are not so good, i am trying the library countdownTimer.
i want to build a coundtimer for my irrigation valve. can you please help me with a small example?
i have the items that keep the input for the duration in minutes, item for the countdown and the item for the on/off of the valve.
I will appreciate any help.
Thanks
Nikos

You are referring to my openhab-rules-tools implementation of countdownTimer?

Assuming you’ve met all the prerequisites (using GraalVM JS, installed the library, etc) you need to require it and instantiate an instance of it. It starts counting down once it’s created. As the docs mention for openhab-rules-tools, the test cases shows examples for how to use the class.

var { countdownTimer } = require('openhab_rules_tools');

var runMe = () => {
  // timer code goes here
}

var timer = new countdownTimer.CountdownTimer(<Duration>, runMe, items.<Countdown Item Name>.name);

<Duration>1 is to be replaced with the full time for the timer and ` as the name. Since you have an Item containing minutes, you’ll need to format that appropriately for the duration.

var dur = 'PT"+items.<Duration Item>.state+'M';

The above is one among many ways to do that.

Thanks for you help,

i know someone can make this better but for me its working.

var { time, items } = require('openhab');
var { countdownTimer, testUtils } = require('openhab_rules_tools');

var logger = log('rules_tools.Countdown Timer Tests');

cache.private.put(ruleUID, false);
var countItem = items.getItem('Countdown_Timer');
var duration = items.getItem('duration');
var valvestate = items.getItem('test1');

var runMe = function() {
  
  // Send "OFF" command to valvestate item
  valvestate.sendCommand('OFF');
};

var timer = null;

// Function to start the timer
var startTimer = function() {
  if (valvestate.state === 'ON') {
    if (timer !== null) {
      logger.info('Timer is already running.');
    } else {
      logger.info('Starting the timer.');
      timer = new countdownTimer.CountdownTimer(duration, runMe, countItem.name);
    }
  } else {
    logger.info('Valvestate is not ON. Timer cannot be started.');
  }
};

// Function to cancel the timer
var cancelTimer = function() {
  if (timer !== null) {
    logger.info('Cancelling the timer.');
    timer.cancel();
    timer = null;
  } else {
    logger.info('Timer is not running.');
  }
};

// Function to periodically check the state of valvestate
var checkValvestate = function() {
  if (valvestate.state === 'OFF') {
    cancelTimer();
  } else if (valvestate.state === 'ON') {
    startTimer();
  }
};

// Run the checkValvestate function every 5 seconds
setInterval(checkValvestate, 5000);

// Call the startTimer function initially
startTimer();

What really matters is that you understand what the code does.

Some things I notice:

  • var { time, items } = require('openhab'); : If you are running with the default settings for the add-on, these are imported for you already by default. This line is not needed.
  • var { countdownTimer, testUtils } = require('openhab_rules_tools'); : You don’t need testUtils
  • cache.private.put(ruleUID, false); : You never use the cache anywhere. This line is doing nothing. If you were using the cache somewhere, this line would make the cache pointless. You want what ever is in the cache to persist between runs of the rule. This line essentially deletes what ever is in the cache every time the rule runs.
  • var countItem = items.getItem('Countdown_Timer'); : Save yourself some work later and get the name of the Item here. You don’t care about the rest.
  • var duration = items.getItem('duration'); : Didn’t you say above that this is a number representing minutes? As used later on it’ll be treated as millseconds, not minutes.
  • var timer = null; : If this rule is triggered while another timer exists, you’ll end up with two copies of this timer running.
  • if (timer !== null) { : as written, this will always be true because of the line above
  • var cancelTimer = function() { : You’ve thrown away the timer instead of saving it in the cache so you’ve nothing to cancel. timer will always be null when this function runs.
  • setInterval(checkValvestate, 5000); : It would be far better to use a cron trigger on the rule. Even better would be to trigger the rule when test1 changes to ON or OFF
  • startTimer(); : Because of the polling to call checkValvestate, this line is redundant.

I would rewrite it as follows:

  • make sure the rule triggers when test1 changes
  • duration is of type Number:Time so it has units.
var { countdownTimer } = require('openhab_rules_tools');
var logger = log('rules_tools.Countdown Timer Tests');

var countItemName = items.Countdown_Timer.name;
var duration = items.duration.quantityState;
var valveItem = items.test1;

var runMe = () => {
  logger.info('Turning off the valve');
  valveItem.sendCommand('OFF');
  cache.private.put('timer', null); // reset the cache so we create a new timer next time
}

// When the valve turned ON
if(event.newState.toString() == "ON") {
  if(cache.private.get('timer') !== null) {
    logger.warn('Valve changed to ON but timer already exists! This should not be possible!, Cancelling the timer before recreating it.')
    cache.private.get('timer').cancel();
  }
  cache.private.put('timer', new countdownTimer.CountdownTimer(duration, runMe, countItemName);
}
// treat OFF, UNDEF, and NULL the same
else if(cache.private.get('timer') !== null) {
    logger.info('Valve turned off, cancelling the timer');
    cache.private.get('timer').cancel();
    cache.private.put('timer', null); // clear the cache
  }
}

I have been using Openhab for several years, and have only just discovered ‘expire’!
Fabulous function.
Till now I have been bodging rules together with timers. Expire makes it so much easier.
I was always googling for how to use timers. This time I googled ‘countdown’ and hit on this thread and discovered expire.
Thank-you openhab developers
Ray

Hi,
I have try your code but i get this error;
Failed to execute script:

org.graalvm.polyglot.PolyglotException: TypeError: Cannot read property “name” from undefined

Thanks
Nikos

Hi, i have modify your code and like this its working but the cancel i can not get it .
I dont know why but if i use the vars like this : var countItemName = items.Countdown_Timer.name; i get errors if i use them like this: var countItemName = items.getItem(‘Countdown_Timer’); its working.
Also this part gives me error : if(event.newState.toString() == “ON”)
I am posting the code that its working to start the timer but with no luck to cancel if the valvestate changed.

var { countdownTimer } = require('openhab_rules_tools');
var logger = log('rules_tools.Countdown Timer Tests');

var countItemName = items.getItem('Countdown_Timer');
var duration = items.getItem('duration');
var valveItem = items.getItem('test1');

var runMe = () => {
  logger.info('Turning off the valve');
  valveItem.sendCommand('OFF');
  cache.private.put('timer', null); // reset the cache so we create a new timer next time
}

// When the valve turned ON
  if (valveItem.state === 'ON') {
    if (cache.private.get('timer') !== null) {
      logger.warn('Valve changed to ON but timer already exists! This should not be possible!, Cancelling the timer before recreating it.');
      cache.private.get('timer').cancel();
    }
    cache.private.put('timer', new countdownTimer.CountdownTimer(duration, runMe, countItemName.name));
    logger.warn('Valve changed to ON Timer start');
  }

// treat OFF, UNDEF, and NULL the same
  else if(cache.private.get('timer') !== null) {
    logger.info('Valve turned off, cancelling the timer');
    cache.private.get('timer').cancel();
    cache.private.put('timer', null); // clear the cache
  }

EDIT after a restart now its working also the cancel.
last think to figure out how to make the input from seconds to minutes

Assuming the item is a Number:Time just set the State Description to use min for the units.