Simplified Jython rule definition (similar to Rules DSL) using a universal decorator

I’ve pushed a major update to the openhab-scripters repo, with a number of changes to the openhab.triggers module, openhab.rules module, and updated the documentation. The biggest changes are a new rule decorator and a single unified trigger decorator that will work for (almost) all of the triggers available in the Rules DSL, and the trigger definitions should look familiar…

from openhab.rules import rule
from openhab.triggers import when

@rule("This is the name of a test rule")
@when("Item Test_Switch_1 received command OFF")
@when("Item Test_Switch_2 received update ON")
@when("Item gMotion_Sensors changed")
@when("Member of gMotion_Sensors changed to OFF")
@when("Descendent of gContact_Sensors changed to ON")
@when("Thing kodi:kodi:familyroom changed")# Note: Thing statuses (from <status> to <status>) cannot currently be used in Thing triggers
@when("Channel astro:sun:local:eclipse#event triggered START")
@when("System started")# Note: `System started` and 'System shuts down' are currenlt broken by and API change!
@when("Time cron 55 55 5 * * ?")
def testFunction(event):
    # do stuff

There have been at least a few people testing the changes, but I’d like to get some feedback, especially on were the documentation could be streamlined. This is a significant improvement over the functionality that was previously available, and my hope is that this change will help to simplify the adoption of the new rule engine, at least for people choosing to use Jython. Please take a look, let me know what you think, and if there are any other features that would be helpful.

Things to keep in mind…

  • The Rules DSL and JSR223 can run simultaneously, so you can easily test or migrate on your production system
  • There are recent improvements and fixes in the Automation API, so it is recommended to be on a snapshot or milestone build. I do not intend to port these changes over to the 2.3 version of the modules (there’s a separate branch for them).
18 Likes

Hi Scott,

Are any of the group methods from this thread available in Jython? ie findFirst etc.

I believe most of those commands are specific to the way the Rules DSL processes collections. I think they borrowed heavily from Linq in C# and the like. They are also IMHO one of the few strengths of the Rules DSL.

I’m not a Python/Jython expert but I believe members in Jython get’s returned as either a dict or a list, or perhaps a tuple. This link should give you all the methods available to you. For findFirst I think you will have to iterate over the list until you find the one you are after. Using next seems to be the best way to do that, see python - Find first sequence item that matches a criterion - Stack Overflow

3 Likes

Great question! members and allMembers are methods of GroupItem. The rest are Xtend expressions, but Jython and Python have equivalent built-in functions that can be used instead. Here are some examples (I’ll add to readme):

log.debug("JSR223: test: members=[{}]".format(ir.getItem("gTest").members))
log.debug("JSR223: test: allMembers=[{}]".format(ir.getItem("gTest").allMembers))

#forEach - use Jython in Membership operator
for item in ir.getItem("gTest").members:
    log.debug("JSR223: test: iterate group=[{}]".format(item.name))

#filter - use Jython fiter() function
listOfMembers = filter(lambda item: item.state == OnOffType.ON, ir.getItem("gTest").members)# returns a list of items, not a group
log.debug("JSR223: test: filtered group=[{}]".format(listOfMembers))

#findFirst - use Jython filter() function again, but take first element
firstMember = filter(lambda item: item.state == OnOffType.ON, ir.getItem("gTest").members)[0]# returns an item
log.debug("JSR223: test: first member=[{}]".format(firstMember))

#sortBy - use Jython sorted() function
sortedBatteryLevel = sorted(battery for battery in ir.getItem("gBattery").getMembers() if battery.state &lt; DecimalType(33), key = lambda battery: battery.state)
log.debug("JSR223: test: sorted list=[{}]".format(sortedBatteryLevel))

#take - use Jython list slicing
slicedListOfMembers = filter(lambda item: item.state == OnOffType.OFF, ir.getItem("gMotion_Sensor").members)[0:3]# returns a list of up to 3 items, not a group
log.debug("JSR223: test: sliced filtered group=[{}]".format(slicedListOfMembers))

#map - use Jython map() function
mapOfItemValues = map(lambda lowBattery: "{}: {}".format(lowBattery.label,str(lowBattery.state) + "%"), ir.getItem("gBattery").members)# returns list
log.debug("JSR223: test: map of item values=[{}]".format(mapOfItemValues))

#reduce - use Jython reduce() function
reduceList = reduce(lambda sum, x: sum.add(x), map(lambda rain: rain.state, ir.getItem("gRainWeeklyForecast").members))# the state.add(state) is a method of QuantityType
log.debug("JSR223: test: reduce list=[{}]".format(reduceList))

#an example using most of these
batteryMessage = "Warning! Low battery alert:\n\n{}".format(",\n".join(map(lambda lowBattery: "{}: {}".format(lowBattery.label,str(lowBattery.state) + "%"), sorted(battery for battery in ir.getItem("gBattery").getMembers() if battery.state < DecimalType(33), key = lambda battery: battery.state))))
log.debug("JSR223: test: sorted list=[{}]".format(batteryMessage))

Let me know if there is anything specific you are having difficulty with!

4 Likes

@rlkoshak fyi, the move to jython eliminated the errors that i was seeing with the rules DSL. Essentially the same code and logic but no weird NULL errors. My last round of testing, it appeared that if 2 rules were iterating over the same list of group items at the same time would cause an error.

@5iver so far really enjoying jython and the simply decorators make it easy!

2 Likes

Scott, how do I replicate this if from the rules DSL?

if(previousState == NULL) return;

I’m happy and not surprised. I suspected JSR223 would be a better fit.

it is… should have taken your advice a month ago :slight_smile:

I’m assuming you are at the start of a function…

def testFunction(event)
    if event.oldItemState == UnDefType.NULL:
        return
    # do stuff

I’ve added this to the README

yes ,that is it.

@5iver is it possible that group events are not being properly processed. For instance, I have an item that is a member of 3 groups. When it changes, I see the event in the group that I am monitoring. But then I also see events come in through the same rule, but with the other 2 groups the item is a member of …

item A is a member of Groups B,C,D

have a rule capturing any changes to members of Group B

log all events from the rule and print the item name.... and I see

A
C
D

when it should just be A

It looks like you have run into this bug…

What I’ve done as a workaround, is to add this to the rule. It still misfires, but doesn’t execute the rest of the rule unless the triggering item is actually in the group. If this does not fit for you, share what the rule is triggering on.

@rule("Light: Area trigger")
@when("Member of gArea_Trigger changed")
def areaTrigger(event):
    if "gArea_Trigger" in ir.getItem(event.itemName).getGroupNames():
        # rest of rule

How is JSR223/Jython in regards to the “problems” with Thread::sleep and timers? are there any “static” limits on threads? Ie. Can I hypothetically run 10 different rules (in the same rules file or otherwise) with different sleep delays all at the same time?

Ref.:

I have not specifically looked through the code for a definitive answer, but from my experience, JSR223 rules do not have the same threadpool limitations found in the Rules DSL. Here is an example…

I have not tried it, but there is potentially multi-processing support too!

1 Like

that will work as a work around…

I am not a python expert, but it looks like the timer object is built on the thread tools of python - so I am guessing jython doen’t have the thread limitations of the dsl.

It goes deeper than that, but you should also be able to use the createTimer action. I haven’t tried it though, as I prefer to use Jython where available.

Any thoughts on simplifying the code below?

item A belongs to groups F, G, H

Group F belongs to group Z

Using item A, I want to find the group item A is a member of (F,G,H) is also in group Z.

ie in this case group F
    triggeringItem = itemRegistry.getItem (event.itemName)
    gArea = itemRegistry.getItem ("gArea")
    areaItem = None
    for area in gArea.getMembers ():
        for name in triggeringItem.getGroupNames ():
            if name == area.name:
                areaItem =itemRegistry.getItem (name)
                break```

These should not be as much of a problem in jython. I don’t think JSR223 Rules use the thread pools, maybe the Quartz thread pool?

However, there is indeed a limit to the number of threads you can run in Python or any other programming language on a given system. I had a sensor module in sensorReporter that was working with BT. I was spinning off a separate thread to poll for nearby devices looking for known macs. I then put reelyActive on the same machine which also uses BT (it’s Node.js not python). So reelyActive got a permanent lock on the BT device and my sensorReporter scanning threads just got stuck and piled up. Eventually the program just died. I would expect something similar to happen in the JSR223.

But, I don’t think there is any hard and fast predefined limit. I think it will continue spawning threads until you run out of RAM to create new one.

1 Like

The code you posted looks good and works, but here are some suggestions:

  1. There are some spaces between the function names and (). This will work, but looks odd.
  2. The break will only exit the current loop. You could add a check above the second for loop to see if areaItem is not None, and then break out of that loop too.
  3. You can use ir as an alias to itemRegistry to save some typing.
    triggeringItem = ir.getItem(event.itemName)
    gArea = ir.getItem ("gArea")
    areaItem = None
    for area in gArea.getMembers():
        if areaItem is not None:
            break
        for name in triggeringItem.getGroupNames():
            if name == area.name:
                areaItem = ir.getItem (name)
                break

BUT, another approach is to use a list comprehension, which is a very powerful tool in Python. Makes your code a one-liner…

areaItem = list(group for group in ir.getItem("gArea").members if ir.getItem(event.itemName) in group.members)[0] 

What are you looking to achieve with the rule? There might be an easier way to get there. This weekend I plan to post about a rule strategy that uses nested groups containing triggering items and action items (Area_Trigger and Area_Action), and a single rule that takes care of all lighting automation (I use it to turn on speakers too), and then some others that make light level adjustments based on lux level. Looks like you may be doing something similar.

1 Like