General Scene Rule for jsr223

I was the OP in this monster thread but never really found a setup that I was happy with despite lots of good ideas shared.

For a few months, I’ve been using a general scene rule that I wrote in javascript for jsr223. It has been working quite well and is stable, so I figured I would share it. Before writing this, I couldn’t find any way to easily write a “scene” that could set various different items to arbitrary (read: not all the same) values. I ended up coding specific rules for each scene, and it was very tedious. I also wanted the possibility of storing an item’s level for a scene in a virtual item, or to define it with a specific value that never changes. This rule allows both of those things.

Here’s the code, and then I’ll explain a little bit underneath.


var scenes = [
  {
    stateItem: "Scene_Living",
    states: [
      {
        stateValue: "0", // OFF
        targetValues: [
          { item: "kitchenLights", value: "OFF" },
          { item: "nookLight", value: "OFF" },
          { item: "livingRoomLamp", value: "OFF" },
          { item: "livingRoomTableLamp", value: "OFF" },
          { item: "livingRoomCeilingLights", value: "OFF" }
        ] 
      },
      {
        stateValue: "1", // DIM
        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", // HALF
        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", // FULL
        targetValues: [
          //{ item: "kitchenLights", value: "ON" },
          { item: "kitchenLights", value: "ON" },
          { item: "nookLight", value: "ON" },
          { item: "livingRoomLamp", value: "ON" },
          { item: "livingRoomTableLamp", value: "ON" },
          { item: "livingRoomCeilingLights", value: "ON" }
        ] 
      }
    ],
    nonMatchValue: "4"
  }
];






var generalSceneRule = new Rule(){
    getEventTrigger: function(){
    var triggers = [];
    var updateItems = [];
  
    for(var i = 0; i < scenes.length; i++){
      var currentScene = scenes[i];
      oh.logInfo("generalSceneRule", "Adding CommandEventTrigger [{}]", scenes[i].stateItem);
      triggers.push(new CommandEventTrigger(scenes[i].stateItem, null));
      
      for(var j = 0; j < currentScene.states.length; j++){
        var currentState = currentScene.states[j];
        
        for(var k = 0; k < currentState.targetValues.length; k++) {
          var currentTargetValue = currentState.targetValues[k];
          
          // add this targetValue to updateItems array if it's not there
          var alreadyAdded = false;
          for(var m = 0; m < updateItems.length; m++){
            if(updateItems[m] == currentTargetValue.item){
              alreadyAdded = true;
              break;
            }
          }
          if(!alreadyAdded){
            //oh.logInfo("generalSceneRule", "Adding item [{}] to updateItems", currentTargetValue.item);
            updateItems.push(currentTargetValue.item);
          }
        }
      }
    }
    
    for(var i = 0; i < updateItems.length; i++){
      triggers.push(new ChangedEventTrigger(updateItems[i]));
    }
    
    return triggers;
  },
  execute: function(event){
    oh.logInfo("generalSceneRule", "event=[{}]", event);
    var triggerItem = event.getItem().getName();
    
    // Scene was triggered
    if(event.triggerType == "COMMAND"){
      var command = event.getCommand();
      oh.logInfo("generalSceneRule", "command=[{}]", command);
    
      // Get the correct Scene
      for(var i = 0; i < scenes.length; i++){
        var currentScene = scenes[i];
        if(currentScene.stateItem == triggerItem){
          // trigger the states
          for(var j = 0; j < currentScene.states.length; j++){
            var currentState = currentScene.states[j];
            
            oh.logInfo("generalSceneRule", "stateValue=[{}]", currentState.stateValue);
            if(currentState.stateValue == command){
              oh.logInfo("generalSceneRule", "got a matching stateValue");
              
              for(var k = 0; k < currentState.targetValues.length; k++){
                var currentTargetValue = currentState.targetValues[k];
                
                var commandToSend = currentTargetValue.value;
                oh.logInfo("generalSceneRule", "Sending command. currentTargetValue.value=[{}]", currentTargetValue.value);
                // if target value is an item, get its state
                
                try{
                  var item = ir.getItem(currentTargetValue.value);
                  oh.logInfo("generalSceneRule", "Got an item");
                  commandToSend = item.state.toString();
                }
                catch(e)
                {
                  oh.logInfo("generalSceneRule", "No item exists with name [{}], treating as command.", currentTargetValue.value);
                }
                
                oh.logInfo("generalSceneRule", "Sending command. Item=[{}], Command=[{}]", currentTargetValue.item, commandToSend);
                be.sendCommand(currentTargetValue.item, commandToSend);
              }
            }
          }
          
          break;
        } 
      }
      
      }
    else if(event.triggerType == "CHANGE"){
      oh.logInfo("generalSceneRule", "Got CHANGE event. event.getItem().getName()=[{}]", event.getItem().getName())
      
        for(var i = 0; i < scenes.length; i++){
          var currentScene = scenes[i];
          //oh.logInfo("generalSceneRule", "stateItem=[{}]", currentScene.stateItem);
          
          var foundStateMatch = false;
          for(var j = 0; j < currentScene.states.length; j++){
            var currentState = currentScene.states[j];
            //oh.logInfo("generalSceneRule", "    stateValue=[{}]", currentState.stateValue);
            
            
            var possibleMatch = false;
            for(var k = 0; k < currentState.targetValues.length; k++){
              var currentTargetValue = currentState.targetValues[k];
              //oh.logInfo("generalSceneRule", "        item=[{}]", currentTargetValue.item);
              
              if(event.getItem().getName() == currentTargetValue.item){
                //oh.logInfo("generalSceneRule", "        Possible state match stateItem=[{}], stateValue=[{}], item=[{}]", currentScene.stateItem, currentState.stateValue, currentTargetValue.item);
                
                possibleMatch = true;
                break;
              }
            }
            
            if(possibleMatch){
              //oh.logInfo("generalSceneRule", "            Possible state match, looping through items in stateItem=[{}], stateValue=[{}]", currentScene.stateItem, currentState.stateValue);
              var currentStateIsMatch = true;
              for(var k = 0; k < currentState.targetValues.length; k++){
                var currentTargetValue = currentState.targetValues[k];
                var itemTargetValue = "";
                try{
                  var item = ir.getItem(currentTargetValue.value);
                  itemTargetValue = item.state;
                }
                catch(e)
                {
                  //oh.logInfo("generalSceneRule", "            No item exists with name [{}], treating as command.", currentTargetValue.value);
                  itemTargetValue = currentTargetValue.value;
                }
                
                var itemState = ir.getItem(currentTargetValue.item).state.toString();
                
                
                //oh.logInfo("generalSceneRule", "            item=[{}], item.state=[{}], itemTargetValue=[{}]", currentTargetValue.item, itemState, itemTargetValue);
                
                if(itemState == itemTargetValue || 
                (itemState == "0" && itemTargetValue.toString().toUpperCase() == "OFF") ||
                (itemState == "100" && itemTargetValue.toString().toUpperCase() == "ON")) 
                {
                  //oh.logInfo("generalSceneRule", "            This item was a match with [{}], continuing.", itemState);
                }
                else { 
                  //oh.logInfo("generalSceneRule", "            This item was a not a match. Breaking.");
                  currentStateIsMatch = false;
                  break;
                }
              }
              
              if(currentStateIsMatch){
                oh.logInfo("generalSceneRule", "            After all items, this state is a match. Setting item [{}] to value [{}].", currentScene.stateItem, currentState.stateValue);
                foundStateMatch = true;
                be.postUpdate(currentScene.stateItem, currentState.stateValue);
              }
              else{
                //oh.logInfo("generalSceneRule", "            After all items, this state is not a match. item [{}] value [{}].", currentScene.stateItem, currentState.stateValue);
              }
            }
            else{
              //oh.logInfo("generalSceneRule", "            Not a match. stateItem=[{}], stateValue=[{}]", currentScene.stateItem, currentState.stateValue);
            }
            
          }
          
          // Didn't find a state match, set scene value to nonMatchValue if there is one
          if(!foundStateMatch){
            oh.logInfo("generalSceneRule", "No scene matches. currentScene.nonMatchValue=[{}]", currentScene.nonMatchValue);
            if(currentScene.nonMatchValue) {
              oh.logInfo("generalSceneRule", "Updating scene to item=[{}], value=[{}]", currentScene.stateItem, currentScene.nonMatchValue);
              be.postUpdate(currentScene.stateItem, currentScene.nonMatchValue);
            }
          }
        }
    }
    
  }
};





function getRules(){return new RuleSet(
  [
    generalSceneRule
  ]);
}     

Scene Configuration Array

The “scenes” variable in this code is an array of objects. Each object has:

  • A “stateItem” property which is the name of the openHAB item which stores the scene state. This would be the item you send a command to to change the scene.
  • A “states” property which is an array of objects. Each object in this array is one of the states that a scene could be in.
  • A “nonMatchValue” property, which is the value the rule will set the “stateItem” to if the states of the items in the scene do not match any of the specified “states”

State Configuration Items

Each item in the “states” array has the following properties:

  • A “stateValue” property. If the “stateItem” gets modified and receives the command for the value set in the “stateValue” property for a scene, all items in that scene will get set to the value specified in the “targetValues” array.
  • A “targetValues” property, which is an array of item settings for the scene. Each item object in this array contains an “item” property which specifies the name of the openHAB item, and a “value” property which specifies the value to set the “item” to. The “value” can contain either a specific command like “OFF” or the name of an openHAB item which contains the state like “setting_lr_dim_kitchenBrightness”. The rule is smart enough to determine which option is specified and act accordingly. You can add an arbitrary number of items into each “targetValues” array.

Rule Triggers

The rule works like this: when the rule file is loaded, the “getEventTrigger” function loops through all of the items in the “scenes” array, and adds a trigger for each “stateItem” and each of the items specified in any of the “targetValues”. After it completes, the rule will run when there is a change to any item which is part of a scene in the “scenes” array.

Changes to a Scene’s "stateItem"

When the rule is triggered by a command to a scene’s “stateItem” it locates the rule in the “scenes” array that has that item as the “stateItem”, and then fines the state in the “states” array whose “stateValue” matches the command given. If it finds one, it sets all of the items in that state’s “targetValues” array to the value specified in the “value” option for that “item”. If “value” is an openHAB item name, it sends the state of that item as a command. Otherwise it sends the “value” string as a command.

Changes to an Item in a State’s “targetValues” Array

When the rule is triggered by a change to any of the items in a scene, the rule loops through all of the “states” within all of the scenes to find any scenes that incorporate the changed item. If it finds any, it checks each of them against the current state of items. If all of the items in the “targetValues” array of a scene state match the current values of the actual items, then it updates the “stateItem” for that scene to the “stateValue” of the matching item in the “states” array. If none of the items in the “states” array match every “item”, then the “nonMatchValue” (if it exists) will be applied to the “stateItem”.

For an example of the previous paragraph, imagine I had set “Scene_Living” to “3” and all of the lights are 100% on according to that state in the scenes array. If I change “kitchenLights” to “OFF”, it will look through the “states” array, and not find any state with “kitchenLights” “OFF” and every other item “ON”. So the “Scene_Living” item gets updated to the “nonMatchValue” of “4”, which I have mapped to display the string “Custom” in the sitemap. If I then proceed to set all items to “OFF”, when the last item is changed, the rule will see that the first state object matches all of the items by having “OFF” values. It will set the “Scene_Living” item to the state’s “stateValue”, which is “0”. The sitemap will show “Scene_Living” with the value “0”, which I have mapped to the display string “Off”.

I hope this is helpful! It’s been very nice and stable for me!

7 Likes

Looks really amazing! What do I have to do to get it running in OH2?

Last I heard, jsr223 wasn’t available for openhab2. I think you just need
to wait unfortunately.

The OH2 JSR223 PR has been merged, but I don’t know if it’s included in the latest stable release or not.

Thanks Steve! Is there a way for me to find out if it is included in stable or any snapshot release?

It would definitely be in a snapshot release. I’m not sure how to determine if it’s in a stable release.

It is definitely included in the 2.1.0-SNAPSHOT Build #924 as of yesterday. Here is my writeup how I set up my jython on OH2. I’m not shure if everythin written there is absolutely necessary, but it seems to work. :wink:

Great! I will test it the next days. Many thanks @ralle!

Thank you very much, that is really something I am missing in OH.
This is a very nice solution for now, but I think there really should be something more easy in OH to define real scenes.
The way I define scenes is that it should at any time give feedback about its actual status depending of the status of its items.
Maybe one could start a development in that direction similar to the rule editor in paper UI ?
(I am not a developer, I cannot do that)
Feature Request: Scene-Editor

What do I have to do to set this up?
Does the code go into a rules file?
Is there any additional code needed?

You need to install the jsr223 addon and put it in a .js script file in the scripts directory. Geting jsr223 installed is beyond the scope of my ability to help right now, but there are good instructions for openhab 1. https://github.com/openhab/openhab1-addons/wiki/Jsr223-Script-Engine

People hinted above that there is an addon working for openhab2. I tried installing it but couldn’t quite get it working. There will probably be better instructions in time.

Once jsr223 is running, you just plug drop the code into a .js file. All you should have to modify is the “var scenes” definition based on what scenes you want to set up and what items you want incorporated in them.