Library for JSR223 in Jython

Hi there,

I created a nice and easy library for jython which makes creating rules even more easier.
It would be really nice to get some feedback and I think this may be useful for other people, too.
So check it out at


Any feedback is apreciated!

Full credit to @steve1 who came up with all this billiant stuff!!


What does it do:

  • Creating an Instance of a rule is enough:
#create an instance of the rule
#Note: this is different! Creating an instance is enough!
MyJSR223RuleName()
  • It prints all the added rules in the logger window. This makes searching for errors really easy.
+--------------------------------------------------------------------------------+
| Adding Rules:                                                                  |
|  - Rule1                                                                       |
|  - Rule2                                                                       |
|  - Rule3                                                                       |
+--------------------------------------------------------------------------------+
  • It checks whether you have defined valid rule triggers:
+--------------------------------------------------------------------------------+
| Checking Rule1:                                                                |
|  - Found item 'MyItem1' for ChangedEventTrigger                                |
|  - Could not find item 'NonexistingItem1' for ChangedEventTrigger              |
| Rule 'Rule1' is not OK!                                                        |
+--------------------------------------------------------------------------------+
| Checking Rule2:                                                                |
|  - Found item 'MyItem2' for ChangedEventTrigger                                |
| Rule 'Rule2' is OK!                                                            |
+--------------------------------------------------------------------------------+
| Checking Rule3:                                                                |
|  - Found item MyItem3 for UpdatedEventTrigger                                  |
| Rule 'Rule3' is OK!                                                            |
+--------------------------------------------------------------------------------+
  • It adds a new Initialize() - function to the rule-class:
    def Initialize(self):
        #initialize all your variables and required states here
        self.__myvariable = 0

It can be linked to a specific item so setting openhab to a defined state (like after startup) is really easy.


SimpleRules:

#Easy ItemChanged - Rule declaration.
@EasyRule.ItemChanged("Itemname")
def MyRule1():
    BusEvent.postUpdate("Itemname", "0")

#no need to use oh-vars for the trigger anymore:
@EasyRule.ItemChanged("NumberItem", None, 1)
def MyRule1Changed():
    BusEvent.postUpdate("NumberItem", "0")

#Easy ItemUpdated- Rule declaration.
@EasyRule.ItemUpdated("Itemname")
def MyRule2():
    BusEvent.postUpdate("Itemname", "0")

#Easy TimerTrigger- Rule declaration.
@EasyRule.TimerTrigger("Chron")
def MyRule3():
    BusEvent.postUpdate("Itemname", "0")

Works also with the event-item:

#Accessing the event-item is also possible:
@EasyRule.ItemChanged("TestString")
def MyRule1(event):
    print(event)

#Easy ItemUpdated- Rule declaration.
@EasyRule.ItemUpdated("TestString")
def MyRule2(event):
    print(event)

The variables of the event get automatically converted to the jython equivalent (DecimalType -> int/float, DateTime to float, StringType -> str). Accessing the original item is still possible. The above two rules produce the following output:

Event [triggerType=CHANGE, item=TestString (Type=StringItem, State=ON, ohitem=(...)), oldState=OFF, newState=ON, command=None, ohEvent=(...)]
Event [triggerType=UPDATE, item=TestString (Type=StringItem, State=ON, ohitem=(...)), oldState=None, newState=ON, command=None, ohEvent=(...)]

Custom Exception-Handler are also possible:
These will push all errors via pushover.

def PushToPushover(Rule, exception):
    pushover = oh.getAction("Pushover")

    tb = traceback.format_exc()

    #try slicing - this is just to make it look pretty
    searchstr = "return cls.execute(self, Event(event) if self.ProcessEvents else event)"
    len_seach = len(searchstr)
    pos = tb.rfind(searchstr)
    if pos != -1:
        tb = tb[-1 * len(tb) + pos + len_seach:]

    pushover.pushover("Error in '{}':\n'{}'\n\n{}".format(Rule.name, exception, tb[-400:].strip(), 1))
EasyRule.SetExceptionHandler(PushToPushover)

Creating complex rule is more convenient, too:

@EasyRule.Rule
class cContact:
    #no need to call the base-class
    def __init__(self, item_name):
        self.__trigger_item = item_name
        self.__counter_item = cContact.__item_alias[item_name]

    #convenient initializer function
    def Initialize(self):
        print( "This function still gets called when the rule is loaded or the init-item changes to ON")

    def execute(self, event):
        #the event has the converted variables, too.
        myfloatvar = event.item.state

        #per Rule logger is automatically available
        self.logger.warning("asdfasdf")
3 Likes

is this for OH1?

The scripting engine for OH2 has not been merged yet.
So this is unfortunately only for OH1.
I am too waiting for it to get merged.

1 Like

@steve1 : Maybe you can take a short peek and give your opinion?

It looks like it has some useful features. Iā€™d really like to see people experimenting with ways to make OH easier to use and then sharing their results.

Iā€™m wondering if your approach could be simplified a little. For example, rather than creating the helper instance and then passing that into the EasyRule constructor, could the helper be created automatically the first time an EasyRule is constructed in a script and then used automatically during instance creation? It would be nice to not need to define a constructor in the rule subclass unless itā€™s needed for some other reason (e.g., variable initialization).

Iā€™ve also experimented with some Jython code for OH1 although my focus was a bit different. For example, Iā€™m not trying to validate item existence. The code is atā€¦

Iā€™ve also been doing experiments with the pending OH2 JSR223 functionality. That code is atā€¦

Yes - you are totally right. Thatā€™s what I thought of but I have not (yet) found an elegant solution for it.
I use the rule helper to return all created rules in the file:

def getRules():
    return RuleSet(helper.GetRules())

How would I do this when creating the instance automatically? When defining collector directly in the library wouldnā€™t the SystemStarted-Trigger not work for file changes any more?

I have already looked at your github project - thatā€™s where I got the oh_globals.py from.
But you seem to have good java background and be more firm in python than I am so unfortunately (for me) I understand very little. :smiley:
Anyway - thank you for your reply!
Maybe we can get a little discussion going so I can learn some things?
I would really apreciate it. :slight_smile:

The basic idea is to get a reference to the globals dictionary (accessed using the globals() function call) for the script. Thatā€™s easy within the script but can be challenging from a library module. Iā€™ll need to do some experimentation to determine how to do it. Once you have access to the globals, you can create the list to hold the rule instances (updated from the base class constructor) and even inject the getRules function automatically if it isnā€™t already defined.

I really liked the idea so I looked up the globals() function in the python documentation:
Return a dictionary representing the current global symbol table. This is always the dictionary of the current module (inside a function or method, this is the module where it is defined, not the module from which it is called).
This means passing the globasl() to a/each library function would be required and thus provide no benefit.

Injecting the getRules function would work without problems, but atm I do not see how to access any module information from a class defined in the library. Maybe you can figure something out ā€¦ ?

Yes, thatā€™s correct. I have prototyped a solution but I wonā€™t be able to post it until later tonight (US/Eastern time). The base class walks the call stack, finds the script stack frame and then accesses the globals from stack frame.

@Spaceman_Spiff, hereā€™s one way to do it. This is just a proof-of-concept so it only includes stubs of your ScriptHelper and related classes.

The following code shows how the script file will look. Iā€™m using a decorator to inject the EasyRule base class and the base class initializer call. However, it someone needs special initialization behavior (e.g., for multiple inheritance) then they can directly inherit from EasyRule (without the decorator) and do whatever they want.

from easyrule import easyrule

@easyrule
class MyJSR223Rule(object):
  def __init__(self, name):
    self.__name = name
    self.__myvariable = 0

  def Initialize(self):
    self.__myvariable = 0

  def __repr__(self):
    return "<Rule {}>".format(self.__name)

# Rules are automatically registered when instance is created.
MyJSR223Rule("test1")
MyJSR223Rule("test2")

# Note there is no getRules function. It is automatically injected 
# when the first easyrule is defined. However, if special functionality
# is needed in the getRules function, it can be explicitly defined
# (before any rules instances are created) and access the helper class
# holding the registered rules using the '__helper__' global variable.

The easyrule.py module is:

from openhab.globals import oh, Rule, RuleSet, StartupTrigger

class EasyRule(Rule):
  class Helper(Rule):
    def __init__(self):
      self.rules = [self]

    def getEventTrigger(self):
      return [ StartupTrigger() ]

    def execute(self, event):
      oh.logInfo(type(self).__name__, "Rules: {}".format(self.rules))

    def getRules(self):
      return RuleSet(self.rules)

  def __init__(self):
    # get the script globals
    script_globals = get_script_globals()
    # create and register the helper, if needed.
    script_helper = script_globals.get('__helper__')
    if script_helper is None:
      script_helper = EasyRule.Helper()
      script_globals['__helper__'] = script_helper
    # inject the getRules function, if needed.
    if 'getRules' not in script_globals:
      script_globals['getRules'] = script_helper.getRules
    script_helper.rules.append(self)

  def getEventTrigger(self):
    return []

  def execute(self, event):
    pass

def get_script_globals():
  """
  Walks the Python stack and finds the script-level globals dictionary.
  """
  depth = 1
  while True:
    frame = sys._getframe(depth)
    name = str(type(frame.f_globals))
    if name == "<type 'scope'>":
      return frame.f_globals
    depth += 1

The key features here are that the EasyRule constructor will automatically create a helper and assign to a global variable in the script, if itā€™s not already there. The script globals are obtained by walking the call stack looking for a special type name for the globals data structure (only used at the script level). EasyRule will also inject the getRules function into the script if it doesnā€™t exist.

For this example, when the rule file is loaded the following log entry is logged by the Helper class:

2017-02-28 20:29:54.487 [INFO ] [rg.openhab.model.jsr223.Helper] - Rules: [org.python.proxies.easyrule$Helper$66@1c4a847, <Rule test1>, <Rule test2>]

In your case, this would be checking triggers and so on. The following classdecorator automatically injects an EasyRule base class and adapts the __init__ method to invoke the EasyRule constructor. You could also use this decorator to add new methods or attributes or do other configuration.

def easyrule(cls):
  """
  Decorator for adding EasyRule subclass and initializer.
  """
  def init(self, *args, **kwargs):
    EasyRule.__init__(self)
    cls.__init__(self, *args, **kwargs)
  derived_class = type(cls.__name__, (cls, EasyRule), {
    '__init__': init
  })
  def new(cls, *args, **kwargs):
    return super(cls, cls).__new__(cls, *args, **kwargs)
  derived_class.__new__ = staticmethod(new)
  return derived_class

If you used a function decorator to tag the initialization method, then you could use any method (including the __init__ method) to do the item-triggered reinitialization. The class decorator would iterate over the methods and find the one with the specified tag. The function decorator could also specify the initialization Item name. Something likeā€¦

  # in class definition...
  @easyrule.initializer("MyItemName")
  def reset(self):
    self.n = 0

Iā€™ll leave the implementation of the function decorator as an exercise for the reader. :wink:

I am a little bit busy atm but I will try to implement the suggestions and report back and maybe ask for help! :slight_smile:
Maybe in two weeks or so I find time. Thanks for the input!

Hi @steve1 ,

I finally managed to play around a little bit with decorators but I am still struggling with getting the script scope:

I do not understand how you came up with this and with the frame name <type 'scope'>. Is this something one just has to know or how do I get there.
Also I tried to get this running using CPython but I only manage to get <class 'dict'> instead of scope. Am I missing something?

That code is based on lots of experimentation with the Jython JSR223 script engine implementation.

The stack walking technique is standard Python, but the scope-related code is very Jython JSR223-specific (ā€œscopeā€ is a JSR223 API concept). Also, you canā€™t run the code directly in Jython (from the command line, for example). It must be executed from a Jython JSR223 script or a module imported from a JSR223 script (so the ā€œscopeā€ is available).

I tried getting the calling script name but I am struggling.
Do you have any idea how to get the script name?

Also I have not figured out this part of the code:

Why the need to call the overridden new function?

Furthermore I am experiencing an error I do not unsterstand.
Maybe you can take a look @steve1?

javax.script.ScriptException: TypeError: unbound method __init__() must be called with BaseRule instance as first argument (got Test1 instance instead) in <script> at line number 26

I created the class decorator and a base class:

def BaseRuleDecorator( cls):

    def init(self, *args, **kwargs):
        BaseRule.__init__(self, *args, **kwargs)
        cls.__init__( self, *args, **kwargs)

    def execute(self, event):
        cls.execute(event)


    #replace init with newly defined one
    derived_class = type(cls.__name__, (cls, BaseRule), {'__init__': init, 'execute' : execute})

    #what does this do?
    def new(cls, *args, **kwargs):
        return super(cls, cls).__new__(cls, *args, **kwargs)
    derived_class.__new__ = staticmethod(new)

    return derived_class




class BaseRule(Rule):
    def __init__(self, *args, **kwargs):
        super(Rule, self).__init__()
        self.rulename = kwargs.get("rulename", self.__class__.__name__)

        helper = helperfunctions.GetScriptHelper()
        helper.AddRule(self)


    #required functions
    def Initialize(self):
        pass
    def getEventTrigger(self):
        return [ ]

    def __repr__(self):
        return "<Rule {}>".format(self.name)

Also I have created another class and a function decorator:

@Rule.BaseRuleDecorator
class Test1:
    def __init__(self, p1):
        print("Init Test1")
        self.rulename = 'TestRule1'

def ItemChanged( *args):
    def inner_func(user_function):
        Test1()
    return inner_func

When I then create a rule with the function decorator

@SR.ItemChanged("TestNumber", 0, 1)
def MyFunctionName( arg):
    print("TestNumber Test: ")
    print(arg)

The Problem seems to only exist within the function generator.
Do you have any idea?

I donā€™t think you would given the your decorator implementation. In my code, the extended __new__ function also initialized the log instance attribute in the created instance.

So - I have finally come around to go further on the library.
I love it very much - thanks @steve1 for your brilliant ideas!

What works:

#Easy ItemChanged - Rule declaration.
@EasyRule.ItemChanged("Itemname")
def MyRule1():
    BusEvent.postUpdate("Itemname", "0")

#no need to use oh-vars for the trigger anymore:
@EasyRule.ItemChanged("NumberItem", None, 1)
def MyRule1Changed():
    BusEvent.postUpdate("NumberItem", "0")

#Easy ItemUpdated- Rule declaration.
@EasyRule.ItemUpdated("Itemname")
def MyRule2():
    BusEvent.postUpdate("Itemname", "0")

#Easy TimerTrigger- Rule declaration.
@EasyRule.TimerTrigger("Chron")
def MyRule3():
    BusEvent.postUpdate("Itemname", "0")

Works also with the event-item:

#Accessing the event-item is also possible:
@EasyRule.ItemChanged("TestString")
def MyRule1(event):
    print(event)

#Easy ItemUpdated- Rule declaration.
@EasyRule.ItemUpdated("TestString")
def MyRule2(event):
    print(event)

The variables of the event get automatically converted to the jython equivalent (DecimalType -> int/float, DateTime to float, StringType -> str). Accessing the original item is still possible. The above two rules produce the following output:

Event [triggerType=CHANGE, item=TestString (Type=StringItem, State=ON, ohitem=(...)), oldState=OFF, newState=ON, command=None, ohEvent=(...)]
Event [triggerType=UPDATE, item=TestString (Type=StringItem, State=ON, ohitem=(...)), oldState=None, newState=ON, command=None, ohEvent=(...)]

Custom Exception-Handler are also possible:
These will push all errors via pushover.

def PushToPushover(Rule, exception):
    pushover = oh.getAction("Pushover")

    tb = traceback.format_exc()

    #try slicing - this is just to make it look pretty
    searchstr = "return cls.execute(self, Event(event) if self.ProcessEvents else event)"
    len_seach = len(searchstr)
    pos = tb.rfind(searchstr)
    if pos != -1:
        tb = tb[-1 * len(tb) + pos + len_seach:]

    pushover.pushover("Error in '{}':\n'{}'\n\n{}".format(Rule.name, exception, tb[-400:].strip(), 1))
EasyRule.SetExceptionHandler(PushToPushover)

Creating complex rule is more convenient, too:

@EasyRule.Rule
class cContact:
    #no need to call the base-class
    def __init__(self, item_name):
        self.__trigger_item = item_name
        self.__counter_item = cContact.__item_alias[item_name]

    #convenient initializer function
    def Initialize(self):
        print( "This function still gets called when the rule is loaded or the init-item changes to ON")

    def execute(self, event):
        #the event has the converted variables, too.
        myfloatvar = event.item.state

        #per Rule logger is automatically available
        self.logger.warning("asdfasdf")

What is missing?

  • Some more testing of course.
  • Getting the RuleFile, currently it only says <script>
    return cls.execute(self, Event(event) if self.ProcessEvents else event)
  File "<script>", line 160, in execute

Does anyone have an idea how to get the script name?

edit: typos

I havenā€™t been able to get the script name. The Jython JSR223 source code has some support for that but I think the current official Jython release doesnā€™t include those changes. However, the OH2 JSR223 implementation will provide the script name when the script starts.

Very nice! I did push the source to github, so maybe you like to take a look?
I created it as a module so there should not be any side effects.
Also I really feel that I have leared a lot so thank you very much @steve1!
I really apreciate your help! :thumbsup: :slight_smile: :thumbsup:

I did some more work. I created a custom ItemRegistry which replaces the default imported ItemRegistry.
The new ItemRegistry automatically returns the converted item and maps from java to jython exception.
Also the item has now a postUpdate and a sendCommand - function.


This code :

myitem = ir.getItem("TestNumber")
print( "Type: '{}' value: '{}'".format( type(myitem.state), myitem.state))
myitem.postUpdate(1)
print( "Type: '{}' value: '{}'".format( type(myitem.state), myitem.state))
myitem.postUpdate(1.7)
print( "Type: '{}' value: '{}'".format( type(myitem.state), myitem.state))

Produces this:

2017-05-23 10:45:36.291 [ERROR] [m.j.EasyRule.OHTypes.__convert] - Error converting 'TestNumber' (Uninitialized) : invalid literal for float: Uninitialized!
Type: '<type 'NoneType'>' value: 'None'
2017-05-23 10:45:36.292 [INFO ] [runtime.busevents             ] - TestNumber state updated to 1
Type: '<type 'int'>' value: '1'
2017-05-23 10:45:36.799 [INFO ] [runtime.busevents             ] - TestNumber state updated to 1.7
Type: '<type 'float'>' value: '1.7'

As you can see in case of error the state is None. If not it is directly mapped to a native type.

@Spaceman_Spiff Did you notice that JSR223 in OH2 is merged and available in the snapshot builds?