Edit: Update for OH 4
Please see Design Pattern: What is a Design Pattern and How Do I Use Them to understand the scope and purpose of this and other Design Patterns.
Problem Statement
Often one may require a loop in a Rule, to wait for some event to occur or some state to change before doing something. For example, if a door is opened for a long time to send an alert every hour until the door is closed.
Concept
Use a Timer that reschedules itself instead of a while loop or sleeps.
openHAB-rules-tools
openHAB Rules Tools Announcements and openHAB Rules Tools [4.1.0.0;4.9.9.9] (if using Blockly) implements a looping timer class which can be used in JS Scripting or Blockly.
Blockly
In the above example the looping timer will run every second until loopCount is 5 and then stop.
If you don’t want to use the openHAB Rules Tools block library you can implement a looping timer as follows:
JS Scripting
Using openHAB Rules Tools.
var {LoopingTimer} = require('openhab_rules_tools');
var timer = cache.private.get('loopingTimer', () => LoopingTimer());
var loopCount = 0;
timer.loop( () => {
loopCount++;
if(loopCount >= 5) return null;
else return 1000;
}, 1000);
The above code creates a LoopingTimer that runs the first time in one second (1000 milliseconds). It increments the loop count and if the loop count is >= 5 the timer function return null
which ends the loop. Otherwise it returns how long before the loop runs again.
One cool feature here is that the time for the next run of the loop can vary from one run to the next based on what the looping function returns.
Implementing a looping timer without the library would look something like this:
var loopCount = 0;
var timer = cache.private.get('loopingTimer', () actions.ScriptExecution.createTimer(time.toZDT(1000), () => {
loopCount++;
if(loopCount < 5) {
cache.private.get('loopingTimer').reschedule(time.toZDT(1000));
}
});
Rules DSL
var loopCount = 0
var timer = privateCache.get('loopingTimer', [ | createTimer(now.plusSeconds(1), [ |
loopCount += 1
if(loopCount) timer = null
else {
// do stuff
(privateCache.get('loopingTimer') as Timer).reschedule(now.plusSeconds(1))
}
])])
Complex Example
We have a Group of water leak sensors called WaterLeakAlarms
. The Group is configured as a Group:Switch:OR(ON,OFF)
so the WaterLeakAlarms
will be ON
if there is one or more alarms that are ON
and OFF
if all are OFF
. We want to send an alert every minute for as long as a water leak is detected.
The rule is triggered by changes to WaterLeakAlarms
.
var {alerting} = require('rlk_personal');
var {LoopingTimer} = require('openhab_rules_tools');
var logger = log('Water Leak Alert');
logger.warn('A water leak was detected!');
var lt = cache.private.get(ruleUID+'_lt', () => LoopingTimer());
var loopGenerator = function() {
return function(){
if(items.getItem('WaterLeakAlarms').state == 'OFF') {
logger.info('No more leaks!');
return null;
}
else {
logger.info('Still seeing a water leak!');
var names = alerting.getNames('WaterLeakAlarms', i => i.state == 'ON');
alerting.sendAlert("There's a leak at " + names + '!');
return 'PT1m';
}
}
}
lt.loop(loopGenerator(), 'PT0s');
The alerting is done using a personal library (see separation of behaviors). The looping timer is create and scheduled to run immediately. When WaterLeakAlarms
returns to OFF
null
is returned by the loop function which ends the loop. Otherwise an alert is sent and PT1m
is returned which schedules the loop to run again in one minute.
Advantages and Disadvantages
Looping Timers have one major advantage: They don’t tie up the Rule while the loop is running. Only one instance of a rule can run at a time so if this were implemented using a sleep the rule would be blocked from executing and the triggers would queue up and be worked off in order.
Related Design Patterns
Design Pattern | How It’s Used |
---|---|
Design Pattern: Simple State Machine (e.g. Time Of Day) | Used in the Complex Example to determine when it is night time |
Design Pattern: Do While | The complex example is an implementation of the Do While DP using Looping Timers |