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.