Design Pattern: Looping Timers

designpattern
Tags: #<Tag:0x00007fd316b29f08>

(Rich Koshak) #1

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

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

var Timer timer = null // global variable usually

...

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

        else {
            // do stuff
            timer.reschedule(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

var Timer timer = null // global variable usually

...

    timer = createTimer(now, [ |
        if(!condition){
            // do stuff
            timer.reschedule(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.

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 has my Rules Stopped link above, this is a really bad idea because it will tie up a Rule execution thread all night long.

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.

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(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" }
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.

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. (Assumes a Thread::sleep in the loop).
  • 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

Where to find syntax for createTimer command
How to create a simple loop?
Problem with rule locking per ReentrantLock
(Scott Rushworth) #2

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())

(Ahmad Yazan Tibi) #3

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

(Rich Koshak) #4

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.