Automation/Orchestration Design Patterns

Somehow I missed this posting when you first posted it. I’ll add my current favorite design pattern for posterity. Maybe others will contribute as well and this could become a wiki page or something.

I like the example and how I’ve subconsciously been using the same approach in a couple of cases in my rules.

I’ll add a new Design pattern which I’ll call Group and Filter .

The overall approach is to put items into Groups and write your rules to operate on the Groups rather than individual items. This approach is best suited for cases where one has multiple items that do the same thing based on certain events, or multiple items that generate the same kind of event. The pattern serves as a way to both simplify and centralize the rules logic and it allows one to change the behaviors of individual items simply by changing their Group membership.

In the rules use the Group as the trigger* and loop through the Groups members to implement the logic.

  • NOTE: Beware of a received update on a Group as the rule will be triggered multiple times for each update

I’ll use a lighting example as well. I have the following categories of lights:

  • Lights which turn on when the weather says it’s cloudy (I don’t have photosensors configured yet)
  • Lights which turn on 90 minutes before sunset
  • Lights which turn on at sunset
  • Light which turn off at 11 pm

One other thing you will see in the rules is that if a light that is turned on or off by the weather is manually turned on or off, that light is considered overridden and will no longer be changed based on the weather until the following day.

Also, in my case the lights I want to come on 90 minutes before sunset are the same as those I want to come on or turn off based on the weather so I use the same group for both. But if that ever changes, I’ll create a new group to separate the two behaviors.

Items:

Group:Switch:OR(OFF,ON) gLights "All Lights"    <light>
Group gWeatherLights "Lights controlled by weather conditions and twilight" <light>
Group gOffTimerLights "Off Timer Lights" <light>
Group gSunsetTimerLights "Sunset On Lights" <light>

Switch  S_L_Front           "Front Room Lamp"           <light> (gLights, gOffTimerLights, gWeatherLights)     {zwave="3:command=switch_binary"}
Switch  S_L_Family          "Family Room Lamp"          <light> (gLights, gOffTimerLights, gWeatherLights)     {zwave="4:command=switch_binary"}
Switch  S_L_Porch       "Front Porch"               <light> (gLights, gOffTimerLights, gSunsetTimerLights) {zwave="6:command=switch_binary"}
Switch  S_L_All         "All Lights"                <light>

Switch          Twilight_Event                                                          (Weather)       { astro="planet=sun, type=set, property=start, offset=-90" }
Switch      Sunset_Event                                (Weather)   { astro="planet=sun, type=set, property=start" }
Switch          Sunrise_Event                                                           (Weather)       { astro="planet=sun, type=rise, property=start" }

Rules:

import org.openhab.core.types.*
import org.openhab.core.library.items.*
import org.eclipse.xtext.xbase.lib.*
import java.util.Map
import java.util.Set
    
//-------------------------
// Global Variables
//-------------------------

// Constants used to tell which rule is attempting to change the light's state, part of the overridden behavior
val String TIMER = "TIMER"
val String WEATHER = "WEATHER"
val String MANUAL = "MANUAL"
var String whoCalled = ""

// Keeps track of which lights have been overridden
val Map<SwitchItem, Boolean> overridden = newHashMap

// turns on or off the weather rule
var boolean day = true

// Yahoo cloudy weather condition IDs
val Set<String> cloudyIds = newImmutableSet("0",  "1",  "2",  "3",  "4",  "5",  "6",  "7",  "8",
                                                "9", "10", "11", "12", "13", "14", "15", "16", "17",
                                                "18", "19", "20", "26", "28", "35", "41", "43", "45",
                                                "46", "30", "38")

// Lambda called when a rule wants to turn on or off a light    
val Functions$Function4 applySwitch = [ State state,
                                            boolean override,
                                            String whoCalled,
                                            SwitchItem light |
        if(state != light.state) {
                if(!override) {
                        logInfo("Lights", whoCalled + " turning " + light.name + " " + state.toString)
                        sendCommand(light, state.toString)
                }
                else {
                        logInfo("Lights", whoCalled + " " + light.name + " is overridden")
                }
        }
]

//-----------------------------------------------------------------------------
// Resets the overrides
rule "Lights System Startup"
when
        System started
then
        whoCalled = MANUAL
        gLights?.members.forEach[light | overridden.put(light as SwitchItem, false) ]
end

//-----------------------------------------------------------------------------
// Called when any light is triggered, used to capture manual changes and override the lights as necessary
rule "Any light in gLight triggered"
when
        Item gLights received update
then
         // These two lines are a way to get the item from the group that triggered the rule. It only works if updates
         // don't occur too fast where another item will receive an update before we can grab mostRecent
        Thread::sleep(250) // give lastUpdate time to be populated
        val mostRecent = gLights?.members.sortBy[lastUpdate].last as SwitchItem

        if(whoCalled == MANUAL) {
                logInfo("Lights", "Overriding " + mostRecent.name)
                overridden.put(mostRecent, true)
        }
end

//-----------------------------------------------------------------------------
// The "Any gLight triggered" rule will be called for each of items in gLights, causing
// them to become overridden
rule "All Lights"
when
        Item S_L_All received command
then
    gLights?.members.forEach(light | sendCommand(light, S_L_All.state.toString))
end

//-----------------------------------------------------------------------------
// Implement the behavior of lights that come on 90 minutes before
// sunset
rule "Lights Twilight"
when
        Item Twilight_Event received update
then
    logInfo("Lights", "Timer turning Twlight lights.")
        whoCalled = TIMER
        day = false // deactivate the weather rule

        // Since the weather rule is turned off at Twight, reset the overridden lights
        gWeatherLights?.members.forEach[light |
                overridden.put(light as SwitchItem, false)
                applySwitch.apply(ON, false, TIMER, light)
        ]
end

//-----------------------------------------------------------------------------
rule "Lights Sunset"
when
        Item Sunset_Event received update
then
        logInfo("Lights", "Timer turning on Sunset lights.")
        whoCalled = TIMER

        // Since the weather rule is turned off at Twight, reset the overridden lights
        gSunsetTimerLights?.members.forEach[light |
                overridden.put(light as SwitchItem, false)
                applySwitch.apply(ON, false, TIMER, light)
        ]
end

//-----------------------------------------------------------------------------
rule "Lights Bedtime"
when
        Time cron "0 0 23 * * ? *"
then
    logInfo("Lights", "Timer turning off bedtime lights.")
        whoCalled = TIMER

        // Since the weather rule is turned off at Twight, reset the overridden lights
        gOffTimerLights?.members.forEach[light |
                overridden.put(light as SwitchItem, false)
                applySwitch.apply(OFF, false, TIMER, light)
        ]
end

//-----------------------------------------------------------------------------
// Reenable the weater rule
rule "Lights Sunrise"
when
        Item Sunrise_Event received command ON
then
        logInfo("Lights", "Good morning, activating Weather Lights rule")
        day = true
        whoCalled = MANUAL

        // Reset the overridden lights
        gLights?.members.forEach[light |
                logDebug("Lights", "Populating overridden map with " + light.name)
                overridden.put(light as SwitchItem, false)
        ]
end

//-----------------------------------------------------------------------------
rule "Weather Lights On"
when
        Item Condition_Id changed
then

        if(day) {
                var State state = OFF
        if(cloudyIds.contains(Condition_Id.state)) state = ON
                logInfo("Lights", "Setting weather lights to " + state)
                whoCalled = WEATHER
                val i = gWeatherLights?.members.iterator
                while(i.hasNext) {
                        val light = i.next
                        if(overridden.get(light) == null) overridden.put(light as SwitchItem, false)
                        applySwitch.apply(state, overridden.get(light).booleanValue, WEATHER, light)
                }
                Thread::sleep(500)
                whoCalled = MANUAL
        }
end

You will note that I have a lot of redundancy built into these rules. For example, the overridden mapping gets reset all over the place. I do this for a few reasons. One is because some items may be members of only one of the groups and if this is the case it will not have its overridden status reset. Another is that if I decide to take out a rule or radically change the behavior for that group the behaviors of the other groups do not rely on a side effect from another group.

By using this design pattern I reduced the size of this original rule set by more than 50% while meeting the same requirements and making it easier to update the behavior of my Items.

2 Likes