Design Pattern: Looping Timers

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

image

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:

image

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
19 Likes

Here is a side by side example using straight JSR223 Jython, which does not have the threading issues with sleep that the Rules DSL has, so there’s no need for a timer example. Note: changes would be needed if the temperature items were QuanityType. There is an ESH PR to add a few missing types to the scope that will make this much easier to handle.

scriptExtension.importPreset("RuleSupport")
scriptExtension.importPreset("RuleSimple")
from org.slf4j import Logger, LoggerFactory
from time import sleep
from threading import Lock
log = LoggerFactory.getLogger("org.eclipse.smarthome.model.script.Rules")

global fanLock
fanLock = Lock()

#rule "Ceiling fan control"
#when 
#    Item MotionSensor changed to ON
class CeilingFanControl(SimpleRule):
    def __init__(self):
        self.triggers = [
            Trigger("CeilingFanControlTrigger", "core.ItemStateChangeTrigger", 
                Configuration({ "itemName": "Fan", "state": "ON"}))
        ]
#then            
    def execute(self, module, input):
        log.debug("JSR223: Ceiling Fan Control: starting rule")
        global fanLock
        #if(vTimeOfDay.state != "NIGHT" || !fanLock.tryLock) return;
        if items["vTimeOfDay"] != StringType("NIGHT") or not fanLock.acquire(False):
            return
        #try {
        try:
            #while(vTimeOfDay.state == "NIGHT"){
            while items["vTimeOfDay"] == StringType("NIGHT"):
                #var newState = "STAY"
                newState = "STAY"
                #if(CurrentTemp.state > TargetTemp.state) newState = "ON"
                if items["CurrentTemp"] > items["TargetTemp"]:# DecimalType comparisons do not need cnversion
                    newState = "ON"
                #else if(CurrentTemp.state < TargetTemp.state - 1) newState = "OFF" // -1 to provide a buffer for hysteresis
                elif float(str(items["CurrentTemp"])) < float(str(items["TargetTemp"])) - 1:# need to convert from DecimalType to float before doing arithmetic
                    newState = "OFF"
                #if(newState != "STAY" && Fan.state.toString != newState) Fan.sendCommand(newState)
                if newState != "STAY" and items["Fan"] != StringType(newState):
                    events.sendCommand("Fan",newState)
                log.debug("JSR223: Ceiling Fan Control: newState={}".format(newState))
                #Thread::sleep(60000)
                sleep(60)
            #}
        #}
        #catch(Exception e) {
        except Exception as e:
            #logError("Fan", "Error controlling the fan: " + e)
            log.debug("JSR223: Ceiling Fan Control: Error controlling the fan: {}".format(e))
        #}
        #finally {
        finally:
            #fanLock.unlock
            fanLock.release()
            log.debug("JSR223: Ceiling Fan Control: ending rule")
        #}
    #end

automationManager.addRule(CeilingFanControl())
2 Likes

Thanks for valuable design pattern, it is helpful.

However the second way fires an error,

var Timer timer = null // global variable usually

...

    timer = createTimer(now, [ |
        if(!condition){
            // do stuff
            timer.reschedule(1000)
        }
    ])

Also the reschedule(1000) has error, I try:

timer.reschedule(now.plusMinutes(1))

What did you set the condition to? That coffee assumes you replace the condition with whatever condition you have to stop the loop. It’s a place holder.

And it is all but impossible to help if you don’t tell me what the error is. “An error” doesn’t help me understand why it doesn’t work.

1 Like

Hi,
Many thanks for all the design patterns you provide. They are really helpful.
If I may add some comments, while following your example, my rule always fired only once. Below is the rule:

var Timer timer_Lumiere_Salon = null

rule "test"
when
        Item Dummy received update
then
        logInfo("test", "rule triggered")
        var dimmer=0
        var dimmermax=60
        if (Dummy.state==OFF)
        {
                logInfo("test", "OFF")
//              sendCommand(Halo_Salon_D,0)
                if(timer_Lumiere_Salon != null)
                {
                        logInfo("test", "timer_Lumiere_Salon != null")
                        timer_Lumiere_Salon = null
                }
                return;
        }
        else
        {
                logInfo("test", "ON")
                timer_Lumiere_Salon = createTimer(now,[|
                if(dimmer < dimmermax)
                {
                        dimmer = dimmer+10
//                      sendCommand(Halo_Salon_D, dimmer)
                        logInfo("test", "dimmer "+dimmer)
                        timer_Lumiere_Salon.reschedule(1000)
                }
                else timer_Lumiere_Salon = null
                ])
                logInfo("test", "end of Else")
        }
        logInfo("test", "end of rule")
end

which gave the following log output when I fired the rule (Item Dummy switched to ON)

2018-10-14 20:57:17.132 [INFO ] [.eclipse.smarthome.model.script.test] - rule triggered 2018-10-14 20:57:17.142 [INFO ] [.eclipse.smarthome.model.script.test] - ON 2018-10-14 20:57:17.162 [INFO ] [.eclipse.smarthome.model.script.test] - end of Else 2018-10-14 20:57:17.166 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 10 2018-10-14 20:57:17.169 [INFO ] [.eclipse.smarthome.model.script.test] - end of rule

But replacing timer_Lumiere_Salon.reschedule(1000) with timer_Lumiere_Salon.reschedule(now.plusSeconds(1)) gave the following log output

2018-10-14 21:42:35.580 [INFO ] [.eclipse.smarthome.model.script.test] - rule triggered 2018-10-14 21:42:35.589 [INFO ] [.eclipse.smarthome.model.script.test] - ON 2018-10-14 21:42:35.597 [INFO ] [.eclipse.smarthome.model.script.test] - end of Else 2018-10-14 21:42:35.601 [INFO ] [.eclipse.smarthome.model.script.test] - end of rule 2018-10-14 21:42:35.605 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 10 2018-10-14 21:42:36.614 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 20 2018-10-14 21:42:37.623 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 30 2018-10-14 21:42:38.633 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 40 2018-10-14 21:42:39.641 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 50 2018-10-14 21:42:40.663 [INFO ] [.eclipse.smarthome.model.script.test] - dimmer 60

I cannot explain this behavior but it does the trick.
For your information, my version of openHAB is 2.3.0-1

Ludovic

This reschedules the timer to go off one second after midnight January 1, 1970.

You need to use now.plusMilliseconds(1000) to schedule it for one second from now.

That is why your change made it work.

understood. Thanks!

Hi @rlkoshak, you might want to change this also in the original DP example, as it states there

            // do stuff
            timer.reschedule(1000)

instead of

            // do stuff
            timer.reschedule(now.plusMilliseconds(1000))

Thanks, I updated the OP.

Please correct me if I’m wrong, but I’m getting the following error in the log

 - Validation issues found in configuration model 'zigbee.rules', using it anyway:
The operator '!=' should be replaced by '!==' when null is one of the arguments.

I think this is where the

if(ceilingTimer != null) return;

checks the state of

var Timer ceilingTimer = null

, and it compares the same type with each other. (as I read here)

So the code has to be

if(ceilingTimer !== null) return;

because null and null are of the same type?

correct

not correct. You need to use !== because null is literally one of the operands. When you see null on either side of the comparison you need to use the operator that has the extra =.

Hi Rich,

Is there a way to extend this design rule to several actions? Do something, wait for condition, do another thing, wait for other condition, do another thing, wait for new condition, … . Can i just place multiple timers below? Thanks

See my reply on your original post for how I would do what you originally described as wanting to do (i.e. do something, wait some amount of time and do something else, then wait a different amount of time and do something else). For that use case, I think just creating separate Timers is the better way to go.

To answer your specific question here, all things are possible but does it make sense? For your use case on the other thread, separate non-looping timers are probably a better choice. If you have a different use case in mind, I’d need more details about what you are trying to accomplish (don’t confuse what you are trying to accomplish with how you are trying to accomplish it, sometimes referred to as the XY Problem).

1 Like

Thanks for your reply. Sorry i just posted here when i saw you responded to my original post.

What is the best way to implement a daemon process which runs always? I don’t want to start it in the “system startup” event because if I develop it I don’t want to restart the system in every minute. So I have created this cron below to start it. But if I change the code, I got I huge exception in the logs. I don’t have problem with that, but is this the best solution for it?

var Timer timer = null

rule "cron process"
when
	Time cron "0/10 * * * * ? *"
then
	if (timer === null)
	{
		logInfo("cron", "start timer")
		timer = createTimer(now, [ |
        	//do the job here....
			
			timer.reschedule(now.plusMillis(200))
    	])
	}
end
2 Likes

The exception in the logs is because the timer will still be running when you save the code, but the code that is supposed to run is recompiled. So this creates a mess when the timer expires and tries to run the code which is no longer where the timer thought it was. So this is quite normal when developing with timers.

I guess this is a possible way to always have some code triggered, but the question always is: Is there not a better way, like reacting to a more specific event to execute some code.

If it is some piece of code that evaluates something and then does nothing in 99% of cases, I would try to make it as efficient as possible in evaluating the case of “nothing to do”.

1 Like

Arno has nailed the answer. But I have my own additional concerns.

OH is not a real time system. It really is not equipped to handle polling something from Rules as fast as you are trying to do. Personally, I’d say anything faster than 750 msec should be moved outside of OH Rules and placed in an external service that polls whatever is being polled and reports changes to OH through the REST API or MQTT.

I don’t think JSR223 Rules will necessarily have the same issue but even there I’d consider polling something this fast as inappropriate. Rapid polling like this really needs to be in the binding or in the device that is reporting to OH.

I suppose you don’t realize that if you change a .rules file, OH will rerun the System started Rules when it reloads the file. There is no need to restart anything to trigger System started Rules. Furthermore, for testing while you are developing, you can always add a testing Switch as a trigger and manually kick off the Rule.

I usually recommend against creating permanent solutions to temporary problems.

Basically what I want to do is to create some kind of “RGB led daemon” for xiaomi gateway which handles and executes the different kind of lightning effects coming from the different rules. For example if I ask this service (via a virtual Item) to dim the light with red color, then this service would turn on the light, change the color, and change the brightness of the light in every 200ms (or 500ms, we will see). This is the reason why I want to implement this logic in OpenHAB. Embedding and copy-pasting this logic for several rules is not the best way, I think.
On the other hand, I would like to implement another service (which runs e.g. in every seconds) which is responsible to handle the “light requests” in the gateway. For example I have a yellow light effect, which means a window is open (remains yellow, until the window is closed). But I have another light effect (blinkling 5 times the blue light), which means the entrance door has been opened. So if the yellow light is on, but someone enters the door, then the gateway should blink the blue light 5 times and then it should switches back to yellow. And this scenario should be more convenient to be implemented with a daemon process, which “remembers” the previously used light color.
These scenarios are only in my theory yet. We will see, what the xiaomi gateway let me to do. :slight_smile:

I’m not sure that Xiaomi gateway can accept and process commands that fast. I know Hue can’t based on what others have reported.

But, based on what you describe, you should trigger the rule when your virtual Item receives a command, then run the looping timer only until the light reaches the desired state. Then the timer exits. Don’t have the CPU sit there spinning and doing nothing if you can help it.

You don’t need to loop for this. You just need to trigger a Rule when the entrance door is opened. Save the current state of the light to a variable. Then blink the light how ever you want. Finally restore the light to the state stored in the variable.

It is absolutely NOT more convenient to implement with a looping timer.

OH is event driven. Write your Rules to respond to events. Don’t have it sitting there looping waiting for things to happen. Let is sleep and react when something does happen.

Thank you for the answer and the suggestion. I also would like to approach most of the scenarios in event-based way. Basically only the first example (light dimming) lead me to this “Looping timers” pattern, but I also have fears that if I force the gateway to dim the whole night, then probably next day I have to order a new device :slight_smile:
If the gateway does not support native dimming effect, then probably I should not force it to do.