Automation/Orchestration Design Patterns

There is reasoning for both courses.

The kitchen light may be ON and Motion sensor still OPEN for a period of time after someone leaves the kitchen. Motion sensors (battery-powered ones anyway) are tripped and then become insensitive for a period of time–staying OPEN until that period expires. In the case of this model, 4 minutes (not changeable).

As Kai noted in an earlier comment, this can get pretty hairy very rapidly. Consider the follow use-case scenarios:

Suppose a person dashed into the kitchen to grab a quick drink of water (thus tripping the motion sensor and turning the light ON), then immediately left the house within the 4 minute period. If the person manually turned out the light when leaving the kitchen the motion sensor would still be OPEN so you could intercept the FDC rule to check if the sensor was OPEN and light was OFF, and, if so bypass the ON/timer set logic. However, if the exiting person does NOT manually turn off the light before leaving, your solution would have to check the motion sensor OPEN and light ON condition for the bypass. So it is really the OPEN motion sensor that is the potential bypass gate, not the light state. If the motion sensor is CLOSED, then the FDC lighting might be bypassed for exiting the house.

However, the front door does not “know” whether the door open/close events are due to an arrival or a exit. If a second person was in the home, grabbed a quick drink from the kitchen (thus triggering the motion sensor OPEN - light ON logic), turned the light OFF manually and went upstairs, and someone arrives home within the motion sensor latency, under your proposal, the arriving person would enter a dark house (the motion sensor would still be OPEN , so your modified FDC rule would bypass the light ON logic). If the at-home second person got their drink and left the light ON, the arriving person could find the lights going OFF on them IF they arrived during the motion sensor latency period (because the motion sensor would go CLOSED, triggering a quick OFF timer).

My code style of pre-emptively/redundantly setting of new_state to OFF is (mostly) an artifact of having written a lot of C in the past i.e., I try to ALWAYS initialize variables to some known value. In this instance, I agree that the case is open-and-shut (so-to-speak), but I stylistically prefer to always initialize default values for variables, and likewise nullify-and-recreate timers rather than reschedule an instance.

(I also hang up my clothes and wash my dishes, so this pattern is probably a personality disorder, but I’ll live with it).

Is there any way to unit test XText rules? With complex logic and scenarios it seems that it would be easy to cause a regression when updating rule definitions.

(Like others in this forum, I don’t like re-using timers and do a lot of checking for timer-existence and clean-up/re-creation.)

Can you point me to the thread that discusses the issues with rescheduling timers instead of cancelling and creating new ones? I tried to search for it but I didn’t find anything.

1 Like

Not sure there is a thread per se re timer re-use. I know that it has come up in passing more than once. Besides myself, I know that @rlkoshak has commented on using this style. I (personally) started doing this adamantly when I had several occasions where an attempted reschedule did not work as expected. (Possibly pilot error on my part, but nonetheless it was a problem. )

That being said, there are also occasions where it is actually a useful positive practice (as opposed to defensive practice) to cancel and nullify timers. A principal example would be for data collection as to the proximate cause of the state change of an item (like a light), where it could have been a manual operation, a timer operation or a motion sensor operation which was the trigger. For example, if you have a timer-scheduled OFF operation queued up, the light goes OFF and you want to know at run-time WHY it went off. If the timer exists but has expired, the timer was the proximate cause. If the timer is exists but is not expired, then something else was the proximate cause. (and you probably want to clean up the unexpired instance lest it jump up and bite you shortly). Add in other causes and timers and it can get hairy to back-figure the causal paths.

That being said, I’m no XText guru. It might be possible to build a bunch of logic into the

[ | 
set up timer
add more stuff here
...
]   

bracketing, BUT then you would easily end up with (for lack of a better term) “rule-snarl” across space-time in a way which I find disturbing.

Thanks for the clarifications.

I’ve been seeing the same thing with the JSR223 timer management which mirrors the implementation used with Xtext rules. It seems that there are at least two issues. One issue is that one cannot reschedule a timer that has expired or been canceled. The underlying Quartz scheduler will actually tell the caller that the reschedule failed. However, the openHAB code does not check the status. It always assumes the Quartz reschedule succeeded, sets the terminated flag to false and returns true from the openHAB reschedule call. I’m going to submit a PR to fix these issues.

THAT is a really good thing to know. When I reworked my rules to use the design pattern described above I also eliminated most of my timers and all of my timer reschedules. But I have been giving advice on the forum with assumption that a reschedule of an expired timer would work.

The answer is not really but you can fake it. I unit test individual rules by creating testing Switches in my Items file and add that Switch to the triggers for the rules I want to test. Then when I manually switch the testing item on the Sitemap it kicks off the rule. Though I can say this quickly becomes untenable when you have complex rules that depend on the state of a lot of other items like Bob’s use case above.

And I’ll just throw in that that debugging lambdas, whether manually defined or included in the [ ] of a timer is a bear because often errors do not propagate or show up in the logs. The Rule or the Timer just stops running. Logging is your friend in this case.

I’ve commented about it offhandedly here and provided examples but I have a fear in the back of my mind that I’ve actually provided an example of rescheduling a timer without checking whether it has expired or not. :frowning: However, like Bob, I usually try to structure my code so a timer doesn’t need to be rescheduled, either creating a new one or using a Time trigger on another rule and a flag to enable/disable the logic.

Normally I’m crazy about avoiding useless polling like this but I’m running openHAB on a sizable machine (an old laptop) and polling once per second or slower has negligible performance impact.

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

Hi Bob,

I’ve been testing out Openhab over the past week or two. In particular I want to improve on my current Motion detector/Lighting functionality using Openremote. This is simply a timer that turns off lights after a given period of no motion. Openhab has a bticino binding which allows me to get feedback on whether lights are on or off so this adds more intelligence to my rules.

I am hoping to be able to use your proxy idea so that if someone switches on a light manually then it stays on (perhaps with an eventual timeout). Likewise if they switch off a light it should stay off (also with some time out).

Otherwise by default I would want the lights to come on when motion is detected and it is dark, then switch off automatically. I have got the astro binding working with a proxy so I am ok for the Is_Dark function.

So far I’ve used your code as a base - but I think there is a difference in the way your motion detectors work. Mine are DSC alarm detectors so Open momentarily if movement is detected then close again.

Are you able to help me get this working? I think I need to make some changes to the Pxy 2 rule.

Thanks
Julian

Hi Julian,

If I understand your situation correctly, your motion detectors do not “LATCH ON” but instead fire an event “ON” – (both metaphorical, but bear with me). So you cannot check state at timer expiration because there is no “LATCH”-ed state. The question is how often they fire an event-“ON”. If it is every time there is motion, you could use that event ON to reset the OFF timer (and eliminate the state check in the example timer-expiration logic).

That help any ?

1 Like

Hi - yes my motion detectors keep firing OPEN and CLOSED events.

I am making slow progress - I think more by trial and error - I’ve eliminated the state check timer so that the OPENING and CLOSING just resets the timer.

The way it is working now is that I have a Proxy switch turns on when there is movement and stays on for a designated time. I can see how I use this to turn on the real lights but I want to have a manual element:

  1. If I switch on the lights then they should stay on and ignore the motion detector for a fixed period of time. This is mostly for outside lights which keep turning off because there is motion which sets up a trigger off event.
  2. If I switch off the lights they should also stay off and ignore the motion detector for a fixed period of time (eg: to allow you to leave a room)

I was think that when I turn on the lights I can test whether the Proxy switch is on. However if you trigger the light before you switch it on then this might not work!

rule “SW_1: When office light is switched on”
when
Item Lt_Office changed from OFF to ON
then
if (Proxy_Lt_Office.state ==OFF) {
//there is not active movement timer running which means this is manual
//set up a timer to keep light on for 1 hours unless turned off manually
//at end of 1 hour revert to motion control
}
end

The rule for not turning off I am not so clear on. I need to intercept the Motion on rule and stop it triggering.

rule “SW_1: When office light is switched off”
when
Item Lt_Office changed from ON to OFF
then {
//there is motion detected during exit from room then ignore
//for a period of time then revert to normal

Is it possible (physically) to manually switch the light (either direction) without being in range of (and triggering) the motion detector ? Or are you using a GUI on web/phone to do the “manual” switching ? If you are using a GUI to do the manual switching, you could set a latching variable in the handling of the proxy event switch from the GUI. (This could also be a second proxy which you could check at execution time in other rules.)

Another approach would be to use the manual OFF operation to start a named timer which is checked for existence during the Motion ON rule and skipping the sendCommand that manipulates the Light if the timer is still running/exists.

Do you also have a PROXY_SYNC-type rule which keeps the proxy and real device states aligned ? Such a rule would trigger on “Item Lt_Office changed from OFF to ON” (as in your above SW_1) or “ON to OFF” as in your SW_2.

rule "PXY_SYNCH: Update Lt_Office PROXY state"
    when
        Item Lt_Office changed from OFF to ON
    then
        Thread::sleep(10)
        logDebug("OFFICE","PXY_SYNCH: Updating Lt_Office Proxy to:" + Lt_Office.state)
        postUpdate(Lt_Office_PROXY, Lt_Office.state)
end

You do have to be careful to avoid open-ended updating loops between devices and proxies. By that I mean use “changed from…to…” as opposed to simply “changed”. So you may want two sync rules for both variants of “from…to…”. You can have either the device or the proxy with a simple “changed” rule, but having both with a simple “changed” will get you a potential race condition. (This can be entertaining in the “WTF is going on sense”, but definitely not what you want.)

Especially with dimmers you can easily get a race condition going between the device and proxy, so it is also important to have a Thread::sleep() in the updating clauses on syncing rules. (You may have to experiment with the argument to sleep() that works for you. ) The sleep() allows for both order of sequence and differential execution timing between sendCommand and postUpdate to be reasonably smoothed to induce the desired behavior.

For the garden lighting yes we can switch manually without being in range. The bticino binding is contunually polling the lighting system so knows whether lights are on or off so I think whether I switch via the GUI or the wall I can pick up an Item change. I think the latching variable sounds the one to try. So do I add another switch item to the proxy rule or is this a variable in which case how does this get sent to the SW_1 rule? If it is a switch perhaps then I can use this in the interface to display or even change the status of the automation.

I think I’d set the latch as another switch variant on the real device name, viz

sw_Light_Office  (the real deal)
sw_Light_Office_PROXY (for action target, interceptable at rule execution time as otherwise shown)
sw_Light_Office_LATCH (a specific state item which can be part of the interception checking routing in the PROXY, but settable by anything in your rule/script space).   

The LATCH.state is just an aggregate result of all the things where you want to override your general rules, but it lets you set/unset the LATCH.state anywhere in your rules and shorthand that into the PROXY state check at interception time rather than enumerating all the exception cases in the PROXY rule. I personally think I"d keep timer checks etc inline in the PROXY rather than having the timer toggle the LATCH, and use the LATCH to capture other exceptional conditions. (eg nobody home, your motion situations, etc)

Thanks that works for me - final question I promise! Can you combine 2 switches on one line in the sitemap - so I’d want to have Office light it’s latch state which I can use as an override (ie turn off/on automation) and also it’s real state which I can also turn on and off. I’m guessing not as I’ve not seen it anywhere.

You might be able to display something in the real item using color driven by the override state, but you’ll need to tinker with that a bit. From a UI/functionality perspective, I don’t think you’d want them fully intertwined with each other in a full control sense. I’d probably put the override “switch(es)” in a separate group entirely and “hide” them one layer down just to keep the confusion factor down.

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

Julian, any chance you can post your working code? I’m trying to do the same thing - kitchen switch which detects motion, but when I want light on permanently I want to override it at the manual switch. I go past my motion sensor on the way in and the way out.

Posting this here so I can easily find it later. Two more design patterns came to mind.

Separation of Behaviors

Problem Statement:
I have found in my rules and from helping others that often times certain sorts of checks, conditionals, etc. cross cut multiple functionality categories. For example, lighting and HVAC may both care about what time of day it is. Just about anything may want to send a notification. Lighting, blind/rollershutter controls/HVAC may all care whether it is cloudy.

What can often happen is the same logic gets duplicated in multiple places as the rules check for these states or generate the actions. For example, if today we are using my.openhab for notifications we might have a call to sendNotification() all over our files and if we decide we don’t like my.openhab and want to use Notify My Android instead we have to change it everywhere.

Concept:

Set up a proxy Item to represent the state or trigger the action and a rule to centralize the logic. There are two approaches depending on the direction of the logic.

The first approach is to set up a String or Number Item that your rules sendCommand some value to that triggers an action, such as sending a Notification. In this rule you can then start to take into consideration additional traits or add additional logic which would be difficult to implement across your files as lambdas such as, as illustrated below, use a different notification service depending on the time of day.

Items

String Notification_Proxy_Info
String Notification_Proxy_Alarm

Rules

val String logNameNotification = "notification"

rule "Dispatch Info Notification"
when
        Item Notification_Proxy_Info received update
then
        val String msg = Notification_Proxy_Info.state.toString
        logInfo(logNameNotification, msg)
        if(TimeOfDay.state != "Night") sendPushToDefaultDevice(msg)
end

rule "Dispatch Alarm Notification"
when
        Item Notification_Proxy_Alarm received update
then
        val String msg = Notification_Proxy_Alarm.state.toString
        logError(logNameNotification, msg)
        if(TimeOfDay.state == "Night") sendPushToDefaultDevice(msg) else sendNotification("email", msg)
end

I can send a notification with a simple Notification_Proxy_Info.sendCommand("Notification text!")

The second approach is to set up a proxy Item to hold the result of a complex calculation (e.g. a Number of a Switch) and the state of this Item is checked in other rules. For example, below I have a Weather_Cloudy Item Switch which gets set to ON when the Weather’s Condition ID says it is cloudy. By centralizing this check I can now react in rules across my environment without duplicating logic and change it if needed in one location (e.g. switch from Yahoo to Wunderground).

NOTE: This is an update to the Lighting rule int eh Group and Filter example above.

Item

Switch      Weather_Cloudy       "Conditions are Cloudy [%s]" <rain>

Rule

rule "Update Cloudy Switch"
when
        Item Condition_Id changed
then
    // Yahoo cloudy weather condition IDs
    val Set<String> yahooCloudyIds = 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", "29", "30", "44")
    // https://www.wunderground.com/weather/api/d/docs?d=resources/phrase-glossary
        val Set<String> wundergroundCloudyIds = newImmutableSet("2",  "3",   "4",  "6", "9", "10", "11", "13",
                                                                "14", "15", "16", "18","19", "20", "21", "22",
                                                                "23", "24")
        val isCloudy = yahooCloudyIds.contains(Condition_Id.state.toString)
        if(isCloudy && Weather_Cloudy.state != ON){
                Weather_Cloudy.sendCommand(ON)
        }
        else if(!isCloudy && Weather_Cloudy.state != OFF) {
                Weather_Cloudy.sendCommand(OFF)
        }
end

In the lighting rule I can check if it is cloudy with a simple Weather_Cloudy.state == ON.

Dead Man’s Switch

Problem Statement
This one is a bit more complex in both implementation and concept. Sometimes you are in a situation where you want to know where a certain command came from but there is no way built into the binding or the device to tell the difference between a manually initiated command at the device or a command initiated by a rule. For example, when one wants to override a rule’s behavior when a light switch is manually triggered.

Concept
Create a dead-man’s-switch which gets set to one value at all times except when a rule is running. When a rule runs it temporarily sets the dead-man’s-switch Item to something beside the default value then changes it back to the default value. A rule gets triggered for ALL changes to the device’s Item. This rule check’s the dead-man’s switch and if it is set to the default value we know the Item was manually triggered.

For example, in the Group and Filter rules above I have a dead-man’s-switch called V_WhoCalled which gets set to “MANUAL” by default, “TIMER” by the rule that changes the lights based on Time Of Day, and “WEATHER” when the weather lighting rule runs.

In a simpler and more genericexample:

Items

String DeadMansSwitch
Group gOverrideItems

// Switches, Dimmers, etc that are members of gOverrideItems which we want to know whether it was manually changed or not

Rules


// Set the Dead Man's Switch off of default during startup
rule "System Started"
when
    System started
then
    DeadMansSwitch.sendCommand("STARTUP")
    createTimer(now.plusSeconds(30), [| DeadMansSwitch.sendCommand("MANUAL")
end

rule "Timer Rule"
when
    Item TimeOfDay changed
then
    DeadMansSwitch.sendCommand("TIMER")
    Thread::sleep(50) // give it time to populate through the event bus
    // do stuff
    Thread::sleep(200) // give stuff time to complete
    DeadMansSwitch.sendCommand("MANUAL")
end

// more rules which trigger members of gOverideItems

rule "Is Manually Triggered?"
when
    Item gOverrideItems received update // we don't care if it triggers more than once per update 
then
    if(DeadMansSwitch.state.toString == "MANUAL") {
        // the Item was manually triggered
    }
end

In the rules above if the Item is updated at the device (assuming the device reports such updates) or on the sitemap DeadMansSwitch will be “MANUAL” and we will know the device was manually updated.

8 Likes

Nice !

One more design pattern to add here so I can find it again and reference is future posts.

#Group Persistence

Problem Statement
There are several reasons why a home automation enthusiast would want to use openHAB persistence: charting, using historical Item states in rules logic, restore previous values on openHAB restart, and my.openhab integration, detailed analysis, access to the data with external tools, etc. However, different databases are more or less well suited to each of these use cases.

Concept
Since no one persistence engine is best for all use cases, configure more than one and use Groups to allocate which Items get saved to which persistence engine and how.

For example, one can set up MapDB for restoreOnStartup, rrd4j for charting recent data and historical data, my.openhab for IFTTT integration, and MySQL for Items for detailed external analysis.

For each use case, create a Group and only use these groups in the .persist files.

Then allocate your Items to whatever group(s) based on how that specific Item will be used.

Example
In my current setup I restoreOnStartup on all my Items, I use rrd4j for charting and historical state, I have a couple of Items I expose to IFTTT through my.openhab, and no Items that I analyze or expose the DB outside of OH.

Items:

// Persistence Groups
Group gMyOpenhab // for Items that are exposed to IFTTT
Group gChart     // for Items to persist for charting
Group gHistory   // for Items to preserve their history
//Group gRestore // for Items to restore on startup (currently everything so commented out)

...
// Example Item that I Chart
Number          Weather_Temp_Min        "Minimum Outside Temp [%.0f] °F"        <temperature> (gChart, gWeather_Temp_Chart)

// Example Item that I use historical data
Contact       N_D_Front                        "Front Door [MAP(en.map):%s]"             <frontdoor>  (gHistory, gDoorSensors, gRemindDoorSensors, gAlarmSensors, gHydraSensors)    { mqtt="<[mosquitto:entry_sensors/main/front_door:state:default]" }

// Example Item that I expose to my.openhab NOTE: it is also a member of gHistory
Switch  T_D_Garage1     "Garage Door 1"         <garagedoor> (gHistory, gMyOpenhab, gGarageOpeners)

*mapdb.persist file: I only use MapDB for restoreOnStartup and restoreOnStartup applies to all Items

Strategies {
        default = everyUpdate
}

Items {
        // persist all items on every change and restore them from the db at startup
        * : strategy = everyChange, restoreOnStartup
}

NOTE: I don’t know if setting a default strategy is required, I included it anyway, but it is basically meaningless because it is never used in the Items section.

To make restoreOnStartup work we need to persist every change. The ‘*’ resolves to all Items.

rrd4j.persist file: I use rrd4j for both charting and historic data. Because of limitations of rrd4j data must be saved at least every minute for charting or else you end up with blank charts. I’ve also found getHistoricState and previousState do not work without saving every minute as well.

Strategies {
        // for rrd charts, we need a cron strategy
        everyMinute : "0 * * * * ?"

        default = everyChange
}

Items {
        // additionally persist weather info every minute
        gHistory* : strategy = everyUpdate, everyMinute
        gChart*   : strategy = everyUpdate, everyMinute
}

myopenhab.persist:

Strategies {
        default = everyUpdate
}

Items {
        gMyOpenhab* : strategy = everyUpdate
}

Advantages

The advantages of this approach include:

  • The persistence behavior for each Item is documented with the Item (i.e. the group membership) keeping as much information about the behavior of the Item in one place
  • Adding new Items or changing how they are persisted simply involves changing that Item’s group membership.
  • Once set up, the .persist files need never be changed unless you decide move from one persistence engine to another
  • Uses the best persist engine for the desired behavior. For example MapDB can save all types of Items but only keeps the last value do it is great for restoreOnStartup but not great for historic data whereas rrd4j is great for charting recent data but can’t save anything that isn’t a numerical value so isn’t so great for restoreOnStartup
12 Likes