Hi everyone,
UPDATE: I updated this initial post so that it now contains the latest and greatest coder and hopefully some improved explanations. Luckily I could restore this post from my browser cache after the forum restore…
This is my custom rollershutter widget. It is mainly targeted to be used on my phone but it can be used on the desktop as well of course. I used this project also to try some more advanced stuff (compared to my previous attempts) and also as a proof of concept to see what configuration options you could give users through widgets since I see a couple of use cases for this. I’m really happy with the result even though it requires a couple of extra items and of course some scripting.
I’ll start with a look at the final result:
In Action
Note that the popups on the following screenshots looks a bit different now than shown here because I switched from a popover to a popup which works much better on a smaller screen and matches more the style of the card popups used by OpenHab by default. The functionality however remains the same.
On an Android phone (Dark Mode):
Two widget instances side by side (on the left with the movement indicator shown)
And here with the opened preset popover
The same on the desktop (but without movement indicator:
My goal was to have three possible controls in a relatively small cell (I use the same size as for my universal toggle widget):
- Up / Down / Stop Controls
- Exact position control
- Apply one of some saved presets (that can preferrably be defined by a regular user)
Also I wanted to have an indicator that the rollershutter is moving since at least my shelly equipment does not update the position until it has reached its final position when using the UP or DOWN commands.
How to Use
Here you can see the configuration options (advanced configuration is only used for localization and thus not shown here):
What do you need?
For basic controls
- Control Item: An item that can be used to issue UP/DOWN/STOP commands
- Position Item: An item that can be used to issue 0,1,2,…,100 commands
For the movement indicator
3. An expression that evaluates to the String “true” when the shutter is moving (leave empty for no movement indicator). I set this to true when the current consumption is > 0 like this:
=(items.dining_room_blinds_consumption_current.state.toString().split(".")[0]) != "0" ? "true" : "false"
Replace dining_room_blinds_consumption_current with the name of your item or use a completely different expression that matches the functionality of your equipment.
For preset configuration
4. Preset Item: A String item in the format: <Num1>|<Num2>|<Num3>...
with Num1/2/3… being a number between 0 and 100
5. Add preset: A string item that receives a string that will be added to the presets item via a script (see below)
6. Delete preset: A string item that receives a string that will be removed from the presets item via a script (see below)
You can either use the same items for all shutters (preset set for one shutter will be available for all others as well) or per shutter/room/whatever…
Other configurations
Apart from that there is a card title (that I do not use) a label for the shutter and a footer that I use to show the current state with an expression like this:
=(items.dining_room_blinds_position.state.toString().split(".")[0]) == 0 ? "Geschlossen" : (items.dining_room_blinds_position.state.toString().split(".")[0]) == 100 ? "Geöffnet" : ((items.dining_room_blinds_position.state).split(".")[0] + "% Geöffnet")
Geöffnet = German for Opened
Geschlosse = German for Closed
Again replace dining_room_blinds_position with the name of the item that is representing your shutter position.
Scripting
To make the preset configuration work you need to create three plain String items for 4. 5. and 6. above. These items don’t need any special settings, so use whatever you like there.
Create two rules:
- Triggered when item used for 5. - Add Preset receives a command. Create an ECMA Script Action with the following code:
var currentPresets = ir.getItem('dining_room_blinds_presets').state.toString()
var presetsArray = currentPresets.split('|')
var index = presetsArray.indexOf(command.toString().split(".")[0])
if (index < 0) {
presetsArray.push(command.toString().split(".")[0])
presetsArray.sort(function(a, b) {
return a - b;
})
var newPresets = presetsArray.join("|")
events.sendCommand(ir.getItem('dining_room_blinds_presets'), newPresets)
}
Replace all string dining_room_blinds_presets strings with the name of the item you used for 4. in the configuration above. The script will retrieve this item and check whether the preset is already available. If not it will add the new preset, sort the list ascending and update the preset item. The .split(".")[0] part is not really needed anymore. I left it here as it does not hurt but I do cut off the decimal places (in case your equipment sends them like my shellys do) already in the widget code.
- Triggered when item used for 6. - Delete Preset receives a command. Create and ECMA Script action with the following code:
var currentPresets = ir.getItem('dining_room_blinds_presets').state.toString();
var presetsArray = currentPresets.split('|');
var index = presetsArray.indexOf(command.toString().split(".")[0]);
if (index > -1) {
presetsArray.splice(index, 1);
var newPresets = presetsArray.join("|");
events.postUpdate(ir.getItem('dining_room_blinds_presets'), newPresets);
}
Replace all dining_room_blinds_presets strings again with the name of your item 4. The script will retrieve item 4. and remove the preset if it exists. I know that the removal function of presets is pretty hidden but that was done intentionally to avoid accidental deletion.
I think that’s all you need for now, please give it a try and please let me know what you think and where you see room for improvement! I think it is really cool to give configuration options to users within widgets without the need for an administrator. I want to use this pattern also for the settings of alarm clock / wake up light in the future.
Resources
Here is the code of the widgets you will need (add in developer tools → widgets):
1. Main Widget (this is what you need to add to a page)
uid: universal_shutter_v4
tags: []
props:
parameters:
- description: Title of the card (empty for none)
label: Title
name: title
required: false
type: TEXT
- description: Unique label shown on top of the card
label: Rollershutter label
name: label
required: true
type: TEXT
- description: The card footer (empty for none)
label: Footer
name: footer
required: false
type: TEXT
- context: item
description: Rollershutter control (receives UP, DOWN, STOP commands)
label: Rollershutter control
name: control
required: true
type: TEXT
- context: item
description: Rollershutter position (receives 0 to 100 commands)
label: Rollershutter position
name: position
required: true
type: TEXT
- description: Set to true via expression when moving to show movement indicator
label: Show movement indicator
name: moving
required: false
type: TEXT
- context: item
description: Presets
label: Preset Item
name: presets
required: true
type: TEXT
- context: item
description: Item that can be used to add a preset
label: Add Preset
name: add_preset
required: true
type: TEXT
- context: item
description: Item that can be used to delete a preset
label: Delete Preset
name: delete_preset
required: true
type: TEXT
- description: Localisation of term Presets
label: Presets Translation
name: presets_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Apply
label: Apply Translation
name: apply_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Add
label: Add Translation
name: add_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Delete
label: Delete Translation
name: delete_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Close
label: Close Translation
name: close_trans
required: false
type: TEXT
advanced: true
parameterGroups: []
timestamp: Feb 7, 2021, 10:08:33 PM
component: f7-card
config:
title: =props.title
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-progressbar
config:
infinite: true
visible: '=props.moving == "true" ? true : false'
style:
position: absolute
bottom: 0px
left: 0px
--f7-progressbar-height: 2px
- component: f7-row
config:
class:
- display-flex
- justify-content-center
- align-items-flex-start
style:
height: 25px
slots:
default:
- component: f7-block-header
slots:
default:
- component: Label
config:
text: =props.label
- component: f7-row
config:
class:
- display-flex
- justify-content-space-evenly
- align-items-center
style:
width: 100%
height: 125px
slots:
default:
- component: f7-col
config:
class:
- display-flex
- flex-direction-column
- justify-content-space-evenly
- align-items-flex-end
style:
height: calc(100% - 28px)
slots:
default:
- component: oh-slider
config:
color: white
label: true
item: =props.position
vertical: true
style:
--f7-range-bar-size: 18px
--f7-range-bar-border-radius: 10px
--f7-range-knob-size: 20px
--f7-range-bar-bg-color: rgba(246, 158, 81, 0.2)
--f7-range-bar-active-bg-color: linear-gradient(to top, rgba(246,158,81,0), rgba(246,158,81,0.8))
--f7-range-knob-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
--f7-range-label-text-color: black
- component: f7-col
config:
class:
- display-flex
- flex-direction-column
- justify-content-center
- align-items-center
style:
height: calc(100% - 20px)
slots:
default:
- component: oh-link
config:
actionItem: =props.control
actionCommand: UP
action: command
colorTheme: gray
iconF7: arrowtriangle_up
iconSize: 20
style:
background-color: rgba(246, 158, 81, 0.2)
border-radius: 15px 15px 0px 0px
padding: 10px
- component: oh-link
config:
actionItem: =props.control
actionCommand: STOP
action: command
iconF7: stop
iconSize: 20
style:
background-color: rgba(246, 158, 81, 0.2)
padding: 10px
- component: oh-link
config:
actionItem: =props.control
actionCommand: DOWN
action: command
colorTheme: gray
iconF7: arrowtriangle_down
iconSize: 20
style:
background-color: rgba(246, 158, 81, 0.2)
border-radius: 0px 0px 15px 15px
padding: 10px
- component: f7-row
config:
class:
- display-flex
- justify-content-space-evenly
- align-items-center
style:
height: 25px
slots:
default:
- component: oh-link
config:
text: '=props.presets_trans ? props.presets_trans : "Presets"'
action: popup
actionModal: widget:preset_popup_v2
actionModalConfig:
position: =props.position
presets: =props.presets
add_preset: =props.add_preset
delete_preset: =props.delete_preset
apply_trans: =props.apply_trans
add_trans: =props.add_trans
delete_trans: =props.delete_trans
close_trans: =props.close_trans
- component: f7-card-footer
slots:
default:
- component: Label
config:
text: =props.footer
2. Widget for the popover (no need to do anything with this execpt adding it):
uid: preset_popup_v2
tags: []
props:
parameters:
- context: item
description: Rollershutter position (receives 0 to 100 commands)
label: Rollershutter position
name: position
required: true
type: TEXT
- context: item
description: Presets
label: Preset Item
name: presets
required: true
type: TEXT
- context: item
description: Item that can be used to add a preset
label: Add Preset
name: add_preset
required: true
type: TEXT
- context: item
description: Item that can be used to delete a preset
label: Delete Preset
name: delete_preset
required: true
type: TEXT
- description: Localisation of term Apply
label: Apply Translation
name: apply_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Add
label: Add Translation
name: add_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Delete
label: Delete Translation
name: delete_trans
required: false
type: TEXT
advanced: true
- description: Localisation of term Close
label: Close Translation
name: close_trans
required: false
type: TEXT
advanced: true
parameterGroups: []
timestamp: Feb 7, 2021, 10:09:38 PM
component: f7-card-content
config:
padding: false
class:
- margin-vertical
- display-flex
- flex-direction-column
- flex-justify-content-flex-start
- flex-align-items-center
slots:
default:
- component: f7-list
slots:
default:
- component: oh-repeater
config:
for: preset
in: =items[props.presets].state.split('|')
fragment: true
slots:
default:
- component: f7-list-item
config:
title: =loop.preset + "%"
visible: =loop.preset != ""
swipeout: true
slots:
after:
- component: oh-button
config:
disabled: =items[props.position].state.split(".")[0] == loop.preset
text: '=props.apply_trans ? props.apply_trans : "Apply"'
actionItem: =props.position
actionCommand: =loop.preset
action: command
class:
- popup-close
default:
- component: f7-swipeout-actions
config:
right: true
slots:
default:
- component: oh-link
config:
text: '=props.delete_trans ? props.delete_trans : "Delete"'
bgColor: red
actionItem: =props.delete_preset
actionCommand: =loop.preset
action: command
class:
- swipeout-close
- component: f7-list-item-row
config:
class:
- display-flex
- align-items-center
- justify-content-center
slots:
default:
- component: oh-button
config:
iconF7: plus_circle_fill
iconSize: 15
text: '=props.add_trans ? props.add_trans : "Add"'
action: command
actionItem: =props.add_preset
actionCommand: =items[props.position].state
popoverClose: true
style:
width: 100%
- component: oh-button
config:
popupClose: true
round: true
fill: true
large: true
text: '=props.close_trans ? props.close_trans : "Close"'
class:
- margin-horizontal