Automation/Orchestration Design Patterns

I’ve debated whether to continue this thread or to start a new one and decided to add to this thread. Ultimately this will probably be added to the wiki and/or included in the OH 2 user’s guide.

So, it has been several months since I posted my design patterns above and as I’ve refactored things I’ve come up with some improvements which I will post here.

Time of Day Design Pattern

In the original above I use Switches to represent the current time of day state. This made sense at the time because it more closely followed how a state machine would work. However, in practice, I found that using multiple switches adds a lot of extra logic when you care about more than one state at a time (see the Group and Filter code below). So the big change is to follow the suggestion @watou made on another thread and instead of using multiple Switches use a single (two actually but more on that later) String Item to represent state.

In the Items and Rules below there are two Items to store the current time of day and the previous time of day. There are a number of other Items used to trigger the start of a new time of day based on sunrise and sunset. We use the switch to trigger a rule to transition to the new time of day state and the DateTime when openHAB starts and we need to figure out what time of day it currently is.

Items

String          TimeOfDay
String          PreviousTimeOfDay

Switch          Twilight_Event                              (Weather) { astro="planet=sun, type=set, property=start, offset=-90" }
DateTime        Twilight_Time   "Twilight [%1$tr]"  <moon>  (Weather) { astro="planet=sun, type=set, property=start, offset=-90" }
Switch          Sunset_Event                                (Weather) { astro="planet=sun, type=set, property=start" }
DateTime        Sunset_Time     "Sunset [%1$tr]"    <moon>  (Weather) { astro="planet=sun, type=set, property=start" }
Switch          Sunrise_Event                               (Weather) { astro="planet=sun, type=rise, property=start" }
DateTime        Sunrise_Time    "Sunrise [%1$tr]"   <sun>   (Weather) { astro="planet=sun, type=rise, property=start" }

String Condition_Id "Weather is [MAP(yahoo_weather_code.map):%s]" (Weather) { weather="locationId=home, type=condition, property=id" }

Rules

import org.openhab.core.library.types.*
import org.joda.time.*
import org.eclipse.xtext.xbase.lib.*

val Functions$Function3 updateTimeOfDay = [String tod, String ptod, boolean update |
        logInfo("Weather", "Setting PreviousTimeOfDay to \"" + ptod + "\" and TimeOfDay to \"" + tod + "\"")
        if(update) {
                TimeOfDay.postUpdate(tod)
                PreviousTimeOfDay.postUpdate(ptod)
        }
        else {
                TimeOfDay.sendCommand(tod)
                PreviousTimeOfDay.sendCommand(ptod)
        }
]

rule "Get time period for right now"
when
        System started
then
    val morning = now.withTimeAtStartOfDay.plusHours(6).millis
        val sunrise = new DateTime((Sunrise_Time.state as DateTimeType).calendar.timeInMillis)
        val twilight = new DateTime((Twilight_Time.state as DateTimeType).calendar.timeInMillis)
        val evening = new DateTime((Sunset_Time.state as DateTimeType).calendar.timeInMillis)
        val night = now.withTimeAtStartOfDay.plusHours(23).millis

        if(now.isAfter(morning) && now.isBefore(sunrise))       updateTimeOfDay.apply("Morning", "Night", true)
        else if(now.isAfter(sunrise) && now.isBefore(twilight)) updateTimeOfDay.apply("Day", "Morning", true)
        else if(now.isAfter(twilight) && now.isBefore(evening)) updateTimeOfDay.apply("Twilight", "Day", true)
        else if(now.isAfter(evening) && now.isBefore(night))    updateTimeOfDay.apply("Evening", "Twilight", true)
        else                                                    updateTimeOfDay.apply("Night", "Evening", true)
end

rule "Morning start"
when
        Time cron "0 0 6 * * ? *"
then
    updateTimeOfDay.apply("Morning", TimeOfDay.state.toString, false)
end

rule "Day start"
when
        Item Sunrise_Event received update ON
then
    updateTimeOfDay.apply("Day", TimeOfDay.state.toString, false)
end

rule "Twilight start"
when
        Item Twilight_Event received update ON
then
    updateTimeOfDay.apply("Twilight", TimeOfDay.state.toString, false)
end

rule "Evening start"
when
        Item Sunset_Event received update ON
then
        logInfo("Weather", "Its Evening!")
        PreviousTimeOfDay.sendCommand(TimeOfDay.state.toString)
        TimeOfDay.sendCommand("Evening")
end

rule "Night started"
when
        Time cron "0 0 23 * * ? *"
then
    updateTimeOfDay.apply("Night", TimeOfDay.state.toString, false)
end

NOTES:

  • We will need both the Times and the Events
  • Persistence is not required for this to work
  • Because rrd4j does not support Strings and mapdb does not give you the previous value, maintaining the previous time of day in a separate Item is required (if you need to know the previous state in your rules)

In a rule that may do something different based on the time of day you use an if statement similar to:

if(TimeOfDay.state.toString == "Night")

To trigger a rule when the time of day changes:

when
    Item TimeOfDay received command
then

To trigger a rule when it becomes a specific time of day:

when
    Item TimeOfDay received command "Night"
then

Group and Filter

The concept is still the same but I wanted to update my example with my current lighting setup so it illustrates my use of TimeOfDay and PreviousTimeOfDay and it illustrates some more examples of how to use Groups to organize things to make rules simpler.

The big changes you will find is the elimination of the lambda and the movement of the state that was being stored in global vars to Items. Also, by using Groups I’m able to consolidate the rules that were triggered based on time of day Switches into a single rule. A new concept illustrated here is also the use of naming conventions which we can use to programmatically construct the name of an Item of a Group and filter that Item or Group out of a higher level group.

The concept is as follows:

  • Each time of day has two groups, an ON group and an OFF group. The group names follow the pattern g<time of day>Lights<ON or OFF> (e.g. gMorningLightsON). Lights which are members of an ON group will be turned on when that time of day starts. Lights which are members of an OFF group will be turned off when that time of day ends. This allows one to turn on a light during one time of day and turn them off at a later time of day without toggling them between times of day.
  • All of the groups belong to a gTimerLights group so we can find the one we want by name when the time of day changes.
  • Each light now has a secondary Override switch and these Overrides belong to the gLightsOverride
  • The old whoCalled var is now a String state and is used to determine whether a light is turned on manually (and therefore should override the rules) or by a rule.
  • There is one rule that toggles the lights based on the weather which can be manually overridden.
  • By default V_WhoCalled is set to “MANUAL”. When a time of day causes the lights to change all overrides are removed and while the rule is processing the V_WhoCalled gets changed to “TIMER”. When the Weather Rule executes V_WhoCalled is set to “WEATHER”. When any light is toggled for any reason (manually, timer, or weather rule) the Override Lights rule gets called. If V_WhoCalled is “MANUAL” it means the light has been manually triggered so the light is marked as overridden.

Items

Group:Switch:OR(ON,OFF) gLights "All Lights"    <light>
Group gTimerLights
Group gMorningLightsON          (gTimerLights)
Group gMorningLightsOFF         (gTimerLights)
Group gDayLightsON              (gTimerLights)
Group gDayLightsOFF             (gTimerLights)
Group gTwilightLightsON         (gTimerLights)
Group gTwilightLightsOFF        (gTimerLights)
Group gEveningLightsON          (gTimerLights)
Group gEveningLightsOFF         (gTimerLights)
Group gNightLightsON            (gTimerLights)
Group gNightLightsOFF           (gTimerLights)
Group gWeatherLights

Group gLightsOverride
String V_WhoCalled

Switch  S_L_Front           "Front Room Lamp"  <light> (gLights, gWeatherLights, gMorningLightsON, gMorningLightsOFF, gTwilightLightsON, gEveningLightsOFF) {zwave="3:command=switch_binary"}
Switch  S_L_Front_Override                             (gLightsOverride)
Switch  S_L_Family          "Family Room Lamp" <light> (gLights, gWeatherLights, gTwilightLightsON, gEveningLightsOFF)                                      {zwave="10:command=switch_binary"}
Switch  S_L_Family_Override                            (gLightsOverride)
Switch  S_L_Porch           "Front Porch"      <light> (gLights, gEveningLightsON, gEveningLightsOFF)                                                       {zwave="6:command=switch_binary"}
Switch  S_L_Porch_Override                             (gLightsOverride)
Switch  S_L_All         "All Lights"           <light>

Rules

import org.openhab.core.types.*
import org.openhab.core.items.*
import org.openhab.core.library.items.*
import java.util.Set

// 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")

// Turn off the lights from the previous time of day and turn on the lights for the current time of day
rule "TimeOfDay changed"
when
        Item TimeOfDay received command
then
        // Disable overrides
        V_WhoCalled.sendCommand("TIMER")

        Thread::sleep(100) // give lastUpdate time to catch up

        // Turn off previous time of day lights
        val lastTod = PreviousTimeOfDay.state.toString
        val offGroupName = "g"+lastTod+"LightsOFF"
        logInfo("Lights", "Timer turning off " + offGroupName)
        val GroupItem offGroup = gTimerLights.members.filter[g|g.name == offGroupName].head as GroupItem
        offGroup.members.forEach[light |
                logInfo("Lights", "Timer turning OFF " + light.name)
                light.sendCommand(OFF)
        ]

        // Turn on current time of day lights
        val onGroupName = "g"+receivedCommand+"LightsON"
        logInfo("Lights", "Timer turning on " + onGroupName)
        val GroupItem onGroup = gTimerLights.members.filter[g|g.name == onGroupName].head as GroupItem
        onGroup.members.forEach[light |
                logInfo("Lights", "Timer turning ON " + light.name)
                light.sendCommand(ON)
        ]

        Thread::sleep(1000) // give all Override rules to finish running after all the switching above
        V_WhoCalled.sendCommand("MANUAL")
        gLightsOverride.members.forEach[o | o.sendCommand(OFF)]
end

// Control ALL the lights with this switch. Put a sleep between triggering each light so the "Override Lights"
// rule is guaranteed to execute correctly.
rule "All Lights Switch"
when
        Item S_L_All received command
then
        V_WhoCalled.sendCommand("MANUAL") // Using the All switch counts as an override
    gLights.members.forEach[light |
        sendCommand(light, S_L_All.state.toString)
        try {Thread::sleep(110)} catch(InterruptedException e) {} // sleep to avoid fouling up mostRecent above
    ]
end

// This rule gets called for ALL updates to ALL lights. Set the Override flag to ON for
// any light that is updated when V_WhoCalled is set to MANUAL
rule "Override Lights"
when
        Item gLights received update
then
        Thread::sleep(100) // give lastUpdate time to be populated
        val mostRecent = gLights.members.sortBy[lastUpdate].last as SwitchItem
        if(V_WhoCalled.state == "MANUAL") {
                logInfo("Lights", "Overriding " + mostRecent.name)
                gLightsOverride.members.filter[o|o.name == mostRecent.name+"_Override"].head.sendCommand(ON)
        }

        // Keep S_L_All up to date, but use postUpdate so we don't trigger rule below
        // If one or more lights is OFF leave the state as OFF so we can toggle the rest
        if(gLights.members.filter[l|l.state==OFF].size > 0) S_L_All.postUpdate(OFF)
        else S_L_All.postUpdate(ON)
end

// When it is Day, turn on or off the WeatherLights when the weather says it is cloudy.
rule "Weather Lights On"
when
        Item Condition_Id changed
then

        // Only run the rule during the day
        if(TimeOfDay.state.toString == "Day") {

                // Get the new light state
                val State state = if(cloudyIds.contains(Condition_Id.state)) ON else OFF

                // Toggle any non-overridden lights
                V_WhoCalled.sendCommand("WEATHER")
                gWeatherLights.members.forEach[ light |
                        if(gLightsOverride.members.filter[o|o.name == light.name + "_Override"].head.state == OFF &&
                           light.state.toString != state.toString){

                                logInfo("Lights", "Weather turning " + light.name + " " + state.toString)
                                light.sendCommand(state.toString)
                                try {Thread::sleep(100)} catch(InterruptedException e){} // don't overwhelm "Any light in gLight triggered" rule
                        }
                ]
                V_WhoCalled.sendCommand("MANUAL")
        }
end
5 Likes