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.