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