Custom rollershutter widget with preset configuration

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):

  1. Up / Down / Stop Controls
  2. Exact position control
  3. 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.

Here is the code of the widgets you will need (add in developer tools -> widgets):

1. 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

2. 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

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

  1. Control Item: An item that can be used to issue UP/DOWN/STOP commands
  2. 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:

  1. 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.

  1. 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.

5 Likes

I think I have not yet fully understood how to correctly use popovers. Though my widget works well when there is only one of them on the page I see some strange behaviour with more than one. I guess that is because I defined multiple popovers in the DOM with the same class. I guess I will have to remove the popover from the code and create a separate widget. Haven’t figured out though how to have different presets for different shutters then… I will post an update here one I figured out the right way to do it. Please let me know if you know how this would be handled correctly…

Incredibly useful post :smiley: will try that

Oh No… My yesterdays update of the initial post is apparently lost due to the forum software update failure…

I will see when I find the time to update it again…

Did you already find the time to update it? I’m quite interested in what’s new :slight_smile:

Oh sorry yes I did. You find the most up to date version of the widget in the first post. Basically there where no changes in functionality but mainly the switch to popups that just works better on a phone screen. Apart from that I updated some of the initial descriptions on how to set things up.

Great, I’ll give it a try later then.

Thank you for creating these beautiful widgets :slight_smile:

Thank you for your positive feedback! Please let me know if you encounter any issues with the setup!

1 Like

hi!

thanks a lot for your work :slight_smile:
q: somehow I do not get how to add a preset. I added the string items and rules, but the text here is not editable:

I cannot replace NULL%
any idea?

You should be able to add the current position as preset by clicking “Add”. To delete “swipe” the NULL % row to the left and click “Delete”. Does this work?

ahh, I though the text should be editable. will try that, thanks :slight_smile:

Though I could imagine real use cases for that (e.g. a ‘Watch TV’ preset) this is not possible right now. I guess this could be done but would require some additional work on the widget.

Right now you only get a preset that is named via its corresponding percentage value.