Please see Design Pattern: What is a Design Pattern and How Do I Use Them.
This Design Pattern is a revisitation of a solution I wrote up here.
Problem Statement
There are many situations where one needs to cause a series of activities to take place in sequence with the option of being able to cancel the sequence of activities and be able to control how long each step takes to complete. One common use case is for controlling an irrigation system that consists of more than one zone.
Concept
OH 2.5 Python and OH 3.x JavaScript Concept
The Python and JavaScript implementations of Design Pattern: Gate Keeper can be used to implement this DP and there are examples for both in that DP.
The concept is to have a Rule that gets triggered when it is time to kick off the series of events. Add the commands to run to the Gatekeeper with the appropriate amount of a delay between each. When a cancel event needs to occur, call cancel_all() on the Gatekeeper.
Rules DSL Concept
The concept is to have a rule that gets triggered when one of the Items representing one of the steps in the series receives a command. This rule figures out what step in the sequence is currently running and based on that figures out what step to run next. A timer is set for the end of the activity which will cause the rule to run again and kick off the next step.
Example
The example will use an irrigation use case. There are two or more zones that need to be irrigated in sequence. The amount of time to irrigate each zone can be different and the amount of time is controlled by an Item that can be edited on the sitemap. The irrigation is initially kicked off by a cron trigger, but could easily be kicked off by an Astro binding event, a Time of Day DP, and Alarm clock example.
Items
String Irrigation_Curr "The current active zone is [%s]"
Switch Irrigation_Auto "Automatic irrigation is [%s]"
Switch Irrigation_Manual "Irrigation state [%s]"
Group:Switch:OR(ON,OFF) gIrrigation "Irrigation is currently [%s]"
Group:Number:SUM gIrrigation_Times "Total Irrigation Time is [%d mins]"
Switch Irrigation_Zone_1 (gIrrigation)
Number Irrigation_Zone_1_Time (gIrrigation_Times)
Switch Irrigation_Zone_2 (gIrrigation)
Number Irrigation_Zone_2_Time (gIrrigation_Times)
Switch Irrigation_Zone_3 (gIrrigation)
Number Irrigation_Zone_3_Time (gIrrigation_Times)
Switch Irrigation_Zone_4 (gIrrigation)
Number Irrigation_Zone_4_Time (gIrrigation_Times)
Switch Irrigation_Zone_5 (gIrrigation)
Number Irrigation_Zone_5_Time (gIrrigation_Times)
Switch Irrigation_Zone_6 (gIrrigation)
Number Irrigation_Zone_6_Time (gIrrigation_Times)
Switch Irrigation_Zone_7 (gIrrigation)
Number Irrigation_Zone_7_Time (gIrrigation_Times)
Switch Irrigation_Zone_8 (gIrrigation)
Number Irrigation_Zone_8_Time (gIrrigation_Times)
OH 3.x JavaScript
triggers:
- id: "1"
configuration:
time: 08:00
type: timer.TimeOfDayTrigger
- id: "2"
configuration:
itemName: Irrigation_Manual
type: core.ItemCommandTrigger
conditions: []
actions:
- inputs: {}
id: "4"
configuration:
type: application/javascript
script: >-
var logger =
Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.model.script.Rules.Irrigation");
var OPENHAB_CONF = java.lang.System.getenv("OPENHAB_CONF");
load(OPENHAB_CONF+"/automation/lib/javascript/community/gatekeeper.js");
this.gk = (this.gk === undefined) ? new Gatekeeper() : this.gk;
var turnOffAll = function(){
this.gk.cancelAll();
events.sendCommand("gIrrigation", "OFF");
events.postUpdate("Irrigation_Curr", "OFF");
}
turnOffAll();
var command_zone = function(zone, cmd) {
events.sendCommand(zone, cmd);
events.postUpdate("Irrigation_Curr", (cmd == ON) ? zone.name : "OFF");
}
if(items["Irrigation_Auto"] == ON || event.itemCommand == ON) {
for(var zone in ir.getItem("gIrrigation").members()){
this.gk.addCommand(items[zone.name+"_Time"], function() { command_zone(zone, "ON"); });
this.gk.addCommand(1, function(){ events.sendCommand(zone.name, "OFF"); });
}
}
type: script.ScriptAction
Note: the above has not been tested and may not work as expected. It’s mainly to illustrate how to use the Gatekeeper to implement this.
Theory of Operation:
Not shown is a rule to send an OFF command to all members of gIrrigation on system start.
At 08:00 or when Irrigation_Manual receives a command the rule will trigger. Any activity that is ongoing will be cancelled and valves will be turned off. If Irrigation_Auto is ON or the rule was manually triggered each zone is scheduled with two commands. The first turns on the zone and sets a delay in the gatekeeper based on the “Time” Item. The second will run when that time has passed and turn off the valve.
OH 2.5 Python
from core.rules import rule
from core.triggers import when
from core.utils import sendCommand, postUpdate
from community.gatekeeper import Gatekeeper
gatekeeper = None
@rule("Reset Irrigation at OH Start")
@when("System started")
def reset_irrigation(event):
for valve in [valve for valve in ir.getItem("gIrrigation").members if valve.state != ON]:
sendCommand(valve, OFF)
postUpdate(Irrigation_Curr, "OFF")
@rule("Start Irrigation at 08:00")
@when("Time cron 0 0 8 * * ?")
@when("Item Irrigation_Manual received command ON")
def start_irrigation(event):
if items["Irrigation_Auto"] == ON or event.itemCommand == ON:
# Reset the gatekeeper
global gatekeeper
if gatekeeper is not None:
gatekeeper.cancel_all()
gatekeeper = Gatekeeper(start_irrigation.log)
# Schedule the Irrigation Zone Commands
def command_zone(zone, cmd):
start_irrigation.log.info("{} Irrigation for {}".format("Starting" if cmd == ON else "Stopping"))
sendCommand(zone, cmd)
postUpdate("Irrigation_Curr", zone.name if cmd == ON else "OFF")
for zone in sorted(ir.getItem("gIrrigation").members):
# Turn on the zone
gatekeeper.add_command(int(items["{}_Time".format(zone.name)]),
lambda: command_zone(zone, ON))
# Turn off the zone, have one second before turning on the next zone
gatekeeper.add_command(1, lambda: command_zone(zone, OFF))
@rule("Cancel Irrigation")
@when("Item Irrigation_Manual received command OFF ")
def cancel_irrigation(event):
global gatekeeper
gatekeeper.cancel_all()
for valve in [valve for valve in ir.getItem("gIrrigation").members if valve.state != ON]:
sendCommand(valve, OFF)
postUpdate("Irrigation_Curr", "OFF")
gatekeeper = None
def scriptUnloaded():
global gatekeeper
if gatekeeper is not None:
gatekeeper.cancel_all()
Theory of Operation:
When openHAB starts up, close all valves and set Irrigation_Curr to OFF.
When irrigation starts, add commands to the Gatekeeper by creating lamdas based on a locally defined function that logs and turns on/off the valve and enforces a delay after the command runs before the next command is allowed to run. Thus we add a command to turn on a valve and update Irrigation_Curr with a delay for how long the valve should be opened. Then schedule a command to turn off the valve and update Irrigation_Curr. The Gatekeeper therefore will control the timing of the irrigation, one after the other.
To cancel the irrigation, call cancel_all() on the Gatekeeper and make sure all the valves are turned off.
NOTE: the above code has not been tested, please let me know of any errors.
OH 2.5 and OH 3.x Rules DSL
var Timer irrigationTimer = null
rule "Reset Irrigation at OH Start"
when
System started
then
// use this line to just turn everything off if OH happens to restart when irrigation is running
gIrrigation.members.filter[valve|valve.state != OFF].forEach[valve| valve.sendCommand(OFF)]
Irrigation_Curr.postUpdate("OFF")
// use this line if you have persistence and want to reset the timer cascade when OH comes back online
// sendCommand(Irrigation_Curr.state.toString, ON) // kicks off the cascade again on the last zone that was running
// Irrigation_Curr.sendCommand(Irrigation_Curr.state)
end
rule "Start Irrigation at 08:00"
when
Time cron "0 0 8 * * ?" or
Item Irrigation_Manual received command ON
then
if(Irrigation_Auto.state == ON || receivedCommand == ON){
Irrigation_Manual.postUpdate(ON) // set it on if not already
logInfo("Irrigation", "Irrigation started, turning on Zone 1")
Irrigation_Curr.sendCommand(Irrigation_Zone_1.name)
}
end
rule "Irrigation Cascade"
when
Item Irrigation_Curr received command
then
// get info for the current valve
val currValve = gIrrigation.members.findFirst[ valve | valve.name == receivedCommand.toString]
val currValveNum = Integer::parseInt(currValve.name.split("_").get(2))
val currValveMins = gIrrigation_Times.members.findFirst[ t | t.name == currValve.name+"_Time" ].state as Number
// get info for the next valve in the sequence
val nextValveNum = currValveNum + 1
val nextValveName = Irrigation_Zone_+nextValveNum
val nextValve = gIrrigation.members.findFirst[ valve | valve.name == nextValveName] // null if there is no member by that name
// TODO: You will probably want to add some error checking above in case Items are NULL or UNDEF
// Turn on curr valve
currValve.sendCommand(ON)
// Create a timer to turn off curr valve and start the next valve
irrigationTimer = createTimer(now.plusMinutes(currValveMins.intValue), [ |
logInfo("Irrigation", "Turning off " + currValve.name)
currValve.sendCommand(OFF)
if(nextValve !== null) {
logInfo("Irrigation", "Turning on " + nextValve.name)
Irrigation_Curr.sendCommand(nextValve.name) // causes the Irrigation Cascade rule to trigger
}
else {
logInfo("Irrigation", "Irrigation is complete")
Irrigation_Manual.sendCommand(OFF) // causes the cancel rule to trigger for cleanup
}
irrigationTimer = null
])
end
rule "Cancel Irrigation"
when
Item Irrigation_Manual received command OFF
then
// Cancel the timer if there is one, the ? will cause the line to be skipped if timer is null
irrigationTimer?.cancel
irrigationTimer = null
// Turn off any open valves
gIrrigation.members.filter[ valve | valve.state != OFF ].forEach[ valve | valve.sendCommand(OFF) ]
// Update curr status
Irrigation_Curr.postUpdate("off")
end
I leave the sitemap as an exercise to the student.
NOTE: the above code has not been tested. It hasn’t even been loaded into VSCode. I’m sure there are errors.
Theory of Operation:
When the starting event occurs (in this case 08:00 time or the Irrigation_Manual Switch receiving and ON command) the https://community.openhab.org/t/design-pattern-proxy-item/15991Irrigation_Curr is updated with the name of the first zone and the first zone in the sequence is started with an ON
command.
This triggers the cascade rule which gets the runtime for the first zone, a reference to the next zone in the sequence, and creates a timer which expires after the first zone’s runtime.
When the timer expires it stops the first zone, sets the place holder to the name of the second zone and starts the second zone with an ON
command. The ON
command triggers the cascade rule again.
The cascade rule continues to run after the timer expires for each zone until we get to the end of the zones. At this point, Irrigation_Manual
is sent an OFF
command which causes the cancel rule to trigger.
The cancel rule gets triggered whenever the Irrigation_Manual
Switch receives an OFF
command. This rule cancels the timer, if it exists and is still running, closes all open valves, and updates the place holder Item to “off” to indicate irrigation is not running.
Advantages and Limitations
One big limitation to the above is that it assumes the same code needs to be run for each zone. For a use case like irrigation that is a safe assumption. However, if you want to do something different for each zone then this design pattern needs a much more complex set of code in the Timer to select the correct activity based on the current zone.
Another risk one takes is that if OH does go down during irrigation and never comes back up the irrigation valve will never turn off. This is not something that can be solved by OH though. If the end device supports it, a fail safe should be implemented.
The sequence is cancellable at any time and the status of the irrigation can be monitored on the sitemap (which zone is running, total runtime of all zones could be calculated, etc.). The amount of time each zone runs for is also controllable from the sitemap.
Related Design Patterns
Design Patterns | How It’s Used |
---|---|
Design Pattern: Proxy Item | Irrigation_Curr |
Design Pattern: Working with Groups in Rules | Lots of Group member operations |
Design Pattern: Associated Items | Obtaining the Irrigation Time Item |
Design Pattern: Sensor Aggregation | Adds up the total irrigation time |
Design Pattern: Unbound Item (aka Virtual Item) | Several Items including Irrigation_Curr |
Edit: Added reference to JavaScript implementation and updates for OH 3