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

jsr223
jython
lucid
Tags: #<Tag:0x00007f18300f8198> #<Tag:0x00007f18300f7e00> #<Tag:0x00007f18300f7bf8>

(B Synnerlig) #7

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.


(Scott Rushworth) #8

Shouldn’t this be…

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


(B Synnerlig) #9

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

I will soon add some documentation for the event object.


(Scott Rushworth) #10

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.


(B Synnerlig) #11

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:


(B Synnerlig) #12

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.


(B Synnerlig) #13

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


(B Synnerlig) #14

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.


(Juelicher) #15

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…


(B Synnerlig) #16

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


(Juelicher) #17

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.


(B Synnerlig) #18

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.


(Juelicher) #19

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

(B Synnerlig) #20

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


(Juelicher) #21

Thanks a lot, that did the trick, after adding the item it works as expected.

Sorry, somehow I must have overlocked that part of the installation instructions!


(B Synnerlig) #22

No you didn’t. I added it afterwards to the installation instructions. I’m happy that it works now! :rofl:


(B Synnerlig) #23

Hello! Did you make it?

Your script may look similar to this now:

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
from lucid.utils import getItemValue

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

            # Only set Test_String_1 to "CAT" when both switches are ON and only when an item trigger has occurred. (Not at cron events)
            bothSwitchesAreOn = ((getItemValue("My_TestSwitch_1", OFF) == ON) and (getItemValue("My_TestSwitch_2", OFF) == ON))
            if bothSwitchesAreOn:
                events.postUpdate("Test_String_1", "CAT")
                self.log.debug('CAT!!!')

        elif 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


(B Synnerlig) #24

We add a second function to our script that triggers upon the update and give it a unique rule name. We use a predefined “constant” for the cron expression and we import yet another trigger type from lucid.triggers named ItemStateUpdateTrigger. As you know, an item update is not the same as an item change. In case you need a recap.

Update your scripts to this:

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

@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(EVERY_30_SECONDS), # Using a cron expression constant
        ]

    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))

            # Only set Test_String_1 to "CAT" when both switches are ON and only when an item trigger has occurred. (Not at cron events)
            bothSwitchesAreOn = ((getItemValue("My_TestSwitch_1", OFF) == ON) and (getItemValue("My_TestSwitch_2", OFF) == ON))
            if bothSwitchesAreOn:
                events.postUpdate("Test_String_1", "CAT")
                self.log.debug('CAT!!!')

        elif 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

@rule
class WatchMyString(object): # Giving the class a unique name
    def getEventTriggers(self):
        return [ItemStateUpdateTrigger('Test_String_1')] # Triggering when the Test_String_1 items updates
    def execute(self, modules, inputs):
        self.log.setLevel(DEBUG)
        self.log.debug('Item: \'' + unicode(self.event.itemName) + '\', was updated')

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

After you’ve saved the new script, you’ll notice in the log that Test_String_1 gets an update every 30 seconds!

We don’t want that to happen.

When we send an update to the Test_String_1 in the first rule, we’d like to check first if the new value differs from the value stored so that the second rule only triggers whenever there is an actual change.

It can be done by inspecting the old value together with an if statement. However we will use another handy function in lucid.utils named postUpdateCheckFirst.

# Checks if the current state of the item is different than the desired new state.
# If the target state is the same, no update is posted.

Use it like this: postUpdateCheckFirst('Item', 'New Value')

Your job is to modify the script to do that.


<Item>.changeCommand(<State>)
(Rich Koshak) #25

Is there the equivalent for sendCommand? I’ve often wished the Rules DSL had this.


(B Synnerlig) #26

Sure there is.

sendCommandCheckFirst(itemName, newValue):

:grin:


<Item>.changeCommand(<State>)