Edit: Updates for OH 4
Please see Design Pattern: What is a Design Pattern and How Do I Use Them.
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
Schedule a sequence of functions to be called with a set amount of time between them.
Blockly
This design pattern can be implemented using the openHAB Rules Tools implementation of Design Pattern: Gate Keeper. In Blockly it can look like this:
Gatekeeper supports cancelling scheduled commands.
JS Scripting:
var {Gatekeeper} = require('openhab_rules_tools');
var gk = cache.private.get('gatekeeper', () => Gatekeeper());
gk.addCommand('PT5M', () => {
console.info('zone 1 start');
items.IrrigationZone1.sendCommand('ON');
});
gk.addCommand('PT10M;, () => {
console.info('zone 2 start');
items.IrrigationZone1.sendCommand('OFF');
items.IrrigationZone2.sendCommand('ON');
}
gk.addCommand('PT2M;, () => {
console.info('zone 3 start');
items.IrrigationZone2.sendCommand('OFF');
items.IrrigationZone3.sendCommand('ON');
}
gk.addCommand('PT0S';, () => {
console.info('irrigation done');
items.IrrigationZone3.sendCommand('OFF');
}
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.
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 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.
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 |