Please Design Pattern: What is a Design Pattern and How Do I Use Them for more information on what a DP is and how to use them.
Problem Statement
Often one will have certain cross cutting code that is used across .rules files by a diverse and varied collection of Rules. Such things can include things like calculating time of day to control the behavior of a rule, sending alerts, calculating state to drive a state machine, etc. For example, Lighting, blind/rollershutter controls and HVAC may all care whether it is cloudy or not.
Sometimes one can implement this sort of thing in a lambda but lambdas can only be called from Rules in the same .rules file as the lambda, forcing you to put everything in one file or duplicating your lambdas across multiple .rules files, violating DRY (Don’t Repeat Yourself). Another alternative is to use Scripts but one cannot pass arguments to Scripts which limits their usefulness to this use case.
Concept
Set up a Design Pattern: Proxy Item and send commands to that Item to update its state and kick off a Rule to implement the cross cutting logic. The value sent to the Item represents the argument(s) for the cross cutting logic to operate on.
A String Item makes the best choice for the Proxy Item as it is able to represent a lot of different types of data and it is easily parsable into multiple arguments if needed.
There are two sides to this design pattern. One side implements something akin to a function call that a Rule initiates by sending a command to the Proxy Item. This would be useful for cases like alerting where the Rules have a message they send to the Proxy to be dealt with. The first simple example below is this sort of implementation.
The second is to have a rule that triggers on its own which populates the Proxy Item with the result of a calculation. The second example below which determines if it is cloudy is this sort of implementation. In this case the Rules check the Proxy Item to determine what the current state is and adjust its behavior based on the current state.
Simple Example 1: Alerting
This is an alerting example. It allows Rules to distinguish between different types of alerts and do something different based on the alert type and the time of day. Some ancillary Items are not shown below.
Items
String Notification_Proxy_Info
String Notification_Proxy_Alarm
JSR 223 Jython with Helper Library
from core.rules import rule
from core.triggers import when
from core.actions import NotificationAction, Mail
from configuration import admin_email, alert_email
@rule("Publish alerts and info", description="Centralizes alerting logic.", tags=["admin"])
@when("Item aAlert received command")
@when("Item aInfo received command")
def send_alert(event):
if event.itemName == "aAlert":
send_alert.log.warn(event.itemCommand)
else:
send_alert.log.info(event.itemCommand)
night = True if items.vTimeOfDay == "NIGHT" or items.vTimeOfDay == "BED" else False
broadcast = True if event.itemName == "aAlert" and night else False
if broadcast:
NotificationAction.sendBroadcastNotification(event.itemCommand)
Mail.sendMail(alert_email, "", event.itemCommand)
else:
NotificationAction.sendNotification(admin_email, str(event.itemCommand))
Mail.sendMail(admin_email, "openHAB Info", str(event.itemCommand))
Rules DSL
rule "Send message"
when
Item aAlert received command or
Item aInfo received command
then
if(triggeringItem.name == "aAlert") logWarn(logName, receivedCommand.toString)
else logInfo(logName, receivedCommand.toString)
val night = if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") true else false
var broadcast = if(triggeringItem.name == "aAlert" && !night) true else false
if(broadcast) {
sendBroadcastNotification(receivedCommand.toString)
sendMail("5555555555@sms.com", "", receivedCommand.toString) // fake SMS email address
}
else {
sendNotification("myemail@email.com", receivedCommand.toString) // fake email
sendMail("myemail@email.com", "openHAB Message", receivedCommand.toString) // fake email
}
end
Theory of Operation
In the above, alerts are logged to openhab.log as warnings, info are logged as info. Then we determine whether we should broadcast the alert or only inform the administrator (me). We only broadcast if it’s an alert and it isn’t night.
In the Jython version, we store the email addresses in configuration.py which is where specific and sensitive information should go. This lets you check in and share the code without worrying about redacting that information.
JSR223 Python Module
The need for this design pattern is less pronounced for JSR223 Rules. An equally valid solution many times is to create a library function that each Rule directly calls. The main purpose of this DP is to deal with the fact that Rules DSL does not provide such a mechanism.
In particular, use a module if the actions taken by the function is different every time is called. If the function performs a calculation that can be reused (see below), this DP still applies.
See the Helper Library Docs for details on creating a module.
util.py
from core.jsr223 import scope
from core.log import logging, LOG_PREFIX
from core.actions import NotificationAction, Mail
from configuration import admin_email, alert_email
def send_info(message):
out = str(message)
logging.getLogger("{}.alert".format(LOG_PREFIX)).info(out.format(5 + 5))
NotificationAction.sendNotification(admin_email, out)
Mail.sendMail(admin_email, "openHAB Info", out)
def send_alert(message):
out = str(message)
night = True if scope.items.vTimeOfDay == "NIGHT" or scope.items.vTimeOfDay == "BED" else False
if not night:
logging.getLogger("{}.alert".format(LOG_PREFIX)).warning(out.format(5 + 5))
NotificationAction.sendBroadcastNotification(out)
Mail.sendMail(alert_email, "", out)
else:
send_info(message)
Example of use:
from core.rules import rule
from core.triggers import when
import personal.util
reload(personal.util)
from personal.util import send_info, send_alert
@rule("Publish alerts and info", description="Centralizes alerting logic.", tags=["admin"])
@when("Item aAlert received command")
@when("Item aInfo received command")
def send_alert_rule(event):
if event.itemName == "aAlert":
send_alert(event.itemCommand)
else:
send_info(event.itemCommand)
Theory of Operation
In JSR223 we have access to all the constructs of a “normal” language. This means we can put instantaneous code like the above into a personal library (stored in $OH_CONF/automation/lib/python/personal
) and import and call them from your Rules. In this case we create two functions, one for alerts and another for info. Notice how we gain access to Items and other core OH stuff using the imported scope.
Simple Example 2: Is it Cloudy?
This example checks the current weather conditions to determine whether it is cloudy or not. See Comprehensive Wunderground using HTTP Binding Example for how the Items below get populated and the contents of the weather.map which is used to map the current conditions to a cloudiness state.
Item
Switch vIsCloudy "Conditions are Cloudy [%s]" <rain>
Rules DSL
rule "Is it cloudy outside?"
when
Item vWeather_Conditions changed
then
logDebug(logName, "New weather conditions: " + vWeather_Conditions.state.toString)
val isCloudy = transform("MAP", "weather.map", vWeather_Conditions.state.toString)
val newState = if(isCloudy === null || isCloudy == "false") OFF else ON
if(newState != vIsCloudy.state) logInfo(logName, "Setting isCloudy to " + newState.toString)
vIsCloudy.postUpdate(newState)
end
rule "Some random rule that cares about cloudy"
when
Item vIsCloudy changed
then
if(vIsCloudy.state == ON) {
// do stuff now that it is cloudy
}
else {
// do stuff now that it isn't cloudy
}
end
NOTE: Since Wunderground shut down their API, here is an example of the Rule using OpenWeatherMap, which is actually much simpler. OpenWeatherMap provides cloudiness as a percent so there is no need to map the conditions to a cloudiness.
rule "Is it cloudy outside?"
when
Item vCloudiness changed // OpenWeatherMap cloudiness Item
then
val newState = if(vCloudiness.state > 50) ON else OFF
if(newState != vIsCloudy.state) vIsCloudyState.postUpdate(newState)
end
Theory of Operation
When the current weather conditions changes, use a map file which returns true if the contidion indicates it is cloudy (snow, rain, partly cloudy, etc) and false otherwise. Then, if the current cloudy conditions are different from vIsCloudy, update vIsCloudy.
Any rule that needs to do something different if it is cloudy just need to check vIsCloudy. Any Rule that needs to do something in response to it becoming cloudy can trigger off of vIsCloudy.
JSR223 Python using the Helper Library
from core.rules import rule
from core.triggers import when
import personal.util
reload(personal.util)
from personal.util import command_if_different
@rule("Is Cloudy", description="Generates an event when it's cloudy or not", tags=["weather"])
@when("Item vCloudiness changed")
def is_cloudy(event):
is_cloudy.log.info(str(items["vCloudiness"]).replace(" %", ""))
newState = "ON" if float(str(items["vCloudiness"]).replace(" %", "")) > 50.0 else "OFF"
is_cloudy.log.info("new state is " + newState)
command_if_different("vIsCloudy", newState)
Theory of Operation
The above Python version of the Rule does the same as the OpenWeatherMap version of the Rules DSL version. Because we can create libraries of useful functions in JSR223, I’ve created a little function that only sends a command (there is an update version too) if the new state is different from the current state of the Item.
As with the above versions, this Rule sends a command to vIsCloudy when the cloudiness changes. Rules that need to check if it’s cloudy need only check vIsCloudy. Rules that need to trigger when it becomes cloudy can trigger off of vIsCloudy. Even though JSR223 Rules have the ability to create functions, this is a good use of this Design Pattern.
Advantages and Limitations
The major advantage this DP provides is centralizing of otherwise cross cutting logic. As a result the cross cutting logic can be more complex yet easier to maintain. If, for example, I decided to use a different notification service for my alerts I would only have to change it in one place because I used this DP.
It also has the advantage that it can more efficiently calculate certain states. For example, if we had put the Is Cloudy example above into a function (if using JSR223 Rules), every time that any Rule needs to determine if it is cloudy that Rule will need to redo the same calculation over and over again and come up with the same answer every time.
It can greatly reduce the number of times certain rules trigger as well. For example, again using the Is Cloudy example, if we have a few Rules that need to do something in response to the cloudiness changing, all of those Rules would have to trigger based on vCloudiness which updates every few minutes even though all the Rule cares about is if the value falls above or below a certain threshold. This DP allows us to centralize these triggers into one simpler Rule and all the rest only trigger when they need to.
The major disadvantage is this DP requires at least one extra unbound Item to store the state.
Related Design Patterns
Design Pattern | How It’s Used |
---|---|
Design Pattern: Unbound Item (aka Virtual Item) | Notification_Proxy_Info and Notification_Proxy_Alarm are both examples of Virtual Items. |
Design Pattern: Proxy Item | Notification_Proxy_Info and Notification_Proxy_Alarm are both examples of Proxy Items |
Comprehensive Wunderground using HTTP Binding Example | Not a DP but the cloudy rule depends on this code |
Design Pattern: Gate Keeper | Gate Keeper is a specific implementation of this DB |
Design Pattern: Time Of Day | Time of Day is a specific implementation of this DP |
Design Pattern: Human Readable Names in Messages | Used to map the current conditions to true/false depending on whether the conditions indicated that it is cloudy or not |
Design Pattern: Sensor Aggregation | ois a specific implementation of this DP |