Tutorial: Beyond XText, supercharged rules using Jython (openHAB 1)

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.

13 Likes

I haven’t had a chance to read this all the way through yet but I’d like to say thank you!

My only experience with Python has been simple changes to other people’s scripts so I was a bit hesitant to dive into the new script engine so this will be a big help personally.

A lot of great tips, thanks much!
So glad I don’t have to use xtend anymore. Jython is a mile better.

1 Like

Thank you so very much for giving me a reason to brush the dust off my rules and converting them to a language I feel more comfortable with :smile:

Thanks for the writeup! I started another thread on this forum asking people about best practices they have figured out because I found the XTend system cumbersome. I love that a lot of the benefits you mention address a number of the things that I wished I could do in rules. I’ve been meaning to try the jsr223 stuff, and it looks like your post will be helpful to have available when I take a stab at it!

Quick question, is JSR223 just for OH1 or does it work with OH2?

As far as I know, the JSR223 bundle has not been ported to OH2 yet.

Ah too bad. Then again, this might explain why my first experiments with OH2 latest snapshot were not quite successful :smiling_imp:

Explains why I couldn’t get it working then. with my OH2 setup.

@steve1, @Spaceman_Spiff,

It would be supremely awesome if this tutorial were updated to reflect the many apparent changes that have been made since 2015 and given that it now works in OH 2…

For instance, execute appears to now take a module and input as arguments instead of event. I’ve tried to figure out what these are and how to get at the event, Item and state but have thus far failed to guess the right way.

All of the examples I’ve found thus far use execute(self, event) but I get an error when I try to use that construct.

Or maybe just a quick way or place to look (source code?) to discover what information is available in module and input and how to access it? For example, is there an event object buried somewhere inside input that I can use with some of the older rule examples I’ve found?

I’ve spent several hours looking through the forum and various github projects including both of yours but either have not found anything that helps or do not have the Python experience to interpret what I’m seeing.

If I’m having this much trouble I imagine new comers will have even more trouble.

I agree it would be good to update the tutorial, maybe after the OH2 scripting support is released in a stable build.

Those would all be openHAB 1 examples. Like you said, the OH 2 execute method is given a module and an input. I haven’t much use for the module argument. The inputs vary depending on the trigger.

The input argument is a Python dictionary. For state update and state change events, there is an “event” key with details about the triggering event (item name, states, etc.).

Have you looked at the docs on the docs.openhab.org site?

http://docs.openhab.org/configuration/jsr223.html

I think it would be good update the docs to include the contents of the inputs dictionary for the built-in trigger types.

Also, if you are looking at my code on GitHub, be sure to look the openhab2-jython project and not the openHAB 1 code.

(I edited the title of this tutorial to make it explicit that it’s for openHAB 1.)

Thanks! Knowing it is a dict I think will help me figure it how or if I want to use it. I’m just noodling around with Jython right now, exploring the possible. Then I plan on updating some (the ones that make sense) of the design patterns with Jython and perhaps JavaScript versions.

I have seen your library, linked to from the docs, and plan on using it once I get a better feel for how it works without your library. I’m a novice at Python at best and when you put too many decorators and Python magic in place I get a little lost.