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