Edit: Updated for OH 4.
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 a many diverse and varied collection of Rules. Such code 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.
There are three difference concepts that can be applied in this design pattern.
- Using an Item to trigger a rule or store the result of a calculation
- Using a library
- Calling other rules
Concept 1: Item
Set up a Design Pattern: Proxy Item and send commands to that Item to 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 a good choice for the proxy Item as it is able to represent a lot of different types of data and it can be relatively easily parsable into multiple arguments if needed.
There are two slightly different ways to implement this approach.
Command Item
This way simulates something like a function call. The âfunctionâ is a rule triggered when the proxy Item receives a command and to âcallâ that âfunctionâ you send a command to the proxy Item. Any data needed by the triggered rule gets encoded in the command sent to the proxy Item.
The advantage of this approach is the âcallsâ to the rule will be queued so one does not have multiple other rules trying to execute the code at the same time which can be a problem with other approaches (e.g. the Script and Scenes examples).
This approach is best when there is not much that needs to be sent to the rule to operate, such as an alerting rule that just needs to know the message to send in an alert but may do something different based on the time of day.
Items:
String Notification_Proxy_Info
String Notification_Proxy_Alarm
To send an alert command either of these two Items with the message to send as part of the alert.
Both of these Items are used to trigger the rule on received command.
Blockly
If the alert is to the info Item and itâs DAY time send the alert as an email. If itâs the alert Item, send the alert as a notification any time of day. In either case, log the message also.
JS Scripting
if(event.itemName.endsWith('Info') && items.TimeOfDay.state == 'DAY') {
actions.Things.getActions('mail', 'mail:smtp:gmail').sendMail('me@domain.tld', 'openHAB Alert', event.itemCommand);
}
else if(event.itemName.endsWith('Alert')) {
actions.NotificationAction.sendBroadcastNotification(event.itemCommand, 'alarm', 'important');
}
console.info(event.itemCommand);
Rules DSL
if(triggeringItemName.endsWith('Info')) {
getActions('mail', 'main:smtp:gmail').sendMain('me@domain.tld', 'openHAB Alert', receivedCommand.toString)
}
else if(triggeringItemName.endsWith('Alert')) {
sendBroadcastNotification(receivedCommand.toString, 'alarm', 'important')
}
logInfo('Alerting', receivedCommand.toString)
Jython with Helper Library (expect changes in the future)
I donât know how well Jython works in UI rules so Iâm showing a file based rule here.
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 Notification_Proxy_Info received command")
@when("Item Notification_Proxy_Alert received command")
def send_alert(event):
if event.itemName == "Notification_Proxy_Info":
Mail.sendMail("me@domain.tld", "openHAB Alert", event.itemCommand)
else:
NotificationAction.sendBroadcastNotification(event.itemCommand, 'alarm', 'important')
night = True if items.vTimeOfDay == "NIGHT" or items.vTimeOfDay == "BED" else False
broadcast = True if event.itemName == "aAlert" and night else False
if broadcast:
Mail.sendMail(alert_email, "", event.itemCommand)
else:
NotificationAction.sendNotification(admin_email, str(event.itemCommand))
Mail.sendMail(admin_email, "openHAB Info", str(event.itemCommand))
send_alert.log.info(event.itemCommand)
Store Result
The second way to use an Item in this design pattern is to calculate some state (e.g. time of day, how cloudy it is, a complicated boolean expression, etc.) and store the result of that calculation in an Item. Other rules just need to check the Item instead of needing to redo the calculation in multiple places.
For example, lets create a rule to keep track of whether itâs cloudy or not. We use a Cloudiness Item (a percent) and will store the result of the is cloudy calculation in a Switch Item. The rule triggers when the cloudiness value changes.
Rules that need to do something based on the cloudiness level simple need to use the IsCloudy Itemâs state.
Blockly
JS Scripting
console.debug('New cloudiness percent: ' + event.itemState);
var newCloudiness = (Quantity(event.itemState).greaterThan(Quantity('45 %'))) ? 'ON' : 'OFF';
if(newCloudiness != items.IsCloudy.state) console.info('Setting isCloudy to ' + newCloudiness);
items.IsCloudy.postUpdate(newCloudiness);
Rules DSL
rule "Is it cloudy outside?"
when
Item Cloudiness changed
then
logDebug(logName, "New cloudiness percent: " + newState)
val newCloudiness = if(newState > | 45 %) ON else OFF
if(newCloudiness != IsCloudy.state) logInfo(logName, "Setting isCloudy to " + newCloudiness.toString)
IsCloudy.postUpdate(newCloudiness)
end
### Jython using the Helper Library
```python
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(" %", "")) > 45.0 else "OFF"
is_cloudy.log.info("new state is " + newState)
command_if_different("vIsCloudy", newState)
Concept 2: Libraries
Most of the rules languages support libraries. Rules DSL is the primary notable exception.
A library is a collection of code: functions, classes, data structures, etc, that can be imported into another library of a rule. For example, a library can be used to replace a block of code that would otherwise be repeated across multiple rules with a simple import and function call.
For the example here we will use the same alerting example from above.
An advantage of this approach is that the library code actually gets imported into the rule where itâs running so there is no chance of undesirable interactions between rules. However, a library (except for Block libraries) cannot be created through the UI and must be done through text files on the host.
Blockly
Unfortunately Blockly libraries are not easy to create. There are two choices: create a block library, create a JS node module and use the inline script block to import and call the library functions.
Block Library
Explaining how to create a block library is beyond the scope of this tutorial. See Tutorial: How To Write Block Libraries for details.
At a high level youâll use Developer Tools â Block Libraries to create your very own Blockly Block(s) which can be used in you rules like any other block. See Block Libraries - openHAB Community for a number of published examples.
JS Scripting
See JavaScript Scripting - Automation | openHAB for instructions on how to install third party libraries or how to create your own personal library. Assuming we put the alerting code from above into a personal library function the code would look something like this:
exports.sendAlert = function(message) {
actions.NotificationAction.sendBroadcastNotification(message, 'alarm', 'important');
console.info(message);
}
exports.sendInfo = function(message) {
actions.Things.getActions('mail', 'mail:smtp:gmail').sendMail('me@domain.tld', 'openHAB Alert', event.itemCommand);
console.info(message);
}
In a rule calling the library functions looks like (assuming the personal library is ârlk_personalâ and the functions are exported under âalertingâ):
var {alerting} = require('rlk_personal');
alerting.sendAlert('Attempting to trigger a garage door but the controller is not online!');
Jython Module
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)
Concept 3: Rules Calling Other Rules
For some time OH has supported the ability for rules to call another rule. In addition, OH 4 introduced the concept of âScenesâ in addition to the concept of âScriptsâ, both of which are a special type of rule but still callable as a rule.
Scenes let you list a bunch of Items and the states you want those Items to be commanded to.
A script is a rule consisting of only a single Script Action. See Rules | openHAB.
All of these are rules and all of them can be called from another rule in most languages with the exception of Rules DSL.
âArgumentsâ can be passed into the rule in many cases (a Rules DSL rule can be called from another rule, but it cannot access the passed in data so the sharedCache
would need to be used instead).
When a rule calls another âregularâ rule that has conditions, you can set a flag to control whether the condition is applied or not. For example, if a condition causes the rule not to run if Foo
is ON
, if you pass the flag to apply Conditions when the rule is called it wonât execute and immediately return while Foo
is ON
. If you pass false the called rule will run even if Foo
is ON
.
Simple UI Rules
Add an Action to the Rule and choose âScenes, Scripts, and Rulesâ for the Action Type.
Fill in something meaningful for the name and description. Keep ârunâ selected, select the rule to call and decide whether or not the rule conditions should be applied.
When this rule is triggered and itâs conditions allow the rule to run, it will call the selected rule.
There is no way to pass additional data to the called rule in this way.
Blockly
Use the ârun rule or script with contextâ block to call another rule. In the called rule, you can access the passed in context using the âget contextual attributeâ block.
Because Blockly converts to JS Scripting, note the limitations identified below for JS Scripting.
JS Scripting
There are some limitations with JS Scripting when rules call other rules. GraalVM JS which underlies the JS Scripting add-on does not allow multithreaded access to the context of any given script. The JS Scripting add-on implements a number of locks to prevent this from happening in the normal course of events (rule triggers, timers, etc.). However, there are no locks in place when a JS Scripting rule is called. Consequently, if two rules call the same rule at the same time, a âMulti-threaded accessâ exception will be thrown and the second call will fail.
Because of this, calling JS Scripting rules or Blockly rules should be the exception rather than the normal way to consider implementing separation of behaviors. If can be very useful in some cases but not all (e.g. it would be a terrible way to centralize your alerting logic). It is great for calling Scenes though as those execute very fast and are unlikely to be called from two different rules at the same time.
To call another rule from a JS Scripting rule, use rules.runRule(ruleUID, dict, conditions)
where
ruleUID
: is a string representation of the rule UID of the rule to calldict
: is a JavaScript dictionary of key/value pairs (e.g.{ "foo": "bar", "baz": "biz" }
)conditions
: whentrue
the called rule will only run itâs action scripts if the conditions are true
For an example from Thing Status Reporting [4.0.0.0;4.9.9.9]
var {helpers} = require('openhab_rules_tools');
console.loggerName = 'org.openhab.automation.rules_tools.Thing Status';
// osgi.getService('org.apache.karaf.log.core.LogService').setLevel(console.loggerName, 'DEBUG');
helpers.validateLibraries('4.1.0', '2.0.0');
console.debug('ThingStatusInfoChangedEvent:' + event.toString());
var parsed = JSON.parse(event.payload);
var data = {};
data['thingID'] = event.topic.split('/')[2];;
data['thing'] = things.getThing(data['thingID']);
data['oldStatus'] = parsed[1].status;
data['oldDetail'] = parsed[1].statusDetail;
data['newStatus'] = parsed[0].status;
data['newDetail'] = parsed[0].statusDetail;
rules.runRule("thing-status-proc", data, true);
The contents of data
get inserted into the called rule as variables. The thing-status-proc
rule I use looks like this:
console.debug('Thing ' + thingID + ' changed from ' + oldStatus + ' (' + oldDetail + ') to '
+ newStatus + '(' + newDetail + ')');
var itemName = cache.shared.get(ruleUID+'_thingToItem')[thingID];
var newState = (newStatus == 'ONLINE') ? 'ON' : 'OFF';
console.debug(cache.shared.get(ruleUID+'_thingToItem'));
console.debug('Thing status update: ' + itemName + ' ' + newState);
items[itemName].postUpdate(newState);
You can see the variables passed from the calling rule are accessed directly.
Advantages and Limitations
The major advantage this DP provides is centralizing of otherwise cross cutting logic, improving Donât Repeat Yourself (DRY) and making the long term maintenance of your rules easier. 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 one of the concepts in 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, 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. By centralizing the calculation, it only needs to be done when the cloudiness actually changes.
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.
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 |
[Deprecated] 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 |