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:
- You have to edit the rule to change something about the scene configuration
- You have to look at the rule code to even see what the current configuration is
- 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:
- 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
- One rule that handles all the actual modification of the metadata where the scene information is stored
- One external library script that handles converting scene metadata into item commands
- 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
- 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.
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.
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.
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.