Automation/Orchestration Design Patterns

As a guy who’s written code for a living for 35+ years, I appreciate the “mother of all rules” post on the wiki and the discussions here which use groups for reduce lines of code.

But I’ve not seen too much on doing systematic coordination/orchestration of automation events and design patterns of logic for same. So, I offer the following.

This starts with my particular use case involving a Light in my kitchen. There’s a switch in the room to control the light, as well as a motion sensor which can trigger the light. The light can also be triggered when the front door opens after dark (using Astro binding). (Kitchen lies just off stairwell from front door, so the light is turned on for a “welcome home”/safety purpose.)

So I’m cheap, and I want the light to go out quickly if not needed. I put timers in for the front door opens rule to automatically turn out the light. Especially needed when someone goes OUT after dark to make sure the light goes off since there are no other events available to trigger the OFF.

So light should go OFF if (a) manually switched, (b) motion detector goes CLOSED , or © a previously set timer expires and turns it off.

The main failure scenario was when someone was already in the kitchen, having tripped the motion detector and thus turning ON the light, then someone arrived home after dark, opened the front door and thus setup an automatic OFF timer. So in a few minutes, the light in the kitchen would go off even though the room was in use.

In essence, I needed to intercept the timer-triggered OFF command at execution time and invalidate it under some condition (namely the motion detector being OPEN).

I did this by creating a second item as a PROXY for the light switch and directing all sendCommands to the PROXY where they could be intercepted.

My solution looks like this (with logging removed for brevity).

(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.)

First the PROXY intercept rules themselves (I intercept ON for symmetry):

rule "PXY_1: sw_LIGHT_Kitchen_PROXY changed to ON"
    when
            Item sw_LIGHT_Kitchen_PROXY changed to ON
    then
             var new_state=ON
             //straight passthru for now
             sendCommand(sw_LIGHT_Kitchen,new_state)
end

rule "PXY_2 : when mo_MOTION_Kitchen has not reset to CLOSED, intercept the OFF command"
    when
        Item sw_LIGHT_Kitchen_PROXY changed to OFF
    then
        var new_state=OFF        
        if (mo_MOTION_Kitchen.state == CLOSED){
            new_state=OFF
        } else {
            if (tim_L_K_timerOFF != null) {
                new_state=ON
                tim_L_K_timerOFF.cancel()
                tim_L_K_timerOFF=null
                postUpdate(sw_LIGHT_Kitchen_PROXY,ON)
                tim_L_K_timerOFF=createTimer(now.plusSeconds(10)) [|
                    sendCommand(sw_LIGHT_Kitchen_PROXY, OFF)        
            } else {
                new_state=OFF
                postUpdate(sw_LIGHT_Kitchen_PROXY,OFF)
            }
        }
        sendCommand(sw_LIGHT_Kitchen,new_state)        
end

PXY_2 intercepts the OFF command and only passes it thru to control the real light IF the motion detector is closed. Otherwise it requeues a timer on a tight loop.

The front door rule looks like this:

rule "FDC: front door CLOSES in KITCHEN.rules file"
    when
        Item co_CONTACT_SENSOR changed from OPEN to CLOSED
    then    
        if (LOGICAL_NIGHT == 1 ) {
            if (tim_L_K_timerON != null) {
                tim_L_K_timerON.cancel()
                tim_L_K_timerON=null            
            }
            tim_L_K_timerON=createTimer(now.plusSeconds(3)) [|
                sendCommand(sw_LIGHT_Kitchen_PROXY, ON) ]
            // this is really only to handle making sure light gets turned off when people LEAVE
            //if entering people go into the kitchen it will trip the motion sensor in rule MO_1
            //that will cause this timer to be canceled
            tim_L_K_timerOFF=createTimer(now.plusSeconds(120)) [|
                    sendCommand(sw_LIGHT_Kitchen_PROXY, OFF)    ]    
        }    
end

The motion sensor rules look like this:

rule "MO_1: Events when motion in room trips"
    when
        Item mo_MOTION_Kitchen changed to OPEN
    then                   
        if (sw_LIGHT_Kitchen.state == OFF) {
        //if light was off, cancel any extant timers and turn it on due due to motion
            if (tim_L_K_motionON != null) {
                logDebug("KITCHEN","MO_1 : extant KITCHEN motion ON timer being checked/created..")
                tim_L_K_motionON.cancel()                        
                tim_L_K_motionON=null
                }
            tim_L_K_motionON=createTimer(now.plusSeconds(1)) [|
                sendCommand(sw_LIGHT_Kitchen_PROXY,ON) ]                       
            } 
        //make sure to kill any extant OFF timer    
        if (tim_L_K_timerOFF != null) {
                tim_L_K_timerOFF.cancel()
                tim_L_K_timerOFF=null
        }                       
end

rule "MO_2 : events when mo_MOTION_Kitchen changed from OPEN to CLOSED"
    when
           Item mo_MOTION_Kitchen changed from OPEN to CLOSED or
           Item mo_MOTION_Kitchen changed from Uninitialized to CLOSED
    then    
        if (tim_L_K_timerOFF != null) {
                tim_L_K_timerOFF.cancel()
                tim_L_K_timerOFF=null
            }
        if (sw_LIGHT_Kitchen.state == ON) {
            tim_L_K_timerOFF=createTimer(now.plusSeconds(10)) [|
                    sendCommand(sw_LIGHT_Kitchen_PROXY, OFF)    ]
        } else { 
        //LIGHT IS OFF when motion sensor resets -- likely due to manual operation or initialization so clean up any extant OFF timer
        //NO-OP        
        }
end

Problem solved.

11 Likes

Your example is a good case for showing how quickly simple requirements can end up in a lot of logic indeed.
In order to more formally capture such design patterns, we are working on introducing “rule templates” in Eclipse SmartHome (and eventually openHAB 2). This should allow to easily share your set of rules with others, which would only need to configure it for their items and needs. The idea is a bit like the recipes of IFTTT. I am really looking forward to have something like this in place :slight_smile:

5 Likes

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

Wouldn’t it just be easier to check if the light was already on (or off in this example)?

if (kitchenLight.state = OFF) { set all your door opening stuff here }

Obviously if the kitchen light is already on when you open the door then there is A) no reason to set the light in the first place, and B) if the kitchen light is already on then the kitchen is in use meaning that a timer should never be set to turn it off. Your solution seems overly complicated, unless I’ve missed something.

Also, just something I noticed while glancing through the code:

var new_state=OFF        
        if (mo_MOTION_Kitchen.state == CLOSED){
            new_state=OFF
        } else {

If your default for new_state is OFF, then there’s no reason to set it back to OFF again in that if statement. Who cares if the motion sensor isn’t picking anything up, you’ve already set the state to OFF in the first place.

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