UI Scene Widget and Control System

EDIT: I have now converted the components of this system into rules and a widget available in the marketplace. The information below on usage and general concept is still correct, but installation is simplified and mediated by the marketplace and the dependencies on helper libraries and custom library have been removed. The creation of the proxy items for the scenes and the widget (see: Setup) is still required.

Marketplace links

I’ve had various questions about the system I use for UI-based control of scenes in OH3. I’ve also recently taken the time to update/streamline the system so I thought I’d put it all down in one place here. There are enough moving parts that at the moment I’m not sure how to best get this into the marketplace so I’m going to put here in the tutorials section for now.

What is it?

This is a fully integrated system for the control and general activation of scenes: groups of associated item states that should all result from a single trigger. It is not difficult to setup basic scenes in OH, that is one of the most fundamental uses of rules, but having scenes based entirely in rules does have a few downsides:

  1. You have to edit the rule to change something about the scene configuration
  2. You have to look at the rule code to even see what the current configuration is
  3. Only users with the experience/comfort with writing rules and access to the system administration can create or modify rules-based scenes

The basic layout for this system has 5 parts:

  1. A custom MainUI widget which shows all the scenes you have configured, allows enabling/disabling of the scenes and configuration of all the items and item states associated with the scene
  2. One rule that handles all the actual modification of the metadata where the scene information is stored
  3. One external library script that handles converting scene metadata into item commands
  4. One basic rule template that calls the external library script and can be used as the basis for all the rules that will trigger scenes
  5. The helper libraries (this is, of course, not strictly necessary, you could convert all the rules and scripts to do the metadata actions from scratch, but that’s not really recommended)

Setup

This system uses proxy items (Type: Switch) to control each scene. By doing this there is one item that holds all the metadata for one scene but also gives the user control over whether the scene is currently enabled or disabled. I find this useful for scenes where the trigger is some daily event or cron time (such as a Wake Up scene) which doesn’t need to occur when you are not at home. Each proxy item that needs to act as a scene controller only needs to have a non-semantic tag, Scene Controller added to it. No other configuration is required. I Give all of my controllers a prefix on the label such as Scene Contoller - XXXX but this is not necessary. If you do, use a hyphen in the label and the widget will only display text after the hyphen, otherwise the widget will show the entire item label.

One additional proxy item (Type: String) is required to carry information between the widget and the configuration rule. In my sample configuration, this item is named Rule_SceneModify. This can be configured in the widget, but that is the default name. If you change this default name you will also need to change the triggers and conditions of the scene modification rule (see below).

The Widget

The centerpiece of this is the UI widget. The widget starts in a basic form which displays each of the available scene controllers in a list and displays whether they are currently enabled or disabled.

image

Each of these is an accordion and can be expanded to show some details of the scene.

This is all fairly straightforward, the only thing that might require comment is that the badge on the devices line shows the number of devices that are currently programmed for that scene, and this devices line is also a accordion that can be further expanded to show those devices and their settings.

Adding a Device to a Scene

Let’s start with putting devices into the scene. The widget will record the current state of each item you add. So, the first step will be to make sure all the items you are going to want in the scene are currently set the way you want them (basically, set up the actual environment you want). Then press the plus icon on the Add a new device... line under the scene you want to add the items to. This will bring up an item selection popup.

You can toggle each of the buttons at the top to show all the items of a particular type, or you can just type in the search box to bring up items filtered by item name. When you locate the item you want, just press the red arrow. The item you select will not disappear from the list, but in the background you should see the item appear in the widget (or at least see the number of items in the badge increment). Do this for each item you want. (If you are interested in the main trick behind this item selection popup, see this post here).

Modify Devices

In the fully expanded list, each device has two buttons associated with it. The trash can, naturally, removes the device from the scene (it does not delete the actual item, it only removes the metadata in the scene controller). You should see the item disappear immediately.

deleteitem

The circular arrows allow you to update the state of the item if you decide that the scene is not quite to your liking. For example, if you have a dimmer in the scene set to 60, but decide that’s too bright, just change the dimmer item to some lower setting and then press the circular arrows to refresh the state in the scene definition to the new setting. You should see the state badge change accordingly.

refreshitem

Widget Code

Here's the full code for the widget
uid: jag_scene_controllers
tags: []
props:
  parameters:
    - context: item
      default: Rule_SceneModify
      description: Proxy item to run scene modification rule
      label: Rule Item
      name: ruleItem
      required: false
      type: TEXT
      groupName: general
  parameterGroups:
    - name: general
      label: General settings
component: oh-list-card
config:
  accordionList: true
  title: Scene controllers
slots:
  default:
    - component: oh-repeater
      config:
        accordionList: true
        fetchMetadata: ActiveItems
        for: controller
        fragment: true
        itemTags: Scene Controller
        key: =Math.random() + items[props.ruleItem].state
        sourceType: itemsWithTags
      slots:
        default:
          - component: oh-list-item
            config:
              badge: "=(items[loop.controller.name].state == 'ON') ? 'Enabled' : ((items[loop.controller.name].state == 'OFF') ? 'Disabled' : 'Unknown')"
              badgeColor: "=(items[loop.controller.name].state == 'ON') ? 'green' : ((items[loop.controller.name].state == 'OFF') ? 'red' : 'yellow')"
              title: =loop.controller.label.split('-').at(-1)
            slots:
              accordion:
                - component: oh-list
                  slots:
                    default:
                      - component: oh-list
                        config:
                          accordionList: true
                        slots:
                          default:
                            - component: oh-toggle-item
                              config:
                                class:
                                  - margin-left
                                item: =loop.controller.name
                                title: "=(items[loop.controller.name].state == 'OFF') ? 'Enable' : 'Disable'"
                            - component: oh-list-item
                              config:
                                badge: "=(!!loop.controller.metadata && !!loop.controller.metadata.ActiveItems && !!loop.controller.metadata.ActiveItems.config) ? JSON.stringify(loop.controller.metadata.ActiveItems.config).slice(1,-1).split(',').length : '0'"
                                class:
                                  - margin-left
                                title: Devices
                              slots:
                                accordion:
                                  - component: oh-repeater
                                    config:
                                      for: activeItem
                                      fragment: true
                                      in: =JSON.stringify(loop.controller.metadata.ActiveItems.config).slice(1,-1).split(',')
                                    slots:
                                      default:
                                        - component: oh-list-item
                                          config:
                                            badge: =loop.activeItem.split(':')[1].slice(1,-1)
                                            class:
                                              - margin-left
                                            title: =loop.activeItem.split(':')[0].slice(1,-1)
                                          slots:
                                            after:
                                              - component: f7-row
                                                config:
                                                  style:
                                                    padding: 10
                                                slots:
                                                  default:
                                                    - component: oh-link
                                                      config:
                                                        action: command
                                                        actionCommand: =loop.controller.name + ',' + loop.activeItem.split(':')[0].slice(1,-1) + ',DELETE'
                                                        actionItem: =props.ruleItem
                                                        iconF7: trash_circle_fill
                                                        style:
                                                          padding-left: 15px
                                                    - component: oh-link
                                                      config:
                                                        action: command
                                                        actionCommand: =loop.controller.name + ',' + loop.activeItem.split(':')[0].slice(1,-1) + ',REFRESH'
                                                        actionItem: =props.ruleItem
                                                        iconF7: arrow_2_circlepath_circle_fill
                                                        style:
                                                          padding-left: 15px
                            - component: oh-list-item
                              config:
                                class:
                                  - margin-left
                                title: Add a new device...
                              slots:
                                after:
                                  - component: oh-link
                                    config:
                                      action: variable
                                      actionVariable: addToController
                                      actionVariableValue: =loop.controller.name
                                      iconF7: plus_circle_fill
                                      popupOpen: .popup.add-item-pop
    - component: f7-popup
      config:
        class:
          - add-item-pop
        style:
          overflow-y: scroll
      slots:
        default:
          - component: f7-card
            config:
              title: "='Add Items to Scene Controller: ' + vars.addToController"
            slots:
              default:
                - component: oh-list
                  config:
                    virtualList: true
                  slots:
                    default:
                      - component: f7-segmented
                        config:
                          raised: true
                        slots:
                          default:
                            - component: oh-repeater
                              config:
                                for: filterType
                                fragment: true
                                in:
                                  - Switch
                                  - Dimmer
                                  - Player
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: ='filter'+loop.filterType
                                      actionVariableValue: "=(!!vars['filter'+loop.filterType]) ? false : true"
                                      active: =vars['filter'+loop.filterType]
                                      text: =loop.filterType
                      - component: f7-segmented
                        config:
                          raised: true
                        slots:
                          default:
                            - component: oh-repeater
                              config:
                                for: filterType
                                fragment: true
                                in:
                                  - Roller
                                  - Color
                                  - Number
                              slots:
                                default:
                                  - component: oh-button
                                    config:
                                      action: variable
                                      actionVariable: ='filter'+loop.filterType
                                      actionVariableValue: "=(!!vars['filter'+loop.filterType]) ? false : true"
                                      active: =vars['filter'+loop.filterType]
                                      text: =loop.filterType
                      - component: f7-row
                        config:
                          style:
                            align-items: center
                        slots:
                          default:
                            - component: oh-icon
                              config:
                                color: gray
                                icon: f7:search
                            - component: oh-input
                              config:
                                clearButton: true
                                outline: true
                                placeholder: "  type to search"
                                style:
                                  flex-grow: 10
                                type: text
                                variable: filterText
                      - component: oh-repeater
                        config:
                          filter: "((!!vars.filterSwitch && loop.ohItem.type=='Switch') || (!!vars.filterPlayer && loop.ohItem.type=='Player') || (!!vars.filterDimmer && loop.ohItem.type=='Dimmer') || (!!vars.filterRoller && loop.ohItem.type=='Rollershutter') || (!!vars.filterColor && loop.ohItem.type=='Color') || (!!vars.filterNumber && loop.ohItem.type.substring(0,6)=='Number') || (!!vars.filterText && loop.ohItem.name.includes(vars.filterText))) ? true : false"
                          for: ohItem
                          fragment: true
                          itemTags: ","
                          sourceType: itemsWithTags
                        slots:
                          default:
                            - component: oh-list-item
                              config:
                                after: =loop.ohItem.state
                                title: =loop.ohItem.label + ' (' + loop.ohItem.name + ')'
                              slots:
                                after:
                                  - component: oh-link
                                    config:
                                      action: command
                                      actionCommand: =vars.addToController + ',' + loop.ohItem.name + ',ADD'
                                      actionFeedback: =loop.ohItem.name + ' (' + loop.ohItem.state + ') added to ' + vars.addToController
                                      actionItem: =props.ruleItem
                                      iconF7: arrow_right_circle_fill
                                      style:
                                        padding-left: 15px

If you look through the code you can see that each one of the action buttons simply creates a comma separated string with the name of the scene controller item, the item to be controlled, and the action to be performed. This string is then sent as the command to the proxy item that is connected to our scene modification rule. Changing the state of this proxy item is what will trigger that rule to write our metatdata changes.

The Rules

I’ve written the rules in the built-in javascript. The main scene modification rule and all the rules to trigger individual scenes can be done in the MainUI. There is one custom script that will need to be in a file and run as a loaded external library.

Again, because this system is based entirely on metadata, these rules rely heavily on the helper libraries. I admit, I don’t know what the status of the update to the javascript helper libraries is at the time of writing this. I’m still using the older version of the libraries because I pretty much only use them for manipulating metadata and those portions of the older libraries still work just fine.

Scene Modification

This is the rule that runs whenever a scene setting is changed via the widget. It is also the rule that must have it’s triggering item and conditional item changed if you do not use the default name for the widget proxy item.

Scene Modification Rule
configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: Rule_SceneModify
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: Rule_SceneModify
      state: None
      operator: "!="
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >+
        var logger =
        Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.ModifyScene');


        var CONF_PATH = java.lang.System.getenv('OPENHAB_CONF');

        load(CONF_PATH + '/automation/lib/javascript/core/metadata.js');


        //Collect all info from scene modify item state

        var modifyInfo = items[event.itemName].toString().split(',')

        var controllerName = modifyInfo[0];

        var targetName = modifyInfo[1];

        var modification = modifyInfo[2];


        //Take metadata write/delete action based on modification

        if (modification == 'ADD' || modification == 'REFRESH') {
          //Format metadata to write
          var targetState = items[targetName].toString();
          var targetData={};
          targetData[targetName]=targetState;

          //Write metadata
          set_metadata(controllerName,'ActiveItems',targetData, null, false);

          
        } else { 
          //Remove metadata
          remove_key_value(controllerName,'ActiveItems', targetName);
        }


        //Cleanup

        logger.info([modification,targetName,'in',controllerName].join(' '));

        events.postUpdate(event.itemName,'None');

    type: script.ScriptAction

Looking at the script itself you can see that all the metadata for this setup is stored in a namespace named ActiveItems. An item’s name is a key within this namespace and the state to set it to is the value of that key. The script first parses the proxy item’s state to get the appropriate controller, target item, and action and then either writes these data or removes them from the namespace as required. At the end of the run the rule clears out the information held in the proxy item state to avoid any duplication of actions.

Scene Activation Script

If you have installed the helper libraries properly then you have an automation folder tree in your OH conf folder. This folder tree contains folders to place the personal scripted automation libraries that you may need to access from your rules: /automation/lib/javascript/personal in the case of js libraries. In this folder, I have created a file automation control.js with the following content:

/*
This library provides functions for the automation control system.
*/

var logger = Java.type('org.slf4j.LoggerFactory').getLogger('org.openhab.rule.automationControl');

var CONF_PATH = java.lang.System.getenv('OPENHAB_CONF');
load(CONF_PATH + '/automation/lib/javascript/core/metadata.js');

'use strict';

(function(context) {
  'use strict';
  context.run_controller = function (controllerID, undoSettings) {
      if (undoSettings === undefined) undoSettings = false;
      var metaList = get_metadata(controllerID, 'ActiveItems');
      if (metaList && undoSettings) {
        var configList = metaList['configuration'];
        for (var id in configList) {
          context.events.sendCommand(id,(configList[id] == 'ON') ? 'OFF' : ((configList[id]) == 'OFF' ? 'ON' : configList[id]));
        };
      } else if (metaList) {
        var configList = metaList['configuration'];
        for (var id in configList) {
          context.events.sendCommand(id,configList[id]);
        };
      } else {
        logger.info(['No items currently controlled by ',controllerID].join(''));
      };
  };
})(this);

This library, when loaded into a rule, will make the run_controller function available. When run_controller is called with the name of one of the scene controller items, it reads the ActiveItems metadata in the controller item to identify all the other items to be changed and then runs through those items sending the appropriate command.

There’s an extra feature you can see in this script that I haven’t mentioned yet. For some scenes, it makes sense to be able to turn the scene on, and then simply reverse the effects of that scene when it is over. So, there is a second, optional, parameter to the run_controller function, undoSettings. When this second parameter is passed a true value, then instead of sending the commands stored in the metadata, for any switch item, the opposite command is sent. The best example of this is the “scene” for my wife’s seedling table. She likes the grow lights and the seedling warmer mats to be turned on at sunrise and off at sunset. I could set up two different scenes: one with all the ON states and one with all the OFF states. Instead, I just have a single rule which runs on the astro daylight event triggers. This rule calls the run_controller function with event.event=='END' in the second parameter. Now if it is START of daylight undoSettings is false and the scene is run as specified to turn on all the various devices, but if it is END of daylight then undoSettings is true and every item that has an ON state in the metadata gets an OFF command instead.

Scene Triggering Rule

The very last step is that every scene needs a scene triggering rule (such as the plant table rule mentioned above). With the automation control library, these are now very simple and straightforward to set up in the MainUI. Each rule simple needs whatever trigger you want for your scene, buttons, presence, astro, etc. As a rule condition (“But only if…”), the switch state of the scene controller should be checked so that the rule doesn’t run if the scene is disabled, and then the rule action is a script with only two pieces. Load the automation control library and call run_controller with the name of the scene controller.

Scene Rule Example
configuration: {}
triggers:
  - id: "1"
    configuration:
      itemName: AutomationTime
      state: NIGHT
    type: core.ItemStateChangeTrigger
conditions:
  - inputs: {}
    id: "3"
    configuration:
      itemName: Auto_LightsOut_Control
      state: ON
      operator: =
    type: core.ItemStateCondition
actions:
  - inputs: {}
    id: "2"
    configuration:
      type: application/javascript
      script: >-
        var CONF_PATH = java.lang.System.getenv('OPENHAB_CONF');

        load(CONF_PATH + '/automation/lib/javascript/personal/automation control.js');


        run_controller('Auto_LightsOut_Control')
    type: script.ScriptAction

Conclusion

As I stated in the beginning, this is a lot of extra complexity when a scene can simply be rendered as a series of item commands in a single rule. For many users, however, I hope this system helps provide one of the feature sets that that seems to be a common expectation, an easy to use scene system that integrates with the UI. If not, then at least as a tutorial, I hope this provides some examples of effective ways to integrate widgets and rules, use metadata, and personal library scripts.

14 Likes

Unfortunately the marketplace doesn’t yet support bundles of widgets and templates. It’s pretty much one-to-one (one marketplace entry for one rule template or ui widget). I’ve been breaking mine up into smaller reusable parts and posting each part individually (e.g. Alarm Clock Rule and To Today rule templates). Where possible I try to make the widget/rule templates more generic so that they are useful and usable on their own outside of the original intent (e.g. using Alarm Clock and To Today rule templates to implement a simple Time of Day). Then I post a tutorial showing how to use those (and associated UI widgets) to build up the more complex/comprehensive solution. The marketplace posting focuses on documenting that one thing with a link to the comprehensive example.

The widget appears pretty complete and installable as is so that would be one post to the UI category. I’d do that even if you don’t create the Rule templates. It’s so much easier for users (and tutorial writers) to just say “go install this from the marketplace” than to deal with copy and paste errors that will inevitably occur.

For the rules, I’d make the rule triggers Items a parameter so the users can select and use their own names. I’d present them, and perhaps adjust them a little to make it really focused on the one job of updating and refreshing the metadata to record the scene and reading that metadata and restoring the scene.

While it’s unlikely, I can envision someone wanting to manually hard code the scene metadata so they’d only need the one rule, for example.

However, it does depend on the installation of the Helper Libraries. There is nothing that requires one to make a rule template stand alone but I personally prefer to do so. So I would probably bring in the functions from metadata.js into the template itself rather than requiring the installation of the helper libraries which is going to be too big of a hurdle for many users who will be installing templates from the marketplace (see my Debounce rule template where I actually copied in my Timer Manager class and part of my time_utils straight into the code rather than importing them for example). That also means your library above would have to be put inline too. Given that your second rule just loads the script and runs it that isn’t really as big of a deal though.

Sometime soon I hope there will be a way to install stuff like the Helper Libraries as an add-on also. But until that happens I want to make my templates stand alone. I also hope to be able to post to the marketplace a bundle so one can have more than one rule template and perhaps the associated ui widgets too in one posting.

Anyway, there is nothing wrong with keeping this posted as is as a tutorial. I only offer the above in case you do want to convert it to the marketplace. This looks really powerful, especially for those users who are low code/no code openHAB users. I can see this being used in conjunction with the Alarm Clock rule template to drive scenes based on DateTimes, the timeline widget to define those times, Ephemeris Time of Day (soon to be rule template), etc.

1 Like

Very slick Justin! :+1:
Because of the UI interface, I could totally see something like this becoming part of the UI. Like scene controller with interface just like rules or pages in the settings where it is ‘no scenes yet’, click the big blue dot, create scene, pick items from list, just like rules.

yes

This pretty much right inline with what I was thinking would need to be done. I didn’t think I was going to have time for this in the near future, but now that I’m suddenly home for the day with the plumber and the flood mitigation company I might just be able to find the time to make these conversions today. :roll_eyes:

Look at the posted example rule templates. Sadly that right now will be the best documentation.

Testing is a pain right now too. You have create the forum post, add the “published” tag, and then you can install the template through MainUI. I know of no other way to install a template right now (I think I opened an issue for this, I know for sure that Yannick and I talked about it).

But as much of a pain creating the template is, I’m expecting the elimination of needing to help with copy/paste/edit errors will more than make up for it. And it will get better.

This looks amazing! This is something that I kinda expected when I saw the scene buttons in the OH3 preview. And since they are missing, I made a very simple rule based implementation… Which is quite lack luster due to the reasons that you listed above.

Thanks so much, I’ll need to implement this over the winter :slight_smile:

I have now (successfully, I think) converted this to the marketplace, so it should be very easy to implement. You might not even have to wait!

See the new links in the initial post.

3 Likes

Hi,
I just tried to install your Scene-widget, but after following the whole tutorial I still get an empty box with the title “Scene Controllers” in my page. No buttons or anything. The helper libraries seems to work based on the fact that I can force trigger the scripts and it no longer complains about missing libs (it did earlier, but that has been sorted now). Any ideas what I have missed or misunderstood?

Kind regards,
Henrik

The widget populates automatically regardless of the state of the rules (rules are necessary to run the scene and to modify the scene with the widget, but if you have properly configured scene items then they should be found by the widget no matter what).

The first thing the widget does is loop through all Items with the Scene Controller tag. So make sure you have properly added that tag to at least one Switch Item. The tags are case sensitive and make sure you have the space as well. As long as that is true the item will be in the widget list. On the items details page it should look like this:

As for the rules, they may be working properly once you get the items configured. But, if you’re on the OH3.2 release, you can also get all three pieces directly form the marketplace and those are newer versions that don’t require the helper libraries which may fix whatever lingering rules issues you are experiencing.

1 Like

Thank you Justin! I added the tag, but I must have failed saving it! I re-added the tag and now it works!