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