Library for JSR223 in Jython

@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?

I did notice it. However I am currently still waiting for the stable release.
The Idea is to get some knowledge what works well and what not with the OH1 instance and this library.
I will change all my rules to this setup and then see what’s good.
Migrating it to OH2 should then be not so much of an effort.
However for additional input I am always open and if you have a good idea just let me know. :slight_smile:

Right now I’m just poking around, but no real progress. I had hopes that I could finally start moving from OH1 to OH2, but I also noticed what @villaRob found in the jython + OH2 thread. :frowning:

So it seems to me the scriptable automation needs some more work.

Created new Module: FiFo (First in first out).

EasyRule.FiFo( "CalShow{}", 5)
EasyRule.FiFo( "CalWGName0", "CalWGName1", "CalWGName2", "CalWGName3")

Normal FiFo-Stuff:

2017-06-30 16:43:17.562 [INFO ] [runtime.busevents             ] - CalShow0 received command 51
2017-06-30 16:43:17.563 [INFO ] [runtime.busevents             ] - CalShow4 state updated to 55
2017-06-30 16:43:17.563 [INFO ] [runtime.busevents             ] - CalShow3 state updated to 54
2017-06-30 16:43:17.563 [INFO ] [runtime.busevents             ] - CalShow2 state updated to 53
2017-06-30 16:43:17.564 [INFO ] [runtime.busevents             ] - CalShow1 state updated to 52