Automation/Orchestration Design Patterns

RLK back with another Design Pattern to add to the list.

Time Of Day Triggers

The Problem:

Many of us have rules that we want to only execute or behave differently during certain time periods of the day. As our rules grow there is the potential that more and more parts of the system will need to have this capability (e.g. lighting, HVAC, blinds, etc). Sometimes the times that define a period are fixed (e.g. 11pm) and others they are based on celestial events (e.g. sunset). This can result in a scattering of cron and Astro triggers throughout your rules which would need to be updated should you decide to change the time things happen.

The Solution:

Create a set of switches that get turned ON at the start of a time period and OFF at the end of the time period. Then in your rules which care about the time of day check the state of these switches.

I’ve expanded my lighting rule from above to illustrate this approach.

Items:

// Light Switches and Groups
Group:Switch:OR(OFF,ON) gLights "All Lights"    <light>
Group gMorningLights "Lights that turn on before dawn, off at dawn" <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, gMorningLights)     {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>

// Time and Weather Items
Switch      Day
Switch      Twilight
Switch      Night
Switch      Morning

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.types.*
import org.openhab.core.library.items.*
import org.eclipse.xtext.xbase.lib.*
import java.util.Map
import java.util.Set
import org.joda.time.*


//----------------Lighting Rules-------------------------
//-------------------------
// Global Variables
//-------------------------
val String TIMER = "TIMER"
val String WEATHER = "WEATHER"
val String MANUAL = "MANUAL"
val Map<SwitchItem, Boolean> overridden = newHashMap
var String whoCalled = ""

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

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

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

//-----------------------------------------------------------------------------
// TODO See if I can just use gLights for switching all on and off
rule "Any light in gLight triggered"
when
    Item gLights received update
then
    Thread::sleep(250) // give lastUpdate time to be populated
    val mostRecent = gLights.members.sortBy[lastUpdate].last as SwitchItem
    logDebug("Lights", "Most recent is " + mostRecent.name)
    if(whoCalled == MANUAL) {
        logInfo("Lights", "Overriding " + mostRecent.name)
        overridden.put(mostRecent, true)
    }

    // Keep S_L_All up to date
    if(gLights.members.filter(l|l.state==ON).size > 0) S_L_All.postUpdate(ON)
    else S_L_All.postUpdate(OFF)
end

//-----------------------------------------------------------------------------
// The Any gLight triggered 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

rule "Lights Morning ON"
when
    Item Morning changed from OFF to ON
then
    logInfo("Lights", "Timer turning on Morning lights.")
    whoCalled = TIMER

    gMorningLights.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(ON, false, TIMER, light)
    ]
end

rule "Light Morning OFF"
when
    Item Morning changed from ON to OFF
then
    logInfo("Lights", "Timer turning off Morning lights.")
    whoCalled = TIMER

    gMorningLights.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(OFF, false, TIMER, light)
    ]
end

rule "Lights Twilight"
when
    Item Twilight changed from OFF to ON
then
    logInfo("Lights", "Timer turning Twlight lights.")
    whoCalled = TIMER

    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

    gSunsetTimerLights.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(ON, false, TIMER, light)
    ]
end

rule "Lights Night"
when
    Item Night changed from OFF to ON
then
    logInfo("Lights", "Timer turning off bedtime lights.")
    whoCalled = TIMER

    gOffTimerLights.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(OFF, false, TIMER, light)
    ]
end

rule "Lights Sunrise"
when
    Item Day changed from OFF to ON
then
    logInfo("Lights", "Good morning, activating Weather Lights rule")
    whoCalled = MANUAL
    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.state == ON) {
        logDebug("Lights", "Checking the weather conditions")
        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
            logDebug("Lights", "Processing " + light.name)
            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

//----------------Time of Day Rules-------------------------
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 night = now.withTimeAtStartOfDay.plusHours(23).millis

    if(now.isAfter(morning) && now.isBefore(sunrise)) {
        logInfo("Weather", "Initializing, it is Morning")
        Morning.sendCommand(ON)
        Day.sendCommand(OFF)
        Twilight.sendCommand(OFF)
        Night.sendCommand(OFF)
    }
    else if(now.isAfter(sunrise) && now.isBefore(twilight)) {
        logInfo("Weather", "Initializing, it is Day")
        Morning.sendCommand(OFF)
        Day.sendCommand(ON)
        Twilight.sendCommand(OFF)
        Night.sendCommand(OFF)
    }
    else if(now.isAfter(twilight) && now.isBefore(night)) {
        logInfo("Weather", "Initializing, it is Twilight")
        Morning.sendCommand(OFF)
        Day.sendCommand(OFF)
        Twilight.sendCommand(ON)
        Night.sendCommand(OFF)
    }
    else {
        logInfo("Weather", "Initializing, it is Night")
        Morning.sendCommand(OFF)
        Day.sendCommand(OFF)
        Twilight.sendCommand(OFF)
        Night.sendCommand(ON)
    }
end

rule "Morning start"
when
    Time cron "0 0 6 * * ? *"
then
    logInfo("Weather", "Its Morning!")
    Morning.sendCommand(ON)
    Day.sendCommand(OFF)
    Twilight.sendCommand(OFF)
    Night.sendCommand(OFF)
end

rule "Sunrise started"
when
    Item Sunrise_Event received update
then
    logInfo("Weather", "Its Sunrise!")
    Morning.sendCommand(OFF)
    Day.sendCommand(ON)
    Twilight.sendCommand(OFF)
    Night.sendCommand(OFF)
end

rule "Twilight started"
when
    Item Twilight_Event received update
then
    logInfo("Weather", "Its Twilight!")
    Morning.sendCommand(OFF)
    Day.sendCommand(OFF)
    Twilight.sendCommand(ON)
    Night.sendCommand(OFF)
end

rule "Night started"
when
    Time cron "0 0 23 * * ? *"
then
    logInfo("Weather", "Its Night!")
    Morning.sendCommand(OFF)
    Day.sendCommand(OFF)
    Twilight.sendCommand(OFF)
    Night.sendCommand(ON)
end

I leave as an exercise to the student how these rules can be collapsed quite a bit using a couple of lambdas.

4 Likes