This is the first of a series of posts where I will provide the before and after, commentary, and lessons learned I’ve made while converting my Rules DSL to JSR223 Jython.
Purpose
These will not be fully realized docs. You will have to reference https://openhab-scripters.github.io/openhab-helper-libraries/index.html repeatedly. Keep that page open in your browser as you code. These will also not be and are not intended to be the best way to implement the Rules in Jython. I’m learning as I go and cannot be considered an expert.
My hope is that these posts will show examples the the thought process behind migrating that I took. Also, it should be useful to see a Rules DSL Rule and a Jython Rule that does the same thing side by side.
My intent is to have one Rule per post, but for really simple Rules I’ll include more than one per post. I’ll have the posts linked to each other at the bottom of each post. Expect the quality of the Jython to improve as I go.
Overall Approach
This is going to be a learning process and, against better advice, I am going to be making the changes on my production system. Therefore I’m going to start small and start around the edges. I have a number of very simple Rules or Rules that are there mainly for monitoring. I’m unlikely to break things with the simple Rules and if the monitoring Rules stop working it’s no big deal.
Some New Concepts
configuration.py
See the docs for how to install and use the JSR223 Helper Libraries. Don’t forget to do step 8, rename configuration.py. This is our first new concept. JSR223 provides a way to have code and variables that are shared across all your Rules. configuration.py’s purpose is to provide values to your Rules that either cut across all your Rules, or are values that you do not want to share (e.g. usernames and passwords). By putting those into configuration.py you can share your code without needing to redact them.
$OH_CONF/automation/lib/python/personal
This is one folder down from where you will find configuration.py. Here is where you will place your personally developed libraries. Helper methods, abstract classes, and other stuff like that will go here. There is an important distinction between the code placed here and the code placed in $OH_CONF/automation/JSR223/python/personal. Code placed here is statically compiled which means you need to force Jython to reload them (either through a restart of OH or an import directive). Simply saving the file will not cause your changed to be reloaded.
Take note, unlike with Rules DSL, functions and classes are very well supported in Jython.
First Rule Translation, Alerting
We are starting small so let’s take on my alerting Rule. This is an implementation of Design Pattern: Separation of Behaviors (note this DP has been updated with the JSR223 code) and how I centralize my alerting code.
val logName = "alert"
rule "Send message"
when
Item aAlert received command or
Item aInfo received command
then
logInfo(logName, receivedCommand.toString)
val night = if(vTimeOfDay.state.toString == "NIGHT" || vTimeOfDay.state.toString == "BED") true else false
var alert = false
if(triggeringItem.name == "aAlert" && !night) alert = true
if(alert) {
sendBroadcastNotification(receivedCommand.toString)
sendMail("<phone number>@vtext.com", "", receivedCommand.toString)
}
else {
sendNotification("<email>@email.com", receivedCommand.toString)
sendMail("<email>@email.com", "openHAB Message", receivedCommand.toString)
}
end
This is a great example of a Rule that would be better implemented as a library function rather than a Rule. That’s not possible in Rules DSL (and have access to it in all of your .rules files) but it is possible with Jython.
from core.jsr223 import scope
from core.actions import NotificationAction, Mail
from configuration import admin_email, alert_email
def send_info(message, logger):
out = str(message)
logger.info("[INFO ALERT] {}".format(message))
NotificationAction.sendNotification(admin_email, out)
Mail.sendMail(admin_email, "openHAB Info", out)
def send_alert(message, logger):
out = str(message)
night = True if scope.items.vTimeOfDay == "NIGHT" or scope.items.vTimeOfDay == "BED" else False
if not night:
logger.warning("[ALERT] {}".format(message))
NotificationAction.sendBroadcastNotification(out)
Mail.sendMail(alert_email, "", out)
else:
send_info(message)
Because this is in a library, we have to import the scope to get access to stuff like Items. We also need to import the logger. I’ve broken the Rule into two separate functions, one to send info alerts and another to send serious alerts.
Notice how I’ve replaced the email addresses for the sendMails with admin_email and alert_email, which are both defined in configuration.py.
We pass in the logger from the Rule to this function because it’s easier and it will tie the alerts to the Rule that generated it.
Using the library methods is as follows:
from core.rules import rule
from core.triggers import when
import personal.util
reload(personal.util)
from personal.util import send_info, send_alert
# -----------------------------------------------------------------------------
# todo: remove once Rules are all transitioned
@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, send_alert_rule.log)
else:
send_info(event.itemCommand, send_alert_rule.log)
Notice how I import personal.util (the functions above are in util.py. the reload(personal.util) causes Jython to recompile util.py to pick up any changes made there. The alternative is to restart openHAB.
I’ve kept my Separation of Behaviors Rule for now but will delete it once all my Rules are transitioned to JSR223.
Note that strange reload of personal.util. When you make changes to library files, those changes will not be picked up until OH restarts. That can be darned inconvenient. But there is a way you can cause the library to be reloaded when the file the above code is in get’s loaded with that reload command. This frees you from needing to restart every time you change the library. But once your changes have been loaded, remove that reload command, it’s unnecessary.
Is it Cloudy?
This Rule is another example of the Separation of Behaviors DP and it is an example of a Rule that it makes sense to keep as a Rule rather than a library function. This Rule triggers when OpenWeatherMap updates the cloudiness percentage and sets a Design Pattern: Unbound Item (aka Virtual Item) Switch.
rule "Is it cloudy outside?"
when
Item vCloudiness changed
then
val newState = if(vCloudiness.state > 50) ON else OFF
if(newState != vIsCloudy.state) vIsCloudy.sendCommand(newState)
end
One of the things to notice here is that we only command vIsCloudy if the new state is different from the current state. Thankfully there is a method in the library that does this for you.
The Rule becomes:
from core.rules import rule
from core.triggers import when
from core.util import sendCommandCheckFirst
@rule("Is Cloudy", description="Generates an event when it's cloudy or not", tags=["weather"])
@when("Item vCloudiness changed")
def is_cloudy(event):
newState = "ON" if items["vCloudiness"] > QuantityType(u"50.0 %") else "OFF"
sendCommandCheckFirst("vIsCloudy", newState)
Initial Lessons Learned
-
If you have clear and concise Rules DSL Rules, the Python equivalent is probably going to be the same or more lines of code.
-
One Rule per file feels like it may make more sense with Python Rules, but I’ll explore that later.
-
The syntax is different but the over all look and feel of the Rules are the same as Rules DSL.
Next post: Journey to JSR223 Python 2 of?
EDIT: Corrections and improvements based on recommendations from CrazyIvan359.
EDIT: Corrections and improvements based on recommendations from 5iver
EDIT: Corrected to use QuantityType/Units of Measure