Jython scripting using steve-bate/openhab2-jython

Hello everyone!

In my quest for a productive openhab2 script environment I have started to evaluate jython scripting using steve-bate/openhab2-jython.

I’ve managed to set it up and it’s seems very promising. I’ll need some time to get used to it and JSR223 scripting in general. There aren’t really so many examples to look upon and I will for sure have some questions in case you’d like to help a newbie.

My first question is what the following Xtend trigger would look like in openhab2-jython:

    System started or Item Network_Internet changed

Two triggers combined and one of them is the system start (I cant see that has been implemented by @steve1 )

Cheers!

2 Likes

It’s not directly supported by openHAB2. Did you try openhab.triggers.StartupTrigger?

1 Like

Thanks a lot @steve1, I appreciate your kind help a lot. I will try it out tomorrow.

If someone knows, how can I make a system call from within a jython script?

I tried os.system(‘ssh someone@somewhere bblahh’) but nothing seems to happen. Would I have to use the “exec binding” to do that maybe? I’d rather not if there is an easier way.

The os.system function works for me from the Jython command line. Be sure to check the return code. There’s lots of potential issues trying to access ssh (known_hosts confirmations, etc.). A more robust solution would be to use the Python subprocess module and capture the standard output and standard error pipe data.

1 Like

Thanks a lot! You are sure right about that. os.system indeed works in jython but I initially missed out the fact that user openhab2 is running the os.system command. ssh credentials were not set up for that user. After I fixed that, it works great! Thanks again for the kind help.

As I start to understand how things work, it’s getting better and better.

I learned this today:

items.TestNumber.postUpdate(12) # Fails. I can can't use the specific manipulator command methods for some reason.
events.postUpdate("TestNumber", 1) # Fails.  This action can only accept strings as arguments.
events.postUpdate("TestNumber", "1") # Works

I found out that if using the item_triggered decorator with a result_item_name I can actually return a number like this (It doesn’t need to be a string)

@item_triggered("SomeItem", result_item_name="MyNumberItem")
def UpdateNumberItem():
    return 9

I also learned that when using postUpdate, follow up triggers will only occur if the value I send differ from the one that is store. That’s good to know.

What surprised me a bit was that using sendCommand seems to work identically. I expected it to trigger follow up actions always, even if the actual value didn’t change.

What slows me down a little bit now in the beginning is that if a statement fails, (runtime errors) there is no errors logged. (I ssh into port 8081 and issue log:tail) That makes it a bit hard to find the errors. There might be a better way. Also I get logged out every 5 minutes or so which is a bit annoying.

Another thing that I’ve added to my experience is that if I change a module that I’ve imported into a script, I need to restart OH before the changes are reflected.

I’m using Visual Studio Code. New editor for me. It works well. Currently it isn’t aware that items and events is declared somewhere. Maybe I just need to add something more to my .env file.

PYTHONPATH=/etc/openhab2/automation/lib/python

Cheers!

I found out that if I just use a “vanilla” jython script like this (E.g not using
steve-bate/openhab2-jython) runtime errors do show up in the log:

scriptExtension.importPreset("RuleSimple")
scriptExtension.importPreset("RuleSupport")

class MyRule(SimpleRule):
    def __init__(self):
        self.triggers = [
             Trigger("MyTrigger", "core.ItemStateUpdateTrigger", 
                    Configuration({ "itemName": "TestString1"}))
        ]
        
    def execute(self, module, input):
        a = 8/0

automationManager.addRule(MyRule())

That is great because it’s really annoying if scripts just stops without anything in the log.
I think I will take a step back and learn more basics.

EDIT: Another problem disappeared. I don’t get logged out every 5 every 5 minutes from karaf. :rofl:

You can also do a normal Python reload from a script. It’s much faster than restarting OH.

1 Like

By “logout”, I assume you mean from the Karaf console? I’m not sure how the logging would be related. In any case, I’ve run OH2 for months without a restart and have never been automatically logged out of the console.

I have many things to learn but I’m getting there.

I can learn a lot by studying your scripts @steve1

Can I ask you @steve1, are you running those scripts on OH 2.2?

@steve1, there is a bug in OH 2.2.
:smile:

Ah, ok. I thought you were suggesting that the Karaf logout was caused by the Jython scripts. Thanks for the clarification.

I pull my hair (haven’t got so much anyway) trying to understand this from the channel event example:

from openhab.triggers import ChannelEventTrigger
from openhab.rules import rule, addRule

@rule
class channelEventExample(object):

    def get_event_triggers(self):__init__(self):
        return [ChannelEventTrigger('astro:sun:away:set#event','START','Sunset_Away') ])
                    
    def execute(self,modules,inputs):
        self.log.info('Sunset triggered')

addRule(channelEventExample())

More specifically, I can’t understand this line: def get_event_triggers(self):__init__(self):

I believe it must be wrong and the line that follows seems to have an unmatched ‘)’

@steve1, or maybe someone else, do you think you can you help me with this?

Cheers!

Those look like incorrect code in the example. I’ve modified the example but I’m not using channel triggers myself so I haven’t tested it. I think the example originally used an explicit SimpleRule subclass and the errors must have been introduced when it was refactored to use the @rule decorator.

Sorry about the hair.

no problem @steve1, maybe the hair will grow out again :rofl: (probably not)

Anyway, I’d like to send you a crate of European beers or something else to show you my gratitude for providing your framework to the community. I’ve heard you’ve only got Budweiser and Sol girl beer over there.

As I wrote before, I’m using your framework on OH 2.2 and I’m trying really hard to make things work using your examples that you’ve kindly bundled.

It’s hard for a newbie like me because there is something “eating” and hiding all the runtime error messages, That is, if I write something stupid like myVar = 8/0 in my scripts, the script will just stop. No error message in the the log whatsoever. I need to have my runtime error messages to show up in the log.

I’ve understood that OH build from November 10 #579 uses log4j instead of slf4j. Trying hard to figure out what’s going on under the hood, I’ve found out that openhab.log uses the old slf4j. Maybe that’s why things are not working so great for me here? Or maybe there is something in the default /var/lib/openhab2/etc/org.ops4j.pax.logging.cfg that I need to configure before logs can be shown? If I should make a guess, (As a newbie I probably should avoid that) there is a try/catch block in the framework capturing all runtime errors and sending them to the log in a way that’s not compatible with my log4j logging so they just end up in /dev/null … That’s just a guess, I’m a newbie after all.

I see a great potential in your framework so I won’t give up easy.

Do you have any ideas what I should do Steve?

Cheers!

EDIT:

Here is an example script:

(I use logging.error because logging.info doesn’t show up in the log at all)

from openhab.log import logging
from openhab.triggers import time_triggered

logging.error('Who let the dogs out?')

@time_triggered("0/10 * * * * ?")
def my_periodic_function():
    logging.error('Who let the cats out?')
    myVar = 8/0
    logging.error('This line will not show up in the log because the previous statement caused a runtime error')

Output in logfile:

9:21:18.354 [INFO ] [ort.internal.loader.ScriptFileWatcher] - Loading script 'test.py'
19:21:18.359 [ERROR] [ROOT                                 ] - Who let the dogs out?
19:21:20.009 [ERROR] [ROOT                                 ] - Who let the cats out?

But if I change my script a bit like this:

from openhab.log import logging
from openhab.triggers import time_triggered

logging.error('Who let the dogs out?')
myVar = 8/0

@time_triggered("0/10 * * * * ?")
def my_periodic_function():
    logging.error('Who let the cats out?')
    myVar = 8/0
    logging.error('This line will not show up in the log because the previous statement caused a runtime error')

Log output is:

9:22:35.019 [INFO ] [ort.internal.loader.ScriptFileWatcher] - Loading script 'test.py'
19:22:35.024 [ERROR] [ROOT                                 ] - Who let the dogs out?
19:22:35.026 [ERROR] [ript.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab2/automation/jsr223/test.py': ZeroDivisionError: integer divisionor modulo by zero in <script> at line number 5

Which is quite helpful for a newbie like me because I’ll be advised immediately that the script stopped and what the cause is.

Hi there!

I had some time looking at where the scripts runtime errors go and I found out that they get output using a print statement in an except clause where further execution stops.

I use the Karaf console to monitor the log in real time and whatever is put in a print statement will never be shown. I guess it’s always the case if starting openHAB as a service.

Anyway, after doing some modifications of the code and re-raising the exceptions things works much better here. I made PR #11.

EDIT: Adding a question:

When using a Trigger function decorator, is there a way to access the self object? E.g. to be able to use the objects logger object. (I know I could just use a Rule Decorator instead)

@time_triggered("0/10 * * * * ?")
def my_periodic_function():
    global count
    count += 1
    # If there was a way to refer to self here, I wouldn't have to
    # create a new logger object. Just curious if it's possible at all.
    # self.log.info("running periodic function: %d", count) 

Interesting question. A little context first… the function decorator dynamically creates a SimpleRule subclass and calls the decorated function from the execute method of the class. The self variable only exists within the bound methods of the dynamic class. A function like my_periodic_function doesn’t have a self variable.

The error printing in the dynamic wrapper object was there because I hadn’t worked out how to provide proper error logging for decorated functions. Your proposed approach is one possible solution: log to a default logging category. There are also other potential solutions. We could extend the decorator to support an optional log argument where a specific logger could be provided (defaulting to a base logger). You can also wrap your function body with a try/except handler and log to wherever you’d like (or send a notification, for example).

Thanks. I tried that but because the exception was caught previously and was never re-raised. My try/except handler wasn’t able to trap any exceptions. Script just ends with no output at all.

If you’re catching the exception in the function body, it cannot propagate to the implicit rule’s ‘except’ block.

@time_triggered("0/10 * * * * ?")
def my_periodic_function():
    try:
        global count
        count += 1/0
    except:
        # Handle the exception