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

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