Using the JSR223 bundle in openHAB, you can write rules in various languages. Some examples include Jython (Python implemented in Java), Groovy and Javascript (Nashorn in Java 8). In this post, I’m going to focus on Jython. I’ll try to keep this post reasonably short and high level, but we can follow up with discussions of specific topics.
You may be wondering about the benefits of using Jython for writing rules. I believe there are several advantages over XText. First, if you know Python already you will not need to learn another language. Even if you haven’t used Python, it is relatively easy to learn and you will benefit from the extensive documentation and information about the language and standard libraries available on the net. You’ll also be learning a language that can be used in many other contexts. Jython has a few quirks (discussed later), but it is a quite capable implementation of the Python language and standard libraries.
Another advantage is that generic, parameterized rules can be created. The rules can be written so they are not hard-coded to use specific items or groups and rule behavior can be specialized through constructor arguments. This means that the community can now write rules that can represent generic home automation algorithms that can be easily reused in other openHAB installations. Other advantages of Jython rules include support for dynamic generation of triggers, rule inheritance, computed item names and more.
If you just want to experiment with Jython, XText rules and Jython rules can be used together so you don’t have use only one or the other language.
Getting Started
Information about installing the JSR223 addon and Jython support can be found on the openHAB wiki.
It’s important to note that Jython rules go into the scripts directory, not the rules directory. The script should have a “.py” suffix so that the script engine recognizes it as a Jython script. During installation, I recommend turning on debug logging and watching the logs carefully to be sure the Jython scripting engine is being found by openHAB and your rules are being loaded correctly. Instructions related to debug logging are on the wiki page.
Rule Structure
Jython rules have the following structure. Global symbols like “Rule” are already imported into the namespace by openHAB so you don’t need Python imports for these. See the wiki page for more details about the available names. Other names can also be included with standard Python import expressions.
class ExampleRule1(Rule):
def getEventTrigger(self):
return [
# triggers ...
]
def execute(self, event):
“rule body"
The script file should include a top-level (not class-level) function called getRules that returns a RuleSet. If a rule instance is not returned by getRules then the rule will not execute.
def getRules():
return RuleSet([ExampleRule1()])
Note the empty parentheses after ExampleRule, this is creating an instance of the ExampleRule1 class. If you leave out the parentheses, it will return the ExampleRule class itself and the rule won’t execute.
The getRules function can be as complex and dynamic as you’d like. For example, it could create instances of rules based on group members that could be different every time the script is loaded.
Simple Rule Example
The following simple rule triggers from an item change event and sends a command to a device. Again, names like ChangedEventTrigger, ItemRegistry and oh (Openhab) are pre-imported into the namespace by openHAB, as described on the wiki page.
class ExampleRule2(Rule):
def getEventTrigger(self):
return [
ChangedEventTrigger("MyDeviceContact")
]
def execute(self, event):
item = ItemRegistry.getItem("MyOtherDevice")
oh.sendCommand(item, event.getItem().getState())
Note that the event provided to the execute method has a reference to the item related to the event and the old and new states (or command). This could be useful if you have a trigger like…
def getEventTrigger(self):
return [
ChangedEventTrigger("MyDeviceContact”),
ChangedEventTrigger(“MyDeviceContact2”),
ChangedEventTrigger(“MyDeviceContact3”),
]
You don’t need to poll the contacts to determine which one triggered the rule since it will be referenced in the event passed to the execute function.
Dynamically-generated Rule Triggers
The triggers can even be generated dynamically from a group item.
def getEventTrigger(self):
return [
ChangedEventTrigger(item.getName()) for item in ItemRegistry.getItem(“MyGroup”).getMembers())
]
This is using Python list comprehension but you could build the list with a for loop instead.
Generic Rules
In the examples above, I used hard-coded item and group names. However, this is not necessary. I could have defined a generic rule like the following.
class ExampleRule3(Rule):
def __init__(self, contact_name, device_name):
self._contact_name = contact_name
self._device_name = device_name
def getEventTrigger(self):
return [
ChangedEventTrigger(self._contact_name)
]
def execute(self, event):
item = ItemRegistry.getItem(self._device_name)
oh.logInfo(“ExampleRule3”, “Sending command to {}”, item.getName())
oh.sendCommand(item, event.getItem().getState())
To enable the rule, I’d create rule instances to be returned by the getRules function in the script.
def getRules():
return RuleSet([
ExampleRule3(“MyContact1”, “MyDevice1”),
ExampleRule3(“MyContact2”, “MyDevice2”),
])
This is a simple example, but imagine if we defined sets of rules for complex behaviors that could easily be shared within the community. For example, different algorithms for doing motion sensor-based lighting controlled could be written and refined by the community and reuse would be as easy as instantiating the rules with your specific item and group names.
Dynamically-generated Item Names
Some rules can be simplified by defined an item naming convention. Let’s say we wanted to use a naming convention for a rule like ExampleRule3 where we provide a prefix and the items used in the rules are named Contact and Switch (where is whatever prefix we specified).
class ExampleRule4(Rule):
def __init__(self, prefix):
self._contact_name = prefix + “Contact"
self._device_name = prefix + “Device"
# . . . rest of rule definition
def getRules():
return RuleSet([
ExampleRule4(“Example1”), # Example1Contact, Example1Device
ExampleRule4(“Example2”), # Example2Contact, Example2Device
])
Rule Base Classes
Frequently used rule functionality can be reused by defining a rule base class. For example, we can define the logging category in the base class and automatically generate the category name from the most-derived rule subclass. The log category name doesn’t need to be repeated in each logging statement in the derived rule.
class OpenhabRule(Rule):
def __init__(self):
self.log = oh.getLogger(type(self).__name__) # log category is determined by the rule type
class ExampleRule5(OpenhabRule):
def __init__(self):
# invoke the base class constructor
super(ExampleRule5, self).__init__()
def getEventTrigger(self):
# logging category will be “ExampleRule5"
self.log.info(“Triggers requested”)
return [ StartupTrigger() ]
def execute(self, event):
self.log.info(“Rule executed”)
Once again, this is a very simple example, but other functionality like shared functions, algorithms and data could be placed into a rule base class. This is another opportunity for sharing within the community.
Jython Quirks
Jython is a decent implementation of Python and is very well integrated with Java. For more information on using Java functionality from Jython see…
http://www.jython.org/jythonbook/en/1.0/JythonAndJavaIntegration.html
One potential problem area is that Python imports don’t always behave as you’d expect in a Java context. Specifically, import expressions of the form “from package import *” may not work correctly. The easiest solution is to explicitly specify any imported names. In most cases, you won’t need to import anything because of openHAB has already done it before loading the script. For more information on this topic, see…
http://www.jython.org/jythonbook/en/1.0/ModulesPackages.html
Most pure Python libraries should work with Jython, but libraries with C-language dependencies will typically not work. The full installation of Jython includes a version of the pip package manager. Be sure to use that one to install Jython packages. If you install the package with the CPython pip, Jython won’t be able to find the package.
Conclusion
This post only touches the surface of what can be done with Jython-based rules. I’m hoping it will inspire others to try Jython or one of the other JSR223-supported languages to create rules and related functionality that can be shared throughout the openHAB community.