Design Pattern: Looping Timers

Tags: #<Tag:0x00007f74595c9340>

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, often with a Thread::sleep to wait for some event to occur or some state to change before continuing the rest of the Rule. However, long running Rules are a bad idea (see Why have my Rules stopped running? Why Thread::sleep is a bad idea).

Concept

image

Use a Timer that reschedules itself instead of a while loop.

Simple Example

This example is as simple as it gets. To implement this while loop:

while(condition){
    // do stuff
    Thread::sleep(1000)
}

use

Python

while condition:
    # do stuff
    sleep(1)

See the first reply. JSR223 Python does not have the same limitation with long running Rules that Rules DSL does.

JSR223 JavaScript

TODO… I believe JavaScript lacks a sleep command so the looping timer approach would work for it.

Rules DSL

var Timer timer = null // global variable usually

...

    timer = createTimer(now, [ |
        if(condition) timer = null

        else {
            // do stuff
            timer.reschedule(now.plusMillis(1000))
        }
    ])

In the original while loop we continue to loop until a condition is met. At the end of the loop we sleep for a second.

In the replacement we first check the condition. If the condition is false we create a Timer to execute now. Inside the Timer we check the condition. If the condition is false, do the body of the loop and reschedule the timer. Then if the condition has been met set the timer to null and exit without rescheduling, effectively canceling the looping.

Hmmm, this looks kind of extra complicated. That’s true because I was very careful to make sure the two loops behave exactly the same.

For example, if the condition is true before the while loop executes, then no loops should occur. This is implemented by checking for the condition and if the condition is true, set the timer to null and exit the timer without rescheduling the timer.

We only check the condition after sleeping for a second, not before. We accomplish this by making the first thing done in the Timer is checking the condition and if it is true don’t perform the loop.

Theory of Operation

We create a Timer to execute immediately.

The first thing the Timer lambda does is check to see if the condition is true. If it is true, simply set the timer to null and exit.

If the condition is not met, perform the loop code then reschedule the timer to run again in another second.

Alternative Implementation

There are other ways to implement this timer with the same behaviors some of which are a bit shorter in lines of code. For example

Rules DSL

var Timer timer = null // global variable usually

...

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

This works if you don’t care whether timer goes back to null again when the Timer exits. Can you see how the two are equivalent?

Complex Example

Let’s say we have a ceiling fan that we want to turn on at the first detection motion at night and continue to run until morning, but only if the temperature is above a threshold. We will check the temp every minute. A naive and dangerous implementation would be a while loop with a minute sleep.

NOTE: Never use ReentrantLocks, they have lots of problems and can crash your Rules no matter how careful you are. Since the below is a counter example anyway, I’ve left the lock.

import java.util.concurrent.locks.ReentrantLock

var fanLock = new ReentrantLock

rule "Ceiling fan control"
when 
    Item MotionSensor changed to ON
then
    if(vTimeOfDay.state != "NIGHT" || !fanLock.tryLock) return;

    try {

        while(vTimeOfDay.state == "NIGHT"){
            var newState = "STAY"
            if(CurrentTemp.state > TargetTemp.state) newState = "ON"
            else if(CurrentTemp.state < TargetTemp.state - 1) newState = "OFF" // -1 to provide a buffer for hysteresis

            if(newState != "STAY" && Fan.state.toString != newState) Fan.sendCommand(newState)

            Thread::sleep(60000)
        }
    }
    catch(Exception e) {
        logError("Fan", "Error controlling the fan: " + e)
    }
    finally {
        fanLock.unlock
    }
end

The theory of operation is when motion occurs and it’s night, loop while it is night time. If the temp is above the target turn on the fan. If it is one degree below the target temp turn off the fan. Then sleep for a minute before checking again. A lock is used to tell when the while loop is running and exit any subsequent instances of the Rule while one has a while loop.

As discussed in the Why have my Rules Stopped link above, this is a really bad idea because it will tie up a Rule execution thread all night long. Worse, because of the lock, additional motion events that occur after the rule starts running at NIGHT will also sit and consume a Rule thread potentially blocking all your Rules until morning.

One might first try to implement this using a Time cron trigger, but we cannot predict from one night to the next when the loop should start. It is a variable event that occurs that kicks off the loop. Obviously one could use an Item or variable to tell the Time cron trigger when it can start controlling the fan, but we can also use a Looping Timer.

Python

Copied from @5iver’s example below, updated to use the Helper Libraries.

from core.rules import rule
from core.triggers import when
from core.util import sendCommandCheckFirst
from time import sleep
from threading import Lock

global fanLock
fanLock = Lock()

@rule("Ceiling fan control")
@when("Item MotionSensor changed to ON")
def ceiling_fan(event):
    ceiling_fan.log.debug("JSR223: Ceiling Fan Control: starting rule")
    global fanLock
    if items["vTimeOfDay"] != StringType("NIGHT") or not fanLock.acquire(False): return

    try:
        while items["vTimeOfDay"] == StringType("NIGHT"):
            newState = "STAY"
            if items["CurrentTemp"] > items["TargetTemp"]: newState = "ON"
            elif float(str(items["CurrentTemp"])) < float(str(items["TargetTemp")) - 1: newState = "OFF"

            if newState != "STAY": 
                sendCommandCheckFirst("Fan", newState)
                ceiling_fan.log.debug("newState = {}".format(newState))

            sleep(60)
        except Exception as e:
            ceiling_fan.log.error("Error controlling the fan: {}".format(e))
        finally:
            fanLock.release()
            ceiling_fan.log.log.debug("JSR223: Ceiling Fan Control: ending rule")

JavaScript

TODO

Rules DSL

var Timer ceilingTimer = null

rule "Ceiling fan control"
when
    Item MotionSensor changed to ON
then
    if(ceilingTimer != null) return;

    ceilingTimer = createTimer(now, [ |
        if(vTimeOfDay.state == "NIGHT"){
            
            var newState = "STAY"
            if(CurrentTemp.state > TargetTemp.state) newState = "ON"
            else if(CurrentTemp.state < TargetTemp.state - 1) newState = "OFF" // -1 to provide a buffer for hysteresis

            if(newState != "STAY" && Fan.state.toString != newState) Fan.sendCommand(newState)

            ceilingTimer.reschedule(now.plusMillis(60000))
        }

        else ceilingTimer = null
    ])
end

Looping Timers with Expire Binding

This section will show the complex example above using the Expire binding rather than Timers.

Switch CeilingFanTimer { expire="1m,command=OFF" }

Python

from core.rules import rule
from core.triggers import when

@rule("Ceiling fan control")
@when("Item MotionSensor changed to ON")
def ceiling_fan(event):
    if items["CeilingFanTimer"] != ON: events.sendCommand("CeilingFanTimer", "OFF") # kick off the loop

@rule("Ceiling fan loop")
@when("Item CeilingFanTimer received command OFF")
def ceiling_timer(event):
    if items["vTimeOfDay"] == StringType("NIGHT"): return

    newState = "STAY"
    if items["CurrentTemp"] > items["TargetTemp"]: newState = "ON"
    elif float(str(items["CurrentTemp"])) < float(str(items["TargetTemp")) - 1: newState = "OFF"

    if(newState != "STAY" && Fan.state.toString != newState) Fan.sendCommand(newState)
    events.sendCommand("CeilingFanTimer", "ON")

Rules DSL

rule "Ceiling fan control"
when
    Item MotionSensor changed to ON
then
    if(CeilingFanTimer.state != ON) CeilingFanTimer.sendCommand(OFF) // kick off the loop immediately
end

rule "Celing Fan Loop"
when
    Item CeilingFanTime received command OFF
then
    if(vTimeOfDay.state != "NIGHT") return;

    var newState = "STAY"
    if(CurrentTemp.state > TargetTemp.state) newState = "ON"
    else if(CurrentTemp.state < TargetTemp.state - 1) newState = "OFF" // -1 to provide a buffer for hysteresis

    if(newState != "STAY" && Fan.state.toString != newState) Fan.sendCommand(newState)

    CeilingFanTimer.sendCommand(ON)

end

The code is slightly simpler with fewer indents and checks but it requires a new Item and two Rules instead of just one. but even in the case of the Python version, that doesn’t need the Timer to avoid blocking, the Expire binding version of the code is simpler.

Advantages and Disadvantages

Looping Timers have a few advantages over while loops in this case.

  • They don’t tie up a Rule Execution thread to run. In fact the rule should take a handful of milliseconds to run and exit.
  • They don’t even tie up a Quartz Timer thread because it only uses a thread when it is running and that should take a handful of milliseconds to complete.
  • They don’t require the same sorts of resource controls (e.g. the ReentrantLock in the Complex Example) to prevent multiple instances of the Rule from running at the same time.

In short, the above code only ties up resources (threads, CPU, etc) which the code is actively running and avoids doing so when the code is waiting to run.

The disadvantage is primarily that implementation requires a few more lines of code. Though once you take into account the locking mechanisms required to use while loops properly the code ends up roughly the same.

Related Design Patterns

Design Pattern How It’s Used
Design Pattern: 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

Edit: Added Python examples, cleaned up some of the Rules DSL code.

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

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

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.