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.
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.
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)
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?
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
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.
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 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