[JSR223-Jython] Simplified rule definition (similar to Rules DSL) and universal decorator

jsr223
jython
decorator
Tags: #<Tag:0x00007f0156c531f0> #<Tag:0x00007f0156c53010> #<Tag:0x00007f0156c52ea8>

(Scott Rushworth) #61

Sorry for the late reply. I am slogging through a really nasty flu/cold and I’m doing my best to catch up on a lot of things.

This is for another Eclipse project, not Eclipse Smart Home.

Looking through the code and from what I understand about OH/ESH, I’m pretty certain this would not be possible. However, you can start/stop install/uninstall bindings through Karaf, which can be done programmatically. Here is an example using a shell script., and here is a DSL Rule (I have this in Jython too, if you want it). You should setup key auth for this.

I’m curious… why do you need to programmatically restart the binding?


(Michael Cumming) #62

@5iver, no worries…

I use both the ISY99 and Omnilink bindings, both are built on the google guava libraries. Both of these bindings can have problems with reconnecting to their external devices if the connection is lost. At this time, a binding restart seems to be the only way to deal with the issues. I thought doing a bundle restart from python would be cleaner than try to launch a script to tell karaf to do this.

OH has suggested not using guava in the future - but we are not sure if this is specific to that version of any version.


(Michael Cumming) #63

@scottk,

Have started trying this out - items work as expected but a group switch does not seem to generate an event when changed… See below.


openhab.items.add (item="TESTGROUP",item_type="Group", gi_base_type = "Switch", group_function = Or(OnOffType.ON, OnOffType.OFF))
openhab.items.add ("TESTITEM","Switch",groups=["TESTGROUP"])


g =ir.getItem('TESTGROUP')
i = ir.getItem ('TESTITEM')

log.info ('ITEM {}'.format (g))
log.info ('ITEM {}'.format (i))

events.postUpdate ('TESTGROUP',"ON")
events.postUpdate ('TESTITEM',"ON")

log.info ('ITEM {}'.format (g))
log.info ('ITEM {}'.format (i))

events.postUpdate ('TESTGROUP',"OFF")
events.postUpdate ('TESTITEM',"OFF")

log.info ('ITEM {}'.format (g))
log.info ('ITEM {}'.format (i))

openhab.items.remove('TESTGROUP')  
openhab.items.remove('TESTITEM') 

2018-11-10 09:54:03.145 [vent.ItemStateChangedEvent] - TESTITEM changed from NULL to OFF

==> /var/log/openhab2/openhab.log <==

2018-11-10 09:54:03.149 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTGROUP (Type=GroupItem, BaseType=SwitchItem, Members=1, State=OFF, Label=null, Category=null)

2018-11-10 09:54:03.152 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTITEM (Type=SwitchItem, State=OFF, Label=null, Category=null, Groups=[TESTGROUP])

2018-11-10 09:54:03.156 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTGROUP (Type=GroupItem, BaseType=SwitchItem, Members=1, State=OFF, Label=null, Category=null)

2018-11-10 09:54:03.159 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTITEM (Type=SwitchItem, State=OFF, Label=null, Category=null, Groups=[TESTGROUP])

2018-11-10 09:54:03.165 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTGROUP (Type=GroupItem, BaseType=SwitchItem, Members=1, State=ON, Label=null, Category=null)

2018-11-10 09:54:03.168 [INFO ] [eclipse.smarthome.model.script.Rules] - ITEM TESTITEM (Type=SwitchItem, State=ON, Label=null, Category=null, Groups=[TESTGROUP])

==> /var/log/openhab2/events.log <==

2018-11-10 09:54:03.178 [vent.ItemStateChangedEvent] - TESTITEM changed from OFF to ON

2018-11-10 09:54:03.185 [vent.ItemStateChangedEvent] - TESTITEM changed from ON to OFF

(Scott Karns) #64

Hi Michael, several others have noted this behavior for which @5iver opened this GitHub issue:


(Michael Cumming) #65

good - I am not going crazy :slight_smile:


(Scott Karns) #66

I have some generic scene mode rules that won’t work until this issue is resolved. I hope it doesn’t take long to sort out the problem.


(Scott Rushworth) #67

I added a few new changes with the help of @scottk. First, the rule UID is now added as an attribute of the function. Second, you can now optionally set tags for the rules. The third is just a validation check to make sure the rule names are unique, which isn’t a real requirement for rules to have (the UIDs are the unique identifiers… :slightly_smiling_face:), but it makes searching for tags easier. The new rule engine allows for rules to be enabled and disabled, but getting a rule UID can be difficult. These changes and examples will make it easier. Enabling/disabling rules adds a whole new layer of possibilities for automation!

One thing to keep in mind… rules will get new UIDs every time the script is saved, which means it will always be enabled. This means the enabled/disabled status of a JSR223 rule does not persist through an OH restart. Rules created in Paper UI do persist their status though. I’ll submit an issue for this.

Here is an example of a rule that disables itself after the first time it runs:

from openhab.rules import rule
from openhab.triggers import when
from openhab import osgi
ruleManager = osgi.get_service("org.eclipse.smarthome.automation.RuleManager")

@rule("One-shot trigger rule", tags=["Away"])
@when("Item Virtual_Switch_1 received update")
def oneAndDone(event):
    # do something        
    ruleManager.setEnabled(oneAndDone.UID, False)

Here is an example of a rule that enables another rule by name (same imports):

@rule("Enable by name", tags=["Evening", "Away"])
@when("Item Virtual_Switch_1 received update")
def exampleEnableByName(event):
    ruleUID = filter(lambda rule: rule.name == "One and done", rules.getAll())[0].UID
    ruleManager.setEnabled(ruleUID, True)

Here is an example of a rule that will enable/disable all rules with the tag “Away” (same imports):

@rule("Select by tag")
@when("Item Presence changed)
def exampleByTag(event):
    for rule in rules.getByTag("Away"):
        ruleManager.setEnabled(rule.UID, True if event.itemState == StringType("Away") else False)

Here’s basically the same one, but using multiple tags and with some extra logging (same imports):

@rule("Select by tags")
@when("Item Presence changed)
def exampleByTags(event):
    log.debug("JSR223: Test [{}]".format(map(lambda rule: rule.name, rules.getByTags("Away", "Night"))))
    for rule in rules.getByTags("Away", "Night"):
        log.debug("JSR223: Test1: before: [{}]: getStatus=[{}], getStatusInfo=[{}]".format(rule.name, ruleManager.getStatus(rule.UID), ruleEngine.getStatusInfo(rule.UID)))            
        ruleEngine.setEnabled(rule.UID, True if event.itemState == StringType("Away") else False)
        log.debug("JSR223: Test1: after: [{}]: getStatus=[{}], getStatusInfo=[{}]".format(rule.name, ruleManager.getStatus(rule.UID), ruleEngine.getStatusInfo(rule.UID)))

Experimental Next-Gen Rules Engine Documentation 4 of : Writing Scripts
(Michael Cumming) #68

Scott, we should add to the Wiki a list of all globals. I spent a few hours trying understand some weird behavior before I finally realized what was going to :frowning:

dir () gives

['Action', 'ActionBuilder', 'ActionType', 'CLOSED', 'Command', 'Condition', 'ConditionBuilder', 'ConfigDescriptionParameter', 'Configuration', 'CronTrigger', 'DECREASE', 'DOWN', 'DateTimeType', 'DecimalType', 'FASTFORWARD', 'File', 'FileUtils', 'FilenameUtils', 'HSBType', 'INCREASE', 'ImperialUnits', 'IncreaseDecreaseType', 'Item_Occupancy_Event_Metadata', 'Logger', 'LoggerFactory', 'MOVE', 'Metadata', 'MetadataKey', 'MetadataRegistry', 'Metadata_Easy', 'MetricPrefix', 'ModuleBuilder', 'ModuleType', 'NEXT', 'NULL', 'NextPreviousType', 'OFF', 'ON', 'OPEN', 'OnOffType', 'OpenClosedType', 'PAUSE', 'PLAY', 'PREVIOUS', 'PercentType', 'PlayPauseType', 'PointType', 'QuantityType', 'REWIND', 'RawType', 'RewindFastforwardType', 'Rule', 'SIUnits', 'STOP', 'SimpleActionHandler', 'SimpleConditionHandler', 'SimpleRule', 'SimpleTriggerHandler', 'SmartHomeUnits', 'State', 'StopMoveType', 'StringListType', 'StringType', 'StringUtils', 'Trigger', 'TriggerBuilder', 'TriggerType', 'UNDEF', 'UP', 'URLEncoder', 'UnDefType', 'UpDownType', 'Visibility', '__builtins__', '__doc__', 'audio', 'automationManager', 'events', 'ir', 'itemRegistry', 'items', 'log', 'metadata_easy', 'osgi', 'rule', 'ruleRegistry', 'rules', 'scriptExtension', 'se', 'things', 'voice', 'when']

(Scott Rushworth) #69

I thought this was pretty well documented here. What were you stepping on?


(Michael Cumming) #70

I guess I should read before I type :slight_smile:

Perhaps, a statement that one needs to be careful before they declare any variables that step on preset globals - there is no warning that you are doing something that will cause you lots of pain :frowning:


(Michael Cumming) #71

Scott, is there a way that a rule could be notified it is about to get unloaded? My testing indicates that if you are using a Python timer that if a rule gets unloaded, the timer still persists and is not garbage collected. I have not tested this with Java/DSL timers.


(Andi) #72

Is it also possible with your new implementation, to set the rulename (at least partially, maybe a random suffix should be created) as UID?

Otherwise the log statements are not human readable

2018-11-13 19:48:20.401 [.event.RuleStatusInfoEvent] - a7b86e52-a729-4bbf-baa6-c4921cc5e249 updated: RUNNING

That would be really great and btw thanks for your great library :wink:


(Scott Rushworth) #73

The rule name is being set… take a look in Paper UI> Rules and you will see all of your JSR223 rules listed by name. That log is displaying the rule UID, which can’t be modified through SimpleRule. Changing that log entry should be a really quick fix, and I’ve been meaning to update it for a while now… thank you for the reminder!


(Scott Rushworth) #74

Not OOTB, that I’m aware of. But a composite module, like the the StartupTrigger.py, may work for you. I’d really like to see a ShutdownTrigger.py get built. But I don’t know how you’d inform the timer to cancel itself. How do you know the timer is not garbage collected? I would think it would have to be, but maybe only after the timer terminated.


(Michael Cumming) #75

Scott, I haven’t dug deeply into this but… it seems timers in Python persist even after rules are reloaded and then they fire. I haven’t had the time to fully understand what is happening. In my use case, the timers are held within an object, and if the object got a “rule unloading” event, the object could clean up before its done.


(Michael Cumming) #76

that would be great…


(Scott Karns) #77

@5iver, I am really enjoying writing rules using the decorators you added to the openHAB Jython libraries, thank you!

I thought I would put this out there as an example of a simple little utility scritpt that I have found very useful when debugging some of my other rule scripts:

from org.slf4j import Logger, LoggerFactory

from openhab import osgi
from openhab.rules import rule
from openhab.triggers import when
#import openhab.rules
#reload(openhab.rules)
#import openhab.triggers
#reload(openhab.triggers)
#from openhab.rules import rule
#from openhab.triggers import when


# requires osgi import
ruleEngine = osgi.get_service("org.eclipse.smarthome.automation.RuleManager")

class GroupLister:
    def __init__(self, instName):
        self.__name__ = instName
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.model.script.Rules.%s" %
                                              self.__class__.__name__)
        self.logger.info("Instantiating instance of class %s" %
                         self.__class__.__name__)

    def __call__(self, event):
        self.logger.info("'%s' action (Jython cron rule)" % self.__name__)

        self.logger.info("All Group items:")
        for gItem in sorted(item for item in ir.getItemsOfType("Group"),
                            key = lambda item: item.name):
            self.logger.info("==> %s" % gItem)
            map(lambda mbr: self.logger.info("  |-- %s" % mbr),
                sorted(gMbr for gMbr in gItem.members,
                       key = lambda mItem: mItem.name))

        ruleEngine.setEnabled(self.UID, False) # one-shot rule


#logger = LoggerFactory.getLogger("org.eclipse.smarthome.model.script.jsr223")

# requires rule and when imports
groupLister = rule("Group Test Rule")(
              when("Time cron 10 0/1 * * * ?")
                  (GroupLister("groupLister")))

#logger.info("groupLister attributes & methods: %s" % dir(groupLister))

… an excerpt of its output from openhab.log:

2018-11-14 11:37:10.147 [INFO ] [thome.model.script.Rules.GroupLister] - ==> gAllOffToggleMonitor_Scene_Monitors (Type=GroupItem, Members=2, State=NULL, Label=null, Category=null)
2018-11-14 11:37:10.152 [INFO ] [thome.model.script.Rules.GroupLister] -   |-- gScene_Monitor_Scene_03 (Type=GroupItem, BaseType=SwitchItem, Members=3, State=ON, Label=null, Category=null, Groups=[gAllOffToggleMonitor_Scene_Monitors])
2018-11-14 11:37:10.156 [INFO ] [thome.model.script.Rules.GroupLister] -   |-- gScene_Monitor_Scene_04 (Type=GroupItem, BaseType=SwitchItem, Members=3, State=ON, Label=null, Category=null, Groups=[gAllOffToggleMonitor_Scene_Monitors])

Lambda calling other lambda? (JSR223/JYTHON)
[SOLVED] DateTime Items compare against fixed DateTimes
(bob_dickenson) #78

First an observation – I just converted some fairly complex Extend-style rules to use the JSR223-Jython interface and it is blazing-fast. The new decorators make a lot of sense and are very usable.

Second – a bit of a conundrum. I have a bunch of rules with “Member of Group changed”-style triggers. They all start out like this:

@rule("Member of gGroup_01 changed")
@when("Member of gGroup_01 changed")
def mainFunction(event):
    thisItem = ir.getItem(str(event.itemName))
    triggeredby = str(thisItem.name)
## DO STUFF based on item state......

@rule("Member of gGroup_02 changed")
@when("Member of gGroup_02 changed")
def mainFunction(event):
    thisItem = ir.getItem(str(event.itemName))
    triggeredby = str(thisItem.name)
## DO STUFF based on item state......

The problem that I am seeing occurs when a given item is a member of both gGroup_01 and also of gGroup_02 ergo both rules fire. MOST of the time everything works swimmingly (and FAST) but every once in a while the second-firing rule ends up throwing a NPE when making the call to ir.getitem(). This occurs on the order of 5 percent (or less) of the time – it is not confined to any particular item and the system recovers – the event in question is lost and so garbage-collection gets triggered in the JVM by the NPE – things in general are insensitive for a few seconds but do recover. I’ve put Try … Except blocks around the code with a delay-retry of the ir.getItem() in the exception block but that does not eliminate the problem (at least so far). Based on some of the logs I am getting, it looks like the window of vulnerability to this is a few thousandths of a second wide.


(Scott Karns) #79

Hi @bob_dickenson, I’m using a fairly complex set of generic, group-based rules that use triggers very similar to yours, in particular, because I have some items that belong to multiple groups referenced in multiple triggers, I have multiple rules triggered nearly simultaneously by changes to a single item common to more than one group. Because my rule actions need to know the group or the groups to which the triggering item belongs, the first part of my rules get a proper item instance as follows (no need for the conversion to a Jython str):

        sceneGroup = ir.getItem(event.itemName)
        if sceneGroup in self.group.members:
            self.logger.debug("scene load group: %s changed to: %s" %
                              (sceneGroup.name, event.itemState))

Also, note that when an item instance’s string name is needed, I haven’t found it necessary to explicitly convert the item’s name attribute to a Jython str.

I have yet to see any indication in my logs that I have bumped into the problem you describe, which seems odd.


(Scott Rushworth) #80

Which version of OH are you running? If not recent (I think it is in M5… definitely in recent snapshots), you may not have this fix. You could log the itemName at the start of the rule, and you might be able to spot the issue. This would only effect you if you have groups as members of the group.

Another thing is that your two functions are defined with the same name (mainFunction), but they should be unique. I’m actually surprised that the script works at all.

Hopefully this helps!