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

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