Lucid script step by step ... (Jython scripting tutorial using lucid,jsr223 Jython helper library)

This little tutorial might be for you if you want to get started doing some Jython scripting using lucid. Even if you are happy with Xtend, just give it a try, it won’t hurt to widen the horizon sometimes.

Prerequisites

It’s assumed that you already have some very basic python knowledge (or at least that you are very willing to learn), you have set up an editor (VSC is recommended but vi if you insist, gedit or any other editor will probably also do) for editing your scripts and you know where to put them. You should also know how to watch the debug output.

Before we start, define 2 openHAB switch items and name them “My_TestSwitch_1” and “My_TestSwitch_2”

Now, we start with a very basic script, full of comments and we add functionality to it step by step. Copy this script and save it by the name stepByStep.py in your jsr223 script folder (E.g. something similar to /etc/openhab2/automation/jsr223 )

Your script will look like this:

# The lucid step_by_step script will take you through some very basic steps using lucid and Jython
# This is the starting script and we will slowly add some complicity.

from lucid.rules import rule, addRule # Needed for defining and adding a rule
from lucid.triggers import ItemStateChangeTrigger # Import the different type of triggers that you will be using

# First we add a rule that triggers upon the change of any of 2 switches.
# To run this, you need to define 2 openHAB switch items and name them "My_TestSwitch_1" and "My_TestSwitch_2"
# Add them to a site map so that you can operate them and watch the debug output.
# You will find that the script won't trigger when My_TestSwitch_2 when changes from ON to OFF

@rule
class StepByStep(object): # Giving the class a unique name
    def getEventTriggers(self):
        return [
            ItemStateChangeTrigger('My_TestSwitch_1'), # Triggering when the switch changes its state.
            ItemStateChangeTrigger('My_TestSwitch_2', ON), # Only trigger when switch turns on
        ]

    def execute(self, modules, inputs):
        self.log.info('One of the test switches has changed its state') # That's all we do

addRule(StepByStep()) # Needed to add the rule, use the same name as defined for the class

Now, place your newly created switches on a sitemap and try toggling them and watch the debug output.

If You have any comments or questions please feel free to post them here in this thread. I also appreciate if you’d just like to leave a comment that you have joined. Then I will know that this isn’t yet another one of my many monologues :joy::rofl::smiley: (A message should never have more than 3 smilies)

Next step will be in my next post. We will add some functionality to the script so that it becomes aware about which one of the switches that triggered the script.

It will probably take a day or two. (My next post)

You might want to watch the thread but others will probably reply to it too.

5 Likes

It would be helpful to provide links to instructions to achieve all of these prerequisites. I know they exist scattered through the openHAB docs and elsewhere. For some of the OH users, this could be a daunting task.

I wrote up a tutorial to get JSR223 installed for those of us running in Docker awhile back. JSR223 Jython with Docker openHAB which might be helpful to some.

This tutorial is fantastic. Please keep posting!

1 Like

Yes, great work!! Haven‘t had the time to try it out myself, but I will do this as soon as possible and of course I will follow this thread!

1 Like

Thanks Rich

I’ve changed the original post a bit. I’ll start from scratch hoping that several members of the community will jump on the Jython/lucid-train, at least for a try.

Cheers!

1 Like

Happy to have you on board @juelicher!

Installed the library and can run your script from this thread without problems, great.

Only the “old” problem with jsr232 scripting persists, the scripts fail to load during startup, but everything is fine after “touching” them while openHAB is running. But this certainly is no problem with your library

Thanks for sharing this work!!

1 Like

Today I’ve been a bit busy with starting the lucid documentation. It will take a while before it’s usable.

Anyway. We need to do another step on our script today. I thought we should just do some small steps today.

  • Add a time based trigger
  • Let the script find out what was triggering it

So. The first thing we need to do is to add a time based trigger.

We like our script to trigger every other minute so we go to cronmaker and make us a cron expression for that.

The resulting cron expression is 0 0/2 * 1/1 * ? * and we can add the following line to the python trigger list object after the other two existing triggers:

            CronTrigger('0 0/2 * 1/1 * ? *'), # Runs every other minute

Did You note the trailing comma just before the comment? You might wonder why it’s there.

It’s because the function we are putting the trigger in, getEventTriggers, is returning a python list. A python list is a python data structure object that can hold items of various kinds. For example:

a = [66.25, 333, 333, 1, 1234.5]

Each item that you put inside the python list named “a” above is separated by a comma. The last item doesn’t need to have a trailing comma even though you can put one if you like.

The above statement can be broken into several lines like this:

a = [
         66.25,
         333,
         333,
         1,
         1234.5
    ]

The list can hold other items than just numbers, for example our trigger objects.

So after adding the CronTrigger, the getEventTriggersFunction of the StepByStep class will look like:

    def getEventTriggers(self):
        return [
            ItemStateChangeTrigger('My_TestSwitch_1'), # Triggering when the switch changes its state.
            ItemStateChangeTrigger('My_TestSwitch_2', ON), # Only trigger when switch turns on
            CronTrigger('0 0/2 * 1/1 * ? *'), # Runs every other minute
        ]

We also need to import the CronTrigger, after ItemStateChangeTrigger, Change that import line to:

from lucid.triggers import ItemStateChangeTrigger, CronTrigger # Import the different type of triggers that you will be using

Make the changes in your file and save it. You should now see log entries each second minute telling you that ‘One of the test switches has changed its state’ which is a lie. You have created your first bug. Congratulations!

The script needs to be aware of whatever trigged it. The self.event object will provide you with a lot of information about the event that caused the rule to trigger.

We can now extend our execute function so it looks like what follows:

    def execute(self, modules, inputs):
        self.log.setLevel(DEBUG)

        if self.event.isItem:
            self.log.debug('One of the test switches has changed its state')
            self.log.debug('Triggering item name: \'' + unicode(self.event.itemName) + '\', state: ' + str(self.event.state))

            if self.event.itemName == 'My_TestSwitch_1':
                self.log.debug('Boooo')

        elif self.event.isCron:
            self.log.debug('A cron event occurred')

You may have noticed that we now explicitly sets the debug level for the script to DEBUG.

As you see the event object that we get has quite a few nice properties that we can access to get information about the event that triggered.

self.event.isItem is True if an item change or an item update caused the script to trigger. self.event.isCron is True when the script has been triggered by a timer event (cron).

self.event.itemName holds the name of the item name that triggered (in case it was triggered by an item)

The final script now looks like this:

# The lucid step_by_step script will take you through some very basic steps using lucid and Jython
# This is the starting script and we will slowly add some complicity.

from lucid.rules import rule, addRule # Needed for defining and adding a rule
from lucid.triggers import ItemStateChangeTrigger, CronTrigger # Import the different type of triggers that you will be using

# First we add a rule that triggers upon the change of any of 2 switches.
# To run this, you need to define 2 openHAB switch items and name them "My_TestSwitch_1" and "My_TestSwitch_2"
# Add them to a site map so that you can operate them and watch the debug output.
# You will find that the script won't trigger when My_TestSwitch_2 when changes from ON to OFF

@rule
class StepByStep(object): # Giving the class a unique name
    def getEventTriggers(self):
        return [
            ItemStateChangeTrigger('My_TestSwitch_1'), # Triggering when the switch changes its state.
            ItemStateChangeTrigger('My_TestSwitch_2', ON), # Only trigger when switch turns on
            CronTrigger('0 0/2 * 1/1 * ? *'), # Runs every other minute
        ]

    def execute(self, modules, inputs):
        self.log.setLevel(DEBUG)

        if self.event.isItem:
            self.log.debug('One of the test switches has changed its state')
            self.log.debug('Triggering item name: \'' + unicode(self.event.itemName) + '\', state: ' + str(self.event.state))

            if self.event.itemName == 'My_TestSwitch_1':
                self.log.debug('Boooo')

        elif self.event.isCron:
            self.log.debug('A cron event occurred')

addRule(StepByStep()) # Needed to add the rule, use the same name as defined for the class

Try it out, try to understand every line of code. Please Ask if you have some questions.

2 Likes

Shouldn’t this be…

self.log.debug(‘Triggering item name: '’ + unicode(event.itemName) + '', state: ’ + str(event.itemState))

No. The event object has no itemState property. It has a state property though.

I will soon add some documentation for the event object.

1 Like

Ah… I’m still using the item_triggered decorator which uses ItemEventTrigger, where the event object does contain itemState. I assumed that this was always the case. That seems like it could get messy if you’re not using the decorators and have a rule with multiple triggers… you’d need to determine which trigger fired in order to know what the event object has in it. Hmmm.

That would be very helpful. Specifically, what the event object contains for each of the trigger classes.

In reading through the reeadme, under Event-based triggers, you have…

ItemStateChangeTrigger('<item>', ['<state>']), # Item changed

Previous state is also supported (item changed from X to Y). The order of the arguments should probably be rearranged to have triggerName last, because without using keyword arguments, it would be…

ItemStateChangeTrigger('<item>', ['<state>'], None, ['<previousState>']), # Item changed

BTW, how is the triggerName used? When I put in a value I don’t see a difference. It looks like it is just for logging though, and I’m doing my logging a little different.

Thanks for your reply. I’d be very happy to discuss all this in detail. Quite interesting. Can we do it in a new thread because it would be a bit off-topic in this tutorial step by step thread. :wink:

Update your items file and your sitemap file so that they match this:

stepbystep.items

Switch My_TestSwitch_1 "My Test Switch 1"
Switch My_TestSwitch_2 "My Test Switch 2"
String Test_String_1 "Test String 1: [%s]"
String Test_String_2 "Test String 2: [%s]"

stepbystep.sitemap

sitemap stepbystep label="Lucid Step By Step" {
	Frame label="Test Items" {
		Default item=My_TestSwitch_1
		Default item=My_TestSwitch_2
		Default item=Test_String_1
		Default item=Test_String_2
	}
}

Now what we will do today is to modify the script a bit so that when the My_TestSwitch_1 is switched on, the string Test_String_1 will be set to “CAT” and when you should make the cron event set the string to “DOG”.

To set a variable (posting updates and sending commands) you use the events object which is part of the default scope, meaning that you don’t have to import it.

Just use it like this:

events.postUpdate("Test_String_1", "some data")

It will be your “home work” to change the script to do the above.

If you followed my instructions, your script should now similar to this:

from lucid.rules import rule, addRule # Needed for defining and adding a rule
from lucid.triggers import ItemStateChangeTrigger, CronTrigger # Import the different type of triggers that you will be using

@rule
class StepByStep(object): # Giving the class a unique name
    def getEventTriggers(self):
        return [
            ItemStateChangeTrigger('My_TestSwitch_1'), # Triggering when the switch changes its state.
            ItemStateChangeTrigger('My_TestSwitch_2', ON), # Only trigger when switch turns on
            CronTrigger('0 0/2 * 1/1 * ? *'), # Runs every other minute
        ]

    def execute(self, modules, inputs):
        self.log.setLevel(DEBUG)

        if self.event.isItem:
            self.log.debug('One of the test switches has changed its state')
            self.log.debug('Triggering item name: \'' + unicode(self.event.itemName) + '\', state: ' + str(self.event.state))

            if self.event.itemName == 'My_TestSwitch_1':
                events.postUpdate("Test_String_1", "CAT")

        elif self.event.isCron:
            self.log.debug('A cron event occurred')
            events.postUpdate("Test_String_1", "DOG")

addRule(StepByStep()) # Needed to add the rule, use the same name as defined for the class

In the next step, we want to change the logic a bit.

Now, we only want to set Test_String_1 to "CAT" when both switches are ON but only when an item trigger has occurred. (Not at cron events)

We now have the need to check items for their current values. How do we do that?

You can get an items state by using the itemRegistry object like this:

itemName = "My_TestSwitch_1"
item = itemRegistry.getItem(itemName)

That will make the variable named item store a reference to the My_TestSwitch_1 item object so that you can access it’s methods and attributes. You can then get the items state simply by it’s state attribute: item.state. To compare whatever is inside item.state with a text string you can conveniently convert it to a string object by item.state.toString().

To check if the switch is on, we’d need to do something like:

itemName = "My_TestSwitch_1"
item = itemRegistry.getItem(itemName)
if item.state == ON:
    # Do something

Now, in case an item isn’t initialized (never used) we should have to handle such cases too and now I think it’s already getting tedious. Lot’s of work for making a small task.

We are more lazy than that. Therefore we take advantage of another nifty function from lucid.utils named getItemValue.

getItemValue(itemName, defVal) #  Returns the items value if the item is initialized otherwise return the default value.

Simply add getItemValue to the import statement above and separate it with a comma sign.

Add an import statement in the code:
from lucid.utils import getItemValue

You can now get an items current value as simple as this example:

getItemValue("My_TestSwitch_1", OFF)

It can be stored in a variable:
someVar = getItemValue("My_TestSwitch_1", OFF)

or it can be used in an if statement directly just like:
if getItemValue("My_TestSwitch_1", OFF) == ON

You may have noticed that we used OFF as the second argument for getItemValue. OFF is here simply the value that we want to get back if the item state we request doesn’t exist (uninitialized).

Now, go ahead and modify the script to achieve what we want using the lucid getItemValue function.

Thanks again for the nice tutorial!

With the script above I get the following error when it is executed:

2018-07-16 10:00:00.987 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'ShowSomeEventInfo-ec62e7cf-415e-4cd1-89bd-97c6145c7fdd': Failed to execute action '1': null

I have no idea why…

@juelicher

So, the script from the initial post works but the latest version fails when cron triggers it. Is that right?

Are you using the exact script above or have you made any modifications to it? I see that your script seems to be named differently: “ShowSomeEventInfo-ec62e7cf-415e-4cd1-89bd-97c6145c7fdd”.

Can you try the exact code from post #13 ?

Sorry, I copied the wrong line from the log, that was one from your example scripts, that shows the same error. But it ist he same for the step by step script:

2018-07-16 10:20:01.249 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-4ba4cb78-d698-4ca8-854a-1cc4bc9fd4bc': Failed to execute action '1': null

I did a copy&paste for the script and the items, so that should be identical.

That’s a bit strange because the script from post #13 works here.

Please double check that you have copied all text from the script.
Please also check again that the script from post 1 is still working.

I’ll be busy now for some hours but I will return later.

The script from the first post works as expected:

2018-07-16 11:19:32.395 [INFO ] [lucid.StepByStep                    ] - One of the test switches has changed its state

The script from post #7 and #13 shows the error, whether triggered by item change or cron:

018-07-16 11:21:55.500 [INFO ] [lucid.utils                         ] - WAITING 1.2100315961 sec !!!
2018-07-16 11:21:55.501 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-dac8a7cc-5976-41fb-b872-b0d1804c8da8': Failed to execute action '1': null
2018-07-16 11:21:57.038 [INFO ] [lucid.utils                         ] - WAITING 0.64235910478 sec !!!
2018-07-16 11:21:57.039 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-dac8a7cc-5976-41fb-b872-b0d1804c8da8': Failed to execute action '1': null
2018-07-16 11:22:00.870 [INFO ] [lucid.utils                         ] - WAITING 0.857558331162 sec !!!
2018-07-16 11:22:00.871 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-dac8a7cc-5976-41fb-b872-b0d1804c8da8': Failed to execute action '1': null
2018-07-16 11:22:14.832 [INFO ] [lucid.utils                         ] - WAITING 1.3118525047 sec !!!
2018-07-16 11:22:14.832 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-dac8a7cc-5976-41fb-b872-b0d1804c8da8': Failed to execute action '1': null
2018-07-16 11:24:01.285 [ERROR] [.automation.core.internal.RuleEngine] - Failed to execute rule 'StepByStep-dac8a7cc-5976-41fb-b872-b0d1804c8da8': Failed to execute action '1': null

@juelicher

I might have an idea what’s going on. Have a look at the last step in installing lucid. (Create an openHAB item named ZZZ_Test_Reload_Finished) I just added it. Does that help?