[Jython] How to: Separating the Business Logic from the rule to make code reuse real easy. Examples included

The problem

I’ve always found reusing of rules cumbersome. Unless your item names match the ones from the rule that you’re trying to copy, it’s a lot of search 'n replace of those item names to match yours. Then there is the problem that the rule may use items that you didn’t define and you need to reverse engineer the rule to define those items. As a result I hardly ever copy any rules and due to time constraints and/or lack of interest to put in the effort my smarthome isn’t as smart as it could be.

When someone on the forum asks for help how to implement a certain rule often some friendly forum member steps in and gives some pointers on how the rule could be implemented or just creates a rule for that person. It seems that the wheel gets reinvented many times.

I figured there must be a better way to do this, and indeed there is.

The solution

The solution to this problem is to extract the business logic out of the rule and putting it in its own class. This class will then form the base class from which you derive your own rule class. The base class can implement some default behavior which you can override in your derived rule class.

The base class can easily be shared and since it does not contain any items it does not need to be modified by whomever wants to derive rules from it. This does not mean that you cannot work with items in your base class. You simply provide the item names in the constructor which then get mapped to class members. Using the item names you simply get the item from the item registry (see the example provided below).

We could create a repository on Github where different base classes could be collected in a community maintained library making it easy to pull in new base classes, updates and bug fixes.

I successfully implemented this concept using Jython, but it may work for Groovy and JavaScript as well.

I’m still experimenting with this concept but I would like to share this idea to trigger ideas and get feedback on things that could be improved. I’m making use of the openhab2-jython library and I’ll be the first to admit that (beyond the basics) I don’t have a clue how this library works. For example I couldn’t get the ItemStateChangeTrigger triggers to work and therefor used the method provided by the Raw ESH Automation API. The library is flexible enough to support this.
Also, I mainly program in Java hence my style of programming Python isn’t really pythonic.

An example

Let’s say you want to dim your lights when you play a movie using Kodi, dim to a different level when you pause the movie so that you can actually see where you’re going when getting some popcorn and drinks, and restore the light to a normal level when the movie ends.
This all sound pretty easy to implement. The Kodi binding offers a control channel which provides the status of the Kodi player which can be mapped to a Player item. When the movie starts playing the state becomes PLAY, when the movie pauses the state becomes PAUSE, and when the movie stops the status becomes PAUSE… eh say what? That’s right. When the Kodi player stops, the state becomes PAUSE because the openHAB Player item has no STOP state (I guess the binding needs to set the state to NULL or UNDEF here), so based on the state of this channel there is no way to differentiate whether the movie is paused or whether it is stopped.

Next problem, you hardly use Kodi to play music. One evening you have visitors and talk about music. You decide to startup Kodi to play some music and then the lights dim. That was unexpected… since your rule triggers on PLAY state of the Player item your lights dimmed. You didn’t foresee that next to the Player item you should also take into account the mediatype of the playing media.

Wouldn’t it be nice if someone else had already found these issues and worked around them and made it darn simple while doing so?

Enough talk, let’s see how this all works.

The rule to implement

The following is all you need to implement to fix all these quirks that you didn’t think about when you implemented your rule (hypothetically speaking of course, I know you already worked around all these issues :slight_smile:).

First import all required modules

import community.kodi
reload(community.kodi)

from org.slf4j import LoggerFactory

import openhab
from community.kodi import KodiPlayerState

Next define the rule class. Note that we’re not using SimpleRule as the baseclass, instead we use KodiPlayerState as the baseclass.
The KodiPlayerState baseclass contains all the business logic that we no longer have to worry about. For brevity I left out the logging in the method calls but while developing you probably want to include some log statements to know what’s going on.


class KodiRule(KodiPlayerState):
    def __init__(self, controlItem, mediatypeItem):
        KodiPlayerState.__init__(self, controlItem, mediatypeItem)
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiRule")

The base class contains some default methods. If you do not override any methods then depending on the type of media you play the following methods will be called (from left to right):

onPlayChannel()
onPlayEpisode()
onPlayMovie() onPlayVideo()
onPlayMusicVideo() onPlay()
onPlayPicture()
onPlayRadio() onPlayAudio()
onPlaySong()

So if you would like to switch the lights off when watching a movie and dim to 10% when watching an episode then you would override onPlayMovie() to switch the lights off and onPlayEpisode() to dim the lights to 10%. Now when you would like to get a drink you probably don’t care whether you’re watching a movie or an episode. You just want to see where you’re going and therefor you would like to dim the lights to 20%. You could override both onPauseMovie() and onPauseEpisode() and dim the lights to 20% in each of these methods. However, you would be repeating yourself, so instead of overriding these two methods simply override only one method, the onPlayVideo() method and dim the lights to 20%.

Switching TV channels is still troublesome. The state changes from PLAY to PAUSE to STOP to PLAY. You don’t want to dim you’re lights every time when you zap from one channel to the next so we override them here as well. In this example we don’t want anything to happen when we pause or stop either a musicvideo or a picture either so we override these methods as well.

May be it would have been easier to just override onPauseMovie(), onStopMovie(), onPauseEpisode(), and onStopEpisode(). Then you wouldn’t have had to override all the channel, musicvideo, and picture methods :thinking: Then again this is an example so it’s good to show what’s possible :innocent:

    # overridden methods
    def onPlayEpisode(self):
        # dim lights to 10%
        events.postUpdate("Light_GF_LivingRoom_Front", "10")

    def onPlayMovie(self):
        # switch lights off
        events.postUpdate("Light_GF_LivingRoom_Front", "OFF")

    def onPauseVideo(self):
        # dim lights to 20%
        events.postUpdate("Light_GF_LivingRoom_Front", "20")

    def onStopVideo(self):
        # dim lights to 70%
        events.postUpdate("Light_GF_LivingRoom_Front", "70")

    def onPlaySong(self):
        # enable disco lights
        events.postUpdate("DiscoLight_GF_LivingRoom", "ON")
        # start spinning disco ball
        events.postUpdate("DiscoBall_GF_LivingRoom", "ON")

    def onStopSong(self):
        # disable disco lights
        events.postUpdate("DiscoLight_GF_LivingRoom", "OFF")
        # stop spinning disco ball
        events.postUpdate("DiscoBall_GF_LivingRoom", "OFF")

    def onPauseChannel(self):
        pass

    def onStopChannel(self):
        pass

    def onPauseMusicVideo(self):
        pass

    def onStopMusicVideo(self):
        pass

    def onPausePicture(self):
        pass

    def onStopPicture(self):
        pass

Finally add the rule using the automationManager. Note that here we provide item names to the KodiRule constructor which will pass these names on to the KodiPlayerState constructor. It is of course also possible to instead hardcode these into your rule when calling the KodiPlayerState.__init__() method. I just like doing it this way.

# add your Kodi Control Channel and Kodi Media Type Channel items
automationManager.addRule(KodiRule("Kodi_Laptop_control", "Kodi_Laptop_mediatype"))

For the fun of it, let’s create another rule for Kodi in the bedroom. Here we’ll only dim the lights when playing a movie.

import community.kodi
reload(community.kodi)

from org.slf4j import LoggerFactory

import openhab
from community.kodi import KodiPlayerState

class KodiBedroomRule(KodiPlayerState):
    def __init__(self, controlItem, mediatypeItem):
        KodiPlayerState.__init__(self, controlItem, mediatypeItem)
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiBedroomRule")

    # overridden methods
    def onPlayMovie(self):
        # dim lights to 10%
        events.postUpdate("Light_FF_Bedroom", "10")

    def onPauseMovie(self):
        # dim lights to 20%
        events.postUpdate("Light_FF_Bedroom", "20")

    def onStopMovie(self):
        # dim lights to 70%
        events.postUpdate("Light_FF_Bedroom", "70")


automationManager.addRule(KodiBedroomRule("Kodi_Bedroom_control", "Kodi_Bedroom_mediatype"))

So there you have it, a two very simple rules with no business logic at all. Just some simple methods that get called when the particular event occurs.

The base class

The base class needs to be stored in your python path. I guess most examples use the automation/lib/python directory. This is the same directory where you also created the openhab directory which contains the openhab2-jython library. In this automation/lib/python directory create the community directory.

In the community directory first create an empty file with the name __init__.py. Next create the kodi.py file and copy in the following contents:

import time

from org.slf4j import LoggerFactory
from org.eclipse.smarthome.automation.core.util import TriggerBuilder
from org.eclipse.smarthome.config.core import Configuration

#import openhab
from openhab import items
from openhab.jsr223.scope import scriptExtension


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

from openhab.jsr223.scope import SimpleRule

class KodiPlayerState(SimpleRule):

    # control states
    PAUSE = "PAUSE"
    PLAY = "PLAY"

    # control and mediatype state
    UNDEF = "UNDEF"

    # mediatype states
    CHANNEL = "channel"
    EPISODE = "episode"
    MOVIE = "movie"
    MUSICVIDEO = "musicvideo"
    PICTURE = "picture"
    RADIO = "radio"
    SONG = "song"

    def __init__(self, controlItem, mediatypeItem, delay=.200):
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.KodiPlayerState")
        self.logger.info("Initializing KodiPlayerState")
        self.controlItem = controlItem
        self.mediatypeItem = mediatypeItem
        self.delay = delay

        self.triggers = [
            TriggerBuilder.create()
                    .withId("trigger_" + controlItem)
                    .withTypeUID("core.ItemStateChangeTrigger")
                    .withConfiguration(
                        Configuration({
                            "itemName": controlItem
                        })).build(),
            TriggerBuilder.create()
                    .withId("trigger_" + mediatypeItem)
                    .withTypeUID("core.ItemStateChangeTrigger")
                    .withConfiguration(
                        Configuration({
                            "itemName": mediatypeItem
                        })).build()
        ]


    def execute(self, module, input):
        # self.logger.info("KodiPlayerStateRule triggered")

        oldState = str(input["oldState"])
        newState = str(input["newState"])
        mediatype = str(items[self.mediatypeItem])

        # tv channel

        # Unfortunately for tv channels, this is not perfect.
        # When switching from one channel to the next this will cause multiple state changes,
        # first to PAUSE, then to STOP, and finally to PLAY again, hence you probably don't
        # want to dim your lights based on these state changes. 
        if oldState == KodiPlayerState.CHANNEL and newState == KodiPlayerState.UNDEF:
            self.onStopChannel()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.CHANNEL:
            self.onPlayChannel()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.CHANNEL:
            self.onPlayChannel()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.CHANNEL:
            time.sleep(delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.CHANNEL:
                self.onPauseChannel()

        # episode from the library
        elif oldState == KodiPlayerState.EPISODE and newState == KodiPlayerState.UNDEF:
            self.onStopEpisode()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.EPISODE:
            self.onPlayEpisode()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.EPISODE:
            self.onPlayEpisode()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.EPISODE:
            time.sleep(self.delay)
            # When media stops playing, its state in openHAB is set to PAUSE. Therefor we add a little delay
            # to give openHAB a chance to update the mediatype to UNDEF. If the media type is indeed UNDEF
            # then kodi has stopped playing the media, otherwise it did indeed pause the media
            if items[self.mediatypeItem].toString() == KodiPlayerState.EPISODE:
                self.onPauseEpisode()

        # movie from the library
        elif oldState == KodiPlayerState.MOVIE and newState == KodiPlayerState.UNDEF:
            self.onStopMovie()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.MOVIE:
            self.onPlayMovie()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.MOVIE:
            self.onPlayMovie()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.MOVIE:
            time.sleep(self.delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.MOVIE:
                self.onPauseMovie()

        # musicvideo from the library
        elif oldState == KodiPlayerState.MUSICVIDEO and newState == KodiPlayerState.UNDEF:
            self.onStopMusicVideo()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.MUSICVIDEO:
            self.onPlayMusicVideo()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.MUSICVIDEO:
            self.onPlayMusicVideo()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.MUSICVIDEO:
            time.sleep(self.delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.MUSICVIDEO:
                self.onPauseMusicVideo()

        # picture
        elif oldState == KodiPlayerState.PICTURE and newState == KodiPlayerState.UNDEF:
            self.onStopPicture()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.PICTURE:
            self.onPlayPicture()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.PICTURE:
            self.onPlayPicture()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.PICTURE:
            time.sleep(self.delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.PICTURE:
                self.onPausePicture()

        # radio channel
        elif oldState == KodiPlayerState.RADIO and newState == KodiPlayerState.UNDEF:
            self.onStopRadio()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.RADIO:
            self.onPlayRadio()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.RADIO:
            self.onPlayRadio()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.RADIO:
            time.sleep(self.delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.RADIO:
                self.onPauseRadio()
        # song
        elif oldState == KodiPlayerState.SONG and newState == KodiPlayerState.UNDEF:
            self.onStopSong()
        elif oldState == KodiPlayerState.UNDEF and newState == KodiPlayerState.SONG:
            self.onPlaySong()
        elif oldState == KodiPlayerState.PAUSE and newState == KodiPlayerState.PLAY and mediatype == KodiPlayerState.SONG:
            self.onPlaySong()
        elif oldState == KodiPlayerState.PLAY and newState == KodiPlayerState.PAUSE and mediatype == KodiPlayerState.SONG:
            time.sleep(self.delay)
            if items[self.mediatypeItem].toString() == KodiPlayerState.SONG:
                self.onPauseSong()


    # Overridable methods
    def onPlayChannel(self):
        self.onPlayVideo()

    def onPauseChannel(self):
        self.onPauseVideo()

    def onStopChannel(self):
        self.onStopVideo()

    def onPlayEpisode(self):
        self.onPlayVideo()

    def onPauseEpisode(self):
        self.onPauseVideo()

    def onStopEpisode(self):
        self.onStopVideo()

    def onPlayMovie(self):
        self.onPlayVideo()

    def onPauseMovie(self):
        self.onPauseVideo()

    def onStopMovie(self):
        self.onStopVideo()

    def onPlayMusicVideo(self):
        self.onPlayVideo()

    def onPauseMusicVideo(self):
        self.onPauseVideo()

    def onStopMusicVideo(self):
        self.onStopVideo()

    def onPlayPicture(self):
        self.onPlayVideo()

    def onPausePicture(self):
        self.onPauseVideo()

    def onStopPicture(self):
        self.onStopVideo()

    def onPlayRadio(self):
        self.onPlayAudio()

    def onPauseRadio(self):
        self.onPauseAudio()

    def onStopRadio(self):
        self.onStopAudio()

    def onPlaySong(self):
        self.onPlayAudio()

    def onPauseSong(self):
        self.onPauseAudio()

    def onStopSong(self):
        self.onStopAudio()

    def onPlayAudio(self):
        self.onPlay()

    def onPauseAudio(self):
        self.onPause()

    def onStopAudio(self):
        self.onStop()

    def onPlayVideo(self):
        self.onPlay()

    def onPauseVideo(self):
        self.onPause()

    def onStopVideo(self):
        self.onStop()

    def onPlay(self):
        pass

    def onPause(self):
        pass

    def onStop(self):
        pass

That’s all. If you now start a song in the living room then your disco lights turn on and your disco ball starts spinning. Party time! :partying_face:

As you can see in the execute method of the base class, I cast the newState, oldState, and mediatypeItem states such as UNDEF, PLAY, and PAUSE to a string. I have not yet figured out how I can use these states as openHAB types such as for example PlayerType.PLAY.

In the rule file (so not the base class file) you’ll notice that I have a reload(community.kodi) statement. This forces openHAB to reload the base class whenever the file containing the rule is changed. This is helpful when updating the base class file since openHAB does not automatically reload the base class file when it has been updated. After updating the base class file I simply add or remove a new line to the rule file and save it which will then cause openHAB to compile and reload the base class file.

Of course polymorphism, inheritance and all that OOP stuff is nothing new. However, I’m pretty excited how well it works when applied to creating openHAB rules. I’d love to hear your thoughts on this.

7 Likes

Sorry for taking so long to reply! I have completed a straight migration of my rules to JSR223-Jython, and I’m just starting to be able to enjoy how much more powerful it is than the Rules DSL! I haven’t taken the time to actually test/use your example, but it looks very promising. I have Kodi setup on 3 FireTVs around the house, with MythTV as a PVR. So, I’ll get back to you with comments on the actual functionality. But, if you are interested, could you take a look here… https://github.com/OH-Jython-Scripters/openhab2-jython/issues/29. I’m planning to restructure the openhab2-Jython repo this weekend for better collaboration, taking some insights from your post. Maybe you have some more ideas on how to do this, and would want to share this example over there?

Hi Scott, no worries. I’m not expecting you to respond to each and every JSR223-Jython topic here on the forum.

I think it would be great if there would be a place at the openHAB Jython Scripters github where these scripts could be shared and where people could send PR’s to either add new scripts or improve on the ones that exist in that repository. That would keep all Jython stuff nicely together and easy to find for people.

In the mean time I will keep adding examples to this topic. I’m currently porting the General Scene Rule for jsr223 which is written in Javascript for openHAB 1.x.

1 Like

Add double click and long click functionality to buttons

I have a few Qubino ZMNHDA2 Flush Dimmers. These dimmers use one button to control the dim level of the light and have another two inputs that can be used to trigger basic on/off states. The Qubino does not support double click, triple click, pressing the button for a longer amount of time, or any other standard scene trigger. This is where the following example comes in.

The following example makes it possible to respond to double click and long button presses. In my openHAB setup we use the double click for turning everything off in the living room and kitchen. The long button press we use to turn on the radio. Whenever this button doesn’t work (due to connectivity problems between the Harmony Hub binding and the Harmony Hub), I’m told immediately; the WAF is very high in this rule. Without this rule I’d have a very hard time upgrading my current openHAB 1.x system to openHAB 2.

I configured our switches such that the Left button controls the Light and the Right button triggers the Rule. This makes it easy to remember which button does what.

The rule to implement

import community.smartswitch
reload(community.smartswitch)

from org.slf4j import LoggerFactory

import openhab
from community.smartswitch import SmartSwitch

# We're not using SimpleRule as the base class, instead we use SmartSwitch as the base class.
# The SmartSwitch base class contains all the business logic that we no longer have to worry about.
class SmartSwitchRule(SmartSwitch):
    def __init__(self, switchItem):
        SmartSwitch.__init__(self, switchItem)
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.SmartSwitchRule")

    # overridden methods
    def onDoubleClick(self):
        self.logger.info("onDoubleClick()")
        # add code here to do whatever you want to do when someone double clicks the button
    
    def onLongClick(self):
        self.logger.info("onLongClick()")
        # add code here to do whatever you want to do when someone long clicks the button

automationManager.addRule(SmartSwitchRule("SmartSwitch"))

Like in the Kodi example, you provide the name of the item that triggers the rule in the automationManager.addRule(SmartSwitchRule("SmartSwitch")) statement (here SmartSwitch).

By default the time you need to press and hold the button for the long click event to be triggered is 2 seconds, however you can of course override this. The default time between two clicks to consider it as a double click is 1 second, but you can override this as well.

For example to change the long click time to 3 seconds and the double click to 2 seconds use the following:

automationManager.addRule(SmartSwitchRule("SmartSwitch", 3, 2))

For the double click it is important not to set it too small to take into account network latencies.

To be clear, this rule is not limited to Qubino dimmers. Basically any device that causes on/off state changes can be used.

The base class

The base class needs to be stored in your python path. I guess most examples use the automation/lib/python directory. This is the same directory where you also created the openhab directory which contains the openhab2-jython library. In this automation/lib/python directory create the community directory.

In the community directory first create an empty file with the name __init__.py. Next create the smartswitch.py file and copy in the following contents:

import time
from threading import Timer

from org.slf4j import LoggerFactory

from org.eclipse.smarthome.automation.core.util import TriggerBuilder
from org.eclipse.smarthome.config.core import Configuration

from openhab.jsr223.scope import scriptExtension
from openhab.jsr223.scope import SimpleRule

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

class SmartSwitch(SimpleRule):

    ON = "ON"
    OFF = "OFF"

    '''
    default timeout for longClick is 2 seconds; if the button state is ON for 2 seconds or longer the onLongClick() method will be invoked
    default doubleClick time is 1 second; if the time between two clicks is less than 1 second the onDoubleClick() method will be invoked
    '''
    def __init__(self, switchItem, longClick=2, doubleClick=1):
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.SmartSwitch")

        self.switchItem = switchItem
        self.longClick = longClick
        self.doubleClick = doubleClick
        self.lastClick = 0.0
        self.timer = None

        self.triggers = [
            TriggerBuilder.create()
                .withId("trigger_" + switchItem)
                .withTypeUID("core.ItemStateChangeTrigger")
                .withConfiguration(
                    Configuration({
                        "itemName": switchItem
                    })).build()
        ]

    def execute(self, module, input):
        # self.logger.info("SmartSwitch triggered")

        newState = str(input["newState"])

        if newState == SmartSwitch.ON:
            self.stateChangedToOn()
        elif newState == SmartSwitch.OFF:
            self.stateChangedToOff()

    def stateChangedToOn(self):
        self.cancelTimer()

        now = time.time()
        if now < (self.lastClick + self.doubleClick):
            self.lastClick = 0.0
            self.onDoubleClick()
        else:
            self.lastClick = time.time()
            self.startTimer()
        

    def stateChangedToOff(self):
        self.cancelTimer()

    def startTimer(self):
        self.cancelTimer()
        self.timer = Timer(self.longClick, self.onTimeout)
        self.timer.start()

    def cancelTimer(self):
        if self.timer != None:
            self.timer.cancel()
            self.timer = None
    
    def onTimeout(self):
        self.timer = None
        self.onLongClick()

    # Overridable methods
    def onDoubleClick(self):
        pass
    
    def onLongClick(self):
        pass

Testing

This chapter is to give you an idea of how I’m developing and testing these rules while my production system is still on openHAB 1.x. On my development PC I run a snapshot release of openHAB 2.4 in a Docker container as described here with the necessary bindings installed.

During development I’m not using an actual Qubino dimmer but I’m using MQTT instead to trigger the item and simply watch the openhab.log for onDoubleClick() and onLongClick() log entries.

Switch SmartSwitch "SmartSwitch"        { mqtt="<[jarvis:livingroom/smartswitch:state:default], >[jarvis:livingroom/smartswitch:command:*:default]" }

Here jarvis is the name of my MQTT server and livingroom/smartswitch is the topic the item subscribes to.

sitemap default label="Smarthome"
{
    Frame label="Debug" {
        Switch    item=SmartSwitch
    }
}

A small sitemap allows me to control the switch from a browser.

To automate testing I’m using a little script in MQTT.fx to trigger the different ON/OFF states:

var Thread = Java.type("java.lang.Thread");

function execute(action) {
    out("Test Script: " + action.getName());
    for (var i = 0; i < 2; i++) {
        switchON();
        Thread.sleep(200);
        switchOFF();
        Thread.sleep(200);
    }

    switchON();
    Thread.sleep(2500);
    switchOFF();

    for (var i = 0; i < 3; i++) {
        switchON();
        Thread.sleep(200);
        switchOFF();
        Thread.sleep(200);
    }

    action.setExitCode(0);
    action.setResultText("done.");
    out("Test Script: Done");
    return action;
}

function switchON() {
    out("SmartSwitch ON");
    mqttManager.publish("livingroom/smartswitch", "ON");
}

function switchOFF() {
    out("SmartSwitch OFF");
    mqttManager.publish("livingroom/smartswitch", "OFF");
}

function out(message){
     output.print(message);
}

I especially like the scripting capability of MQTT.fx because it allows me to consistently run the same tests over and over again.

1 Like

General Scene Rule

This is a Jython port of the General Scene Rule for jsr223 which was written in JavaScript for openHAB 1.x by @jonnydev13. I recommend reading his topic for the why, how, and what about this rule.

Next to porting the rule I extracted the business logic into a separate base class, did some refactoring, and added some minor optimizations, e.g. when an item already has the correct state then that item will not be sent a state update; only when a state change is required will a state update be sent to the item. Other than that the behaviour should not be different from the original.

Even though the example shows scenes controlling only lights, you are not limited to just controlling lights. You could for example set the volume level of your AV receiver, tune in to a different radio station, close the blinds, ignite the fireplace, etc.

The rule to implement

import community.scene
reload(community.scene)

from community.scene import GeneralScene

class SceneLivingRoomRule(GeneralScene):
    def __init__(self):
        scenesJsonString = """
        [{
            "stateItem": "Scene_Living",
            "states": [
            {
                "stateValue": "0",
                "targetValues": [
                    { "item": "kitchenLights", "value": "OFF" },
                    { "item": "nookLight", "value": "OFF" },
                    { "item": "livingRoomLamp", "value": "OFF" },
                    { "item": "livingRoomTableLamp", "value": "OFF" },
                    { "item": "livingRoomCeilingLights", "value": "OFF" }
                ] 
            },
            {
                "stateValue": "1",
                "targetValues": [
                    { "item": "kitchenLights", "value": "setting_lr_dim_kitchenBrightness" },
                    { "item": "nookLight", "value": "setting_lr_dim_nookBrightness" },
                    { "item": "livingRoomLamp", "value": "setting_lr_dim_sofaLampBrightness" },
                    { "item": "livingRoomTableLamp", "value": "setting_lr_dim_tableLampBrightness" },
                    { "item": "livingRoomCeilingLights", "value": "setting_lr_dim_ceilingLightBrightness" }
                ] 
            },
            {
                "stateValue": "2",
                "targetValues": [
                    { "item": "kitchenLights", "value": "setting_lr_med_kitchenBrightness" },
                    { "item": "nookLight", "value": "setting_lr_med_nookBrightness" },
                    { "item": "livingRoomLamp", "value": "setting_lr_med_sofaLampBrightness" },
                    { "item": "livingRoomTableLamp", "value": "setting_lr_med_tableLampBrightness" },
                    { "item": "livingRoomCeilingLights", "value": "setting_lr_med_ceilingLightBrightness" }
                ] 
            },
            {
                "stateValue": "3",
                "targetValues": [
                    { "item": "kitchenLights", "value": "ON" },
                    { "item": "nookLight", "value": "ON" },
                    { "item": "livingRoomLamp", "value": "ON" },
                    { "item": "livingRoomTableLamp", "value": "ON" },
                    { "item": "livingRoomCeilingLights", "value": "ON" }
                ] 
            }
            ],
            "nonMatchValue": "4"
        }]
        """

        GeneralScene.__init__(self, scenesJsonString)

automationManager.addRule(SceneLivingRoomRule())

So all you need to do is derive your rule class from the GeneralScene base class, define the correct states in the scenesJsonString string and call the GeneralScene constructor.

The base class

The base class needs to be stored in your python path. I guess most examples use the automation/lib/python directory. This is the same directory where you also created the openhab directory which contains the openhab2-jython library. In this automation/lib/python directory create the community directory.

In the community directory first create an empty file with the name __init__.py. Next create the scene.py file and copy in the following contents:

import json
import time
import traceback
import uuid

from org.slf4j import LoggerFactory

from org.eclipse.smarthome.automation.core.util import TriggerBuilder
from org.eclipse.smarthome.config.core import Configuration
import org.eclipse.smarthome.core.items.ItemRegistry

from openhab import items

from openhab.jsr223.scope import scriptExtension
from openhab.jsr223.scope import SimpleRule
from openhab.jsr223.scope import events

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

class GeneralScene(SimpleRule):

    def __init__(self, scenesJsonString, delay=.200):
        self.logger = LoggerFactory.getLogger("org.eclipse.smarthome.automation.GeneralScene")
        self.scenes = json.loads(scenesJsonString)
        self.delay = delay
        self.DEFAULT_NON_MATCH_VALUE = -1

        triggers = []
        updateItems = []

        try:
            for scene in self.scenes:
                self.logger.info("Adding ItemCommandTrigger [{}]", scene['stateItem'])
                stateItem = scene['stateItem']
                triggers.append(TriggerBuilder.create().withId(uuid.uuid1().hex).withTypeUID("core.ItemCommandTrigger").withConfiguration(Configuration({"itemName": stateItem})).build())

                for state in scene['states']:
                    self.logger.debug("stateValue: [{}]", state['stateValue'])
                    for targetValue in state['targetValues']:
                        self.logger.debug("    targetValue:  [{}]", targetValue)
                        # add this targetValue to updateItems array if it's not there
                        alreadyAdded = False
                        for updateItem in updateItems:
                            if updateItem == targetValue['item']:
                                self.logger.debug("    Already added item [{}] to updateItems", targetValue['item'])
                                alreadyAdded = True
                                break
                        
                        if not alreadyAdded:
                            self.logger.debug("    Adding item [{}] to updateItems", targetValue['item'])
                            updateItems.append(targetValue['item'])

            for updateItem in updateItems:
                self.logger.info("Adding ItemStateChangeTrigger [{}]", updateItem)
                triggers.append(TriggerBuilder.create().withId(uuid.uuid1().hex).withTypeUID("core.ItemStateChangeTrigger").withConfiguration(Configuration({"itemName": updateItem})).build())

            self.triggers = triggers
        except:
            self.logger.info("{}", traceback.format_exc())


    def execute(self, module, input):
        # Scene was triggered
        if input['event'].type == "ItemCommandEvent":
            self.receivedItemCommandEvent(input)
            # give the message bus time to process the updates
            time.sleep(self.delay)
        elif input['event'].type == "ItemStateChangedEvent":
            self.receivedItemStateChangedEvent(input)

    def isStateEqual(self, oldState, newState):
        if oldState == None and newState == None:
            return True
        elif oldState == None or newState == None:
            return False

        oldState = str(oldState).upper()
        newState = str(newState).upper()
        self.logger.trace("isStateEqual(): oldState [{}] newState [{}]", oldState, newState)
        return (oldState == newState or
                (oldState == "0" and newState == "OFF") or
                (oldState == "100" and newState == "ON"))

    def receivedItemCommandEvent(self, input):
        try:
            self.logger.info("Got COMMAND event for item [{}], command=[{}]", input['event'].itemName, input['command'])
            
            for scene in self.scenes:

                # Get the correct Scene
                if scene['stateItem'] == input['event'].itemName:
                    # trigger the states
                    for state in scene['states']:
                        self.logger.debug("    stateValue: [{}]", state['stateValue'])
                        
                        if state['stateValue'] == str(input['command']):
                            self.logger.debug("        Got a matching stateValue")
                            
                            for targetValue in state['targetValues']:
                                self.logger.debug("        item=[{}]", targetValue['item'])
                                self.logger.debug("            Is targetValue['value']=[{}] a command or an item?", targetValue['value'])
                                
                                # if target value is an item, get its state
                                try:
                                    item = items[targetValue['value']]
                                    self.logger.debug("            Found item, state=[{}]", item)
                                    commandToSend = str(item)
                                except:
                                    self.logger.trace("{}", traceback.format_exc())
                                    self.logger.debug("            No item exists with name [{}], treating as command", targetValue['value'])
                                    commandToSend = targetValue['value']
                                
                                if not self.isStateEqual(items[targetValue['item']], commandToSend):
                                    self.logger.info("            Sending command. Item=[{}], Command=[{}]", targetValue['item'], commandToSend)
                                    events.sendCommand(targetValue['item'], commandToSend)
                                else:
                                    self.logger.info("            Item [{}] has the correct state [{}] already", scene['stateItem'], state['stateValue'])

                            break
                break
        except:
            self.logger.error("{}", traceback.format_exc())

    def receivedItemStateChangedEvent(self, input):
        try:
            self.logger.info("Got CHANGE event for item [{}], new state=[{}]", input['event'].itemName, input["newState"])

            for scene in self.scenes:
                self.logger.debug("scene['stateItem']=[{}]", scene['stateItem'])
                
                foundStateMatch = False
                for state in scene['states']:
                    self.logger.debug("    stateValue: {}", state['stateValue'])

                    possibleMatch = False
                    for targetValue in state['targetValues']:
                        self.logger.debug("        item=[{}]", targetValue['item'])

                        if input['event'].itemName == targetValue['item']:
                            self.logger.debug("            Possible state match stateItem=[" + scene['stateItem'] + "], stateValue=[" + state['stateValue'] + "], item=[" + targetValue['item'] + "]")

                            possibleMatch = True
                            break


                    if possibleMatch == True:
                        self.logger.debug("            Looping through items in stateItem=[{}], stateValue=[{}]", scene['stateItem'], state['stateValue'])

                        currentStateIsMatch = False
                        for targetValue in state['targetValues']:
                            self.logger.debug("                item=[{}]", targetValue['item'])
                            self.logger.debug("                    Is targetValue['value']=[{}] a command or an item?", targetValue['value'])
                            itemTargetValue = targetValue['value']
                            try:
                                item = items[targetValue['value']]
                                self.logger.debug("                    Found item, state=[{}]", item)
                                itemTargetValue = str(item)
                            except:
                                self.logger.debug("                    No item exists with name [{}], treating as command", targetValue['value'])
                                self.logger.trace("{}", traceback.format_exc())

                            itemState = str(items[targetValue['item']])
                            currentStateIsMatch = self.isStateEqual(itemState, itemTargetValue)
                            if currentStateIsMatch == True:
                                self.logger.debug("                    This item's state [" + itemState + "] was a match [" + itemTargetValue + "], continuing")
                            else:
                                self.logger.debug("                    This item's state [" + itemState + "] was not a match [" + itemTargetValue + "] breaking")
                                break

                        if currentStateIsMatch == True:
                            foundStateMatch = True
                            if str(items[scene['stateItem']]) == state['stateValue']:
                                self.logger.info("        After all items, this state is a match. Item [{}] has the correct state [{}] already", scene['stateItem'], state['stateValue'])
                            else:
                                self.logger.info("        After all items, this state is a match. Setting item [{}] to value [{}]", scene['stateItem'], state['stateValue'])
                                events.postUpdate(scene['stateItem'], state['stateValue'])
                            break # break here, were' done
                        else:
                            self.logger.debug("        After all items, this state is not a match. item [{}] value [{}]", scene['stateItem'], state['stateValue'])

                    else:
                        self.logger.debug("            Not a match. stateItem=[{}], stateValue=[{}]", scene['stateItem'], state['stateValue'])



                # Didn't find a state match, set scene value to nonMatchValue if there is one
                if not foundStateMatch:
                    try:
                        self.logger.info("No scene matches. scene.nonMatchValue=[{}]", scene['nonMatchValue'])
                        if scene['nonMatchValue']:
                            if self.isStateEqual(items[scene['stateItem']], scene['nonMatchValue']):
                                self.logger.info("Scene item [{}] has the correct state [{}] already", scene['stateItem'], state['nonMatchValue'])
                            else:
                                self.logger.info("Updating scene to item=[{}], value=[{}]", scene['stateItem'], scene['nonMatchValue'])
                                events.postUpdate(scene['stateItem'], scene['nonMatchValue'])
                    except:
                        if self.isStateEqual(items[scene['stateItem']], self.DEFAULT_NON_MATCH_VALUE):
                            self.logger.info("Scene item [{}] has the correct state [{}] already", scene['stateItem'], self.DEFAULT_NON_MATCH_VALUE)
                        else:
                            self.logger.info("Setting item [{}] to value [{}]", scene['stateItem'], self.DEFAULT_NON_MATCH_VALUE)
                            events.postUpdate(scene['stateItem'], str(self.DEFAULT_NON_MATCH_VALUE))

        except:
            self.logger.error("{}", traceback.format_exc())

Testing

If you would like to test this rule then you can setup items which you can control via MQTT. This allows you to simulate fysical button presses and updating the scene from a different rule.

Items

String Scene_Living                             "Scene"                                     { mqtt="<[jarvis:livingroom/scene:state:default], >[jarvis:livingroom/scene:command:*:default]" }

Dimmer kitchenLights                            "kitchenLights"                             <slider>    { mqtt="<[jarvis:light/kitchenlights:state:default], >[jarvis:light/kitchenlights:command:*:default]" }
Dimmer nookLight                                "nookLight"                                 <slider>    { mqtt="<[jarvis:light/nooklight:state:default], >[jarvis:light/nooklight:command:*:default]" }
Dimmer livingRoomLamp                           "livingRoomLamp"                            <slider>    { mqtt="<[jarvis:light/livingroomlamp:state:default], >[jarvis:light/livingroomlamp:command:*:default]" }
Dimmer livingRoomTableLamp                      "livingRoomTableLamp"                       <slider>    { mqtt="<[jarvis:light/livingroomtablelamp:state:default], >[jarvis:light/livingroomtablelamp:command:*:default]" }
Dimmer livingRoomCeilingLights                  "livingRoomCeilingLights"                   <slider>    { mqtt="<[jarvis:light/livingroomceilinglights:state:default], >[jarvis:light/livingroomceilinglights:command:*:default]" }

Dimmer setting_lr_dim_kitchenBrightness         "setting_lr_dim_kitchenBrightness"          <slider>    { mqtt="<[jarvis:light/setting_lr_dim_kitchenbrightness:state:default], >[jarvis:light/setting_lr_dim_kitchenbrightness:command:*:default]" }
Dimmer setting_lr_dim_nookBrightness            "setting_lr_dim_nookBrightness"             <slider>    { mqtt="<[jarvis:light/setting_lr_dim_nookbrightness:state:default], >[jarvis:light/setting_lr_dim_nookbrightness:command:*:default]" }
Dimmer setting_lr_dim_sofaLampBrightness        "setting_lr_dim_sofaLampBrightness"         <slider>    { mqtt="<[jarvis:light/setting_lr_dim_sofalampbrightness:state:default], >[jarvis:light/setting_lr_dim_sofalampbrightness:command:*:default]" }
Dimmer setting_lr_dim_tableLampBrightness       "setting_lr_dim_tableLampBrightness"        <slider>    { mqtt="<[jarvis:light/setting_lr_dim_tablelampbrightness:state:default], >[jarvis:light/setting_lr_dim_tablelampbrightness:command:*:default]" }
Dimmer setting_lr_dim_ceilingLightBrightness    "setting_lr_dim_ceilingLightBrightness"     <slider>    { mqtt="<[jarvis:light/setting_lr_dim_ceilinglightbrightness:state:default], >[jarvis:light/setting_lr_dim_ceilinglightbrightness:command:*:default]" }

Dimmer setting_lr_med_kitchenBrightness         "setting_lr_med_kitchenBrightness"          <slider>    { mqtt="<[jarvis:light/setting_lr_med_kitchenbrightness:state:default], >[jarvis:light/setting_lr_med_kitchenbrightness:command:*:default]" }
Dimmer setting_lr_med_nookBrightness            "setting_lr_med_nookBrightness"             <slider>    { mqtt="<[jarvis:light/setting_lr_med_nookbrightness:state:default], >[jarvis:light/setting_lr_med_nookbrightness:command:*:default]" }
Dimmer setting_lr_med_sofaLampBrightness        "setting_lr_med_sofaLampBrightness"         <slider>    { mqtt="<[jarvis:light/setting_lr_med_sofalampbrightness:state:default], >[jarvis:light/setting_lr_med_sofalampbrightness:command:*:default]" }
Dimmer setting_lr_med_tableLampBrightness       "setting_lr_med_tableLampBrightness"        <slider>    { mqtt="<[jarvis:light/setting_lr_med_tablelampbrightness:state:default], >[jarvis:light/setting_lr_med_tablelampbrightness:command:*:default]" }
Dimmer setting_lr_med_ceilingLightBrightness    "setting_lr_med_ceilingLightBrightness"     <slider>    { mqtt="<[jarvis:light/setting_lr_med_ceilinglightbrightness:state:default], >[jarvis:light/setting_lr_med_ceilinglightbrightness:command:*:default]" }

Here jarvis is the name of my MQTT server so you will need to update this to reflect yours.

Sitemap

sitemap default label="Smarthome"
{
    Frame label="General Scene Rule" {
        Switch    item=Scene_Living        mappings=["0"="Off", "1"="Dim", "2"="Half", "3"="Full", "4"="None"]
        Text      item=Scene_Living
        Slider    item=kitchenLights
        Slider    item=nookLight
        Slider    item=livingRoomLamp
        Slider    item=livingRoomTableLamp
        Slider    item=livingRoomCeilingLights

        Slider    item=setting_lr_dim_kitchenBrightness
        Slider    item=setting_lr_dim_nookBrightness
        Slider    item=setting_lr_dim_sofaLampBrightness
        Slider    item=setting_lr_dim_tableLampBrightness
        Slider    item=setting_lr_dim_ceilingLightBrightness

        Slider    item=setting_lr_med_kitchenBrightness
        Slider    item=setting_lr_med_nookBrightness
        Slider    item=setting_lr_med_sofaLampBrightness
        Slider    item=setting_lr_med_tableLampBrightness
        Slider    item=setting_lr_med_ceilingLightBrightness
    }
}

MQTT.fx test script

An optional test script to for MQTT.fx to automatically change to different scene states.

var Thread = Java.type("java.lang.Thread");

function execute(action) {
    out("Test Script: " + action.getName());

    setSceneDimDefaults();
    setSceneHalfDefaults();
    
    setSceneNumber("0");
    Thread.sleep(2500);

    setSceneNumber("1");
    Thread.sleep(2500);

    setSceneNumber("2");
    Thread.sleep(2500);

    setSceneNumber("0");
    Thread.sleep(2500);

    setSceneNumber("3");
    Thread.sleep(2500);

    setLight("kitchenlights", "20");
    Thread.sleep(200);
    setLight("nooklight", "25");
    Thread.sleep(200);
    setLight("livingroomlamp", "30");
    Thread.sleep(200);
    setLight("livingroomtablelamp", "35");
    Thread.sleep(200);
    setLight("livingroomceilinglights", "40");
    Thread.sleep(2500);

    setLight("kitchenlights", "50");
    Thread.sleep(200);
    setLight("nooklight", "50");
    Thread.sleep(200);
    setLight("livingroomlamp", "50");
    Thread.sleep(200);
    setLight("livingroomtablelamp", "50");
    Thread.sleep(200);
    setLight("livingroomceilinglights", "50");
    Thread.sleep(2500);

    setLight("kitchenlights", "OFF");
    Thread.sleep(200);
    setLight("nooklight", "OFF");
    Thread.sleep(200);
    setLight("livingroomlamp", "OFF");
    Thread.sleep(200);
    setLight("livingroomtablelamp", "OFF");
    Thread.sleep(200);
    setLight("livingroomceilinglights", "OFF");
    Thread.sleep(2500);

    setLight("kitchenlights", "ON");
    Thread.sleep(200);
    setLight("nooklight", "ON");
    Thread.sleep(200);
    setLight("livingroomlamp", "ON");
    Thread.sleep(200);
    setLight("livingroomtablelamp", "ON");
    Thread.sleep(200);
    setLight("livingroomceilinglights", "ON");
    Thread.sleep(2500);

    action.setExitCode(0);
    action.setResultText("done.");
    out("Test Script: Done");
    return action;
}

function setSceneDimDefaults() {
    out("Set Dim scene defaults")
    mqttManager.publish("light/setting_lr_dim_kitchenbrightness", "20");
    mqttManager.publish("light/setting_lr_dim_nookbrightness", "25");
    mqttManager.publish("light/setting_lr_dim_sofalampbrightness", "30");
    mqttManager.publish("light/setting_lr_dim_tablelampbrightness", "35");
    mqttManager.publish("light/setting_lr_dim_ceilinglightbrightness", "40");
}

function setSceneHalfDefaults() {
    out("Set Half scene defaults")
    mqttManager.publish("light/setting_lr_med_kitchenbrightness", "50");
    mqttManager.publish("light/setting_lr_med_nookbrightness", "50");
    mqttManager.publish("light/setting_lr_med_sofalampbrightness", "50");
    mqttManager.publish("light/setting_lr_med_tablelampbrightness", "50");
    mqttManager.publish("light/setting_lr_med_ceilinglightbrightness", "50");
}

function setSceneNumber(scene) {
    switch(scene) {
    case "0": 
            out("Set scene OFF");
            mqttManager.publish("livingroom/scene", scene);
            break;
    case "1": 
            out("Set scene DIM");
            mqttManager.publish("livingroom/scene", scene);
            break;
    case "2": 
            out("Set scene HALF");
            mqttManager.publish("livingroom/scene", scene);
            break;
    case "3": 
            out("Set scene FULL");
            mqttManager.publish("livingroom/scene", scene);
            break;
    }
}

function setLight(item, value) {
    out("Set " + item + " to " + value);
    mqttManager.publish("light/" + item, value);
}

function out(message){
     output.print(message);
}
1 Like

I love seeing my tile being used and adapted! Thanks for sharing! It’s great to see it being made more object oriented too. I definitely had to make some design compromises on the original, but it worked and was happy with it. It’s fun seeing improvements being made.

1 Like

Thank you for sharing the code in the first place!

My current scenes in openHAB 1.x are programmed in the DSL and are too complicated to maintain. Over time I have implemented state changes in different rules making it one big mess (my fault). I love the way you have solved this by using a simple JSON object. I also love the way that the JSON object is used to configure the triggering items of the rule.

JSR223 scripting makes it possible to share code without the code being tied to specific item names. Your rule is a great example of that.

Dear Marcel,

I enjoyed reading your post on separating business logic from rules and started with your General Scene Rule. I reduced the scenario and adapted it to only one of my items to be toggled between two states upon a change of the scene number.

Unfortunately, I only got the following error message upon loading the rule (and storing the associated base class scene.py in the python community library):

2021-04-16 20:32:10.154 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/python/personal/sceneLivingRoom.py': ImportError: No module named smarthome in <script> at line number 1

Probably my OpenHAB 3.0 setup is not yet finished to properly run your code. I followed the guide to install Jython and the Helper Libraries: Ivan’s Helper Libraries - OH3, Python, JavaScript

The Hello World example from OpenHAB Scripters works fine.

Does anybody has a hint what is missing?

Best regards,
Peter

Peter, the issue is that you are trying to use code for OH2 in OH3. You will need to make the same changes to Marcel’s code as I have done in the Helper Libraries.

Any references to org.eclipse.smarthome need to be changed to org.openhab. You can look in the Core libraries to see the try/except pattern used to support both OH2 and 3 at the same time if you’re curious.

1 Like

Dear Michael,

Thank you very much for your hint. After having a look into the core libraries, I changed the import section as follows:

from core.jsr223.scope import TriggerBuilder
from core.jsr223.scope import Configuration
from core.jsr223.scope import itemRegistry

from core.jsr223.scope import items

from core.jsr223.scope import scriptExtension
from core.jsr223.scope import SimpleRule
from core.jsr223.scope import events

Now the General Scene Rule also works with OpenHAB 3.0.

Best regards,
Peter

1 Like

That’s not quite what I meant, but interesting that all of those things are available from the different script scopes…

If the code in that file gets triggered by anything other than a rule you may run into issues.

I haven’t upgraded to openHAB 3.0 yet so haven’t run into this issue yet. I really appreciate that you posted a solution to the problem that you faced!

Thanks Michael for pointing Peter in the right direction and fixing the compatibility issues and making them available.

1 Like

Dear Michael,

CrazyIvan359 Michael Murton
That’s not quite what I meant, but interesting that all of those things are available from the different script scopes…

I saw the chain of try / except in lib/python/core of the JSR223 helper libraries, but the imports there did not concern classes like TriggerBuilder, itemRegistry etc.

Nevertheless, I was able to identify from core.jsr223.scope import ... for TriggerBuilder, itemRegistry etc. in various files in lib/python/core, but no other source. So I have no idea what other package would be better suited as I could not find any other way. Unfortunately, my skills in working with libraries and especially Java are quite low so that I might have searched in the wrong place.

Best regards,
Peter

Hi @marcel_erkel,

Thank you for your thorough description.
I have a real hate-love relationship with the Xtend-based Rules DSL. Main problem: you cannot separate the business logic, meaning I have to repeat a lot of code in many rules files - of course causing inconsistencies over time…
An now you show how easy it can be done by jumping on the Jython wagon. :slightly_smiling_face:

Before migrating from openHAB 2.5.x to 3.x I’ll want to migrate all my rules files from Xtend to Jython. (Basically I’m fed up with Xtend and after an early upgrade attempt from OH2 to OH3 and seeing the many compilation errors I quickly put the SSD card image from before the update back in the RPi…)

Thus thanks for the inspiration! :+1:

Thanks Robert! I’m still on 2.5.x as well. It’s on my todo list to move to 3.x but since 2.5.x is running fine I keep prioritizing other stuff that I want to do as well :slight_smile: