Design Pattern: Cascading Timers

designpattern
Tags: #<Tag:0x00007fe051b265e0>

(Rich Koshak) #1

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

image
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 or one of the Alarm Clock examples, or based on any other event supported by OH.

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)

Rules

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

    // 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
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.postUpdate(Irrigation_Zone_1.name)
        Irrigation_Zone_1.sendCommand(ON)
    }
end

rule "Irrigation Cascade"
when
    Member of gIrrigation received command
then
    // get info for the current valve
    val currValve = gIrrigation.members.findFirst[ valve | valve.name == Irrigation_Curr.state.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
    
    // Create a timer to turn off curr valve and start the next valve
    irrigationTimer = createTimer(now.plusMinutes(currValveMins), [ |
        logInfo("Irrigation", "Turning off " + currValve.name)
        currValve.sendCommand(OFF)

        if(nextValve !== null) {
            logInfo("Irrigation", "Turning on " + nextValve.name)
            Irrigation_Curr.postUpdate(nextValve.name)
            nextValve.sendCommand(ON) // 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 (switch statement would be a good choice).

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.

Some of the advantages to the above is that each zone is individually controllable, the trigger that kicks off the sequence is independent, and there is code to recover in case OH goes down while irrigation is running.

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

Design Pattern: Expire Binding Based Timers
Complete watering system based on openHAB
Best way to make long time macro
Design Pattern: Cancel Activity
Rule for changing temp and resume rule after power loss
Integrate Siemens Logo (plclogo) in OpenHAB 2
OH 2.x Timer Things
OpenSprinkler Irrigation and Cascading timers rule
AT Smart House
Help with a loop (Runs too fast / Ignores timers)
[SOLVED] Help me condense this rule
Linking a string and integer together to create a string for notifications
Irrigation Controller need help with Master Valves
Control of Solar& Pellets Heater system - OpenHab Beginners experience
Slider trigger only when mouse up?
Configurable Sequencer with timed steps (e.g. for holiday light effects)
Complete watering system based on openHAB
Design Pattern: Recursive Timers
(Carsten Wuthenow) #2

Hi,

I adjusted the rule code after checking in designer and fixed some minor errors. The cascading rule should only be triggered by ON commands, otherwise the OFF commands also trigger the rule which is creating a misbehaviour.

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

rule "Start Irrigation"
when
    Item Irrigation_Auto received command ON
then
	logInfo("Irrigation", "Irrigation started, turning on Zone 1")
	Irrigation_Curr.postUpdate(Irrigation_Zone_1.name)
	Irrigation_Zone_1.sendCommand(ON)
end

rule "Irrigation Cascade"
when
    Item Irrigation_Zone_1 received command ON or
    Item Irrigation_Zone_2 received command ON or
    Item Irrigation_Zone_3 received command ON or
    Item Irrigation_Zone_4 received command ON or
    Item Irrigation_Zone_5 received command ON 

then
    // get info for the current valve
    val currValve = gIrrigation.members.filter[valve|valve.name == Irrigation_Curr.state.toString].head
    //logInfo("Irrigation", "currValue: " + currValve.name)
    val currValveNum = Integer::parseInt(currValve.name.split("_").get(2))
    //logInfo("Irrigation", "currValveNum: " + currValveNum)
    val currValveMins = gIrrigation_Times.members.filter[t|t.name == currValve.name+"_Time"].head.state as Number
	logInfo("Irrigation", "irrigation active for : " + currValveMins + " mins")

    // get info for the next valve in the sequence
    val nextValveNum = currValveNum + 1
    val nextValveName = "Irrigation_Zone_"+nextValveNum
    val nextValve = gIrrigation.members.filter[valve|valve.name == nextValveName].head // null if there is no member by that name
    
    // 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.postUpdate(nextValve.name)
            nextValve.sendCommand(ON) // causes the Irrigation Cascade rule to trigger
        }
        else {
            logInfo("Irrigation", "Irrigation is complete")
            Irrigation_Auto.sendCommand(OFF) // causes the cancel rule to trigger for cleanup
        }
        irrigationTimer = null
    ])
end


rule "Cancel Irrigation"
when
    Item Irrigation_Auto 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

(Alpoy) #3

These design pattern posts and examples, wouldn’t it be reasonable to give these posts it’s own sub-category or at least pinned under the examples and tutorials or setup/configuration/rules ?
IMHO, these posts are brilliant (thank you for making these Rich!) and should almost be the first thing that you see when you load the forum page! Pretty sure a lot of the problems people post about could be solved by reading these…

My 2 cents…:wink:


(Angelos) #4

they are tagged with the “designpattern” and there is a reference to them in:
http://docs.openhab.org/tutorials/
(check the last 2 lines in that docs entry)

Yes, they are excellent :slight_smile:

Ps: I need to notify @ThomDietrich cause the last link gives 404 (it should be: https://community.openhab.org/c/tutorials-examples instead of https://community.openhab.org/c/setup-configuration-and-use/tutorials-examples)… I will open up a PR when I find some time