Skip to content

Instantly share code, notes, and snippets.

@lukasvice
Last active June 27, 2024 11:54
Show Gist options
  • Save lukasvice/b364724d84c3ac4e160f7a7d8fa37066 to your computer and use it in GitHub Desktop.
Save lukasvice/b364724d84c3ac4e160f7a7d8fa37066 to your computer and use it in GitHub Desktop.
Home Assistant script to control venetian blinds with Shelly
# Have a look at the blog post about this script:
# https://medium.com/@lukasvice/a-utility-script-for-controlling-venetian-blinds-with-shelly-in-home-assistant-2e5cbf2d8d5f
script:
cover_position_tilt:
mode: parallel
fields:
entity_id:
description: "The cover entity"
example: "cover.X"
position:
description: "Position of the cover"
example: 100
tilt_position:
description: "Tilt position (optional)"
example: 100
sequence:
- alias: "Set variables"
variables:
# Time in ms for a full tilt
tilt_time_ms: 1800
# Time between blinds move commands
cmd_wait_time_ms: 500
_original_position: "{{ state_attr(entity_id, 'current_position') }}"
- alias: "Open/Close tilt depending on current position"
choose:
# When closing
- conditions: "{{ state_attr(entity_id, 'current_position') > position|int }}"
sequence:
# Move to the desired position
- service: cover.set_cover_position
data_template:
entity_id: "{{ entity_id }}"
position: "{{ position|int }}"
# Blinds have to be tilted, if tilt_position is set and tilt_position is not fully closed
- alias: "Check if blinds should be tilted"
condition: template
value_template: "{{ tilt_position is defined and tilt_position != none and tilt_position|int > 0 }}"
# Wait for the blinds to stop (Shelly updates current_position on start/stop)
# Cancel the script if the position was not received after 100 seconds
- wait_for_trigger:
- platform: template
value_template: "{% if state_attr(entity_id, 'current_position') != _original_position %}true{% endif %}"
timeout: 100
continue_on_timeout: false
# If it's not the desired position, the blinds were stopped manually (in this case cancel the script)
- alias: "Check if blinds have reached desired position"
condition: template
value_template: "{% if is_state_attr(entity_id, 'current_position', position|int) %}true{% endif %}"
- delay:
milliseconds: "{{ cmd_wait_time_ms }}"
# Move the blinds the required time for tilting in the original direction
- service: cover.close_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms / 100 * tilt_position|int }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ cmd_wait_time_ms }}"
# Move the blinds the required time for tilting in the opposite direction
- service: cover.open_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms / 100 * tilt_position|int }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
# When opening
- conditions: "{{ state_attr(entity_id, 'current_position') < position|int }}"
sequence:
# Move to the desired position
- service: cover.set_cover_position
data_template:
entity_id: "{{ entity_id }}"
position: "{{ position|int }}"
# Blinds have to be tilted, if tilt_position is set and tilt_position is not fully open
- alias: "Check if blinds should be tilted"
condition: template
value_template: "{{ tilt_position is defined and tilt_position != none and tilt_position|int < 100 }}"
# Wait for the blinds to stop (Shelly updates current_position on start/stop)
# Cancel the script if the position was not received after 100 seconds
- wait_for_trigger:
- platform: template
value_template: "{% if state_attr(entity_id, 'current_position') != _original_position %}true{% endif %}"
timeout: 100
continue_on_timeout: false
# If it's not the desired position, the blinds were stopped manually (in this case cancel the script)
- alias: "Check if blinds have reached desired position"
condition: template
value_template: "{% if is_state_attr(entity_id, 'current_position', position|int) %}true{% endif %}"
- delay:
milliseconds: "{{ cmd_wait_time_ms }}"
# Move the blinds the required time for tilting in the original direction
- service: cover.open_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms / 100 * (100 - tilt_position|int) }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ cmd_wait_time_ms }}"
# Move the blinds the required time for tilting in the opposite direction
- service: cover.close_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms / 100 * (100 - tilt_position|int) }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
# Special case: the blinds are already in the desired position
default:
- alias: "Continue only if blinds are not fully opened or closed"
condition: template
value_template: "{{ state_attr(entity_id, 'current_position') > 0 and state_attr(entity_id, 'current_position') < 100 }}"
- choose:
# When the blinds are almost closed, move up for the tilt time
- conditions: "{{ state_attr(entity_id, 'current_position') < 10 }}"
sequence:
- service: cover.open_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
# When the blinds are open, move down for the tilt time
default:
- service: cover.close_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ tilt_time_ms }}"
- service: cover.stop_cover
data_template:
entity_id: "{{ entity_id }}"
- delay:
milliseconds: "{{ cmd_wait_time_ms }}"
# Trigger event to restart the script with the original parameters
- event: start_cover_position_tilt
event_data:
entity_id: "{{ entity_id }}"
position: "{{ position }}"
tilt_position: "{{ tilt_position }}"
automation:
# Automation triggered by a custom event to restart the script
- id: start_cover_position_tilt
alias: "Start Cover Position Tilt"
mode: parallel
trigger:
- platform: event
event_type: "start_cover_position_tilt"
action:
- service: script.cover_position_tilt
data_template:
entity_id: "{{ trigger.event.data.entity_id }}"
position: "{{ trigger.event.data.position }}"
tilt_position: "{{ trigger.event.data.tilt_position }}"
service: script.cover_position_tilt
data:
entity_id: cover.shelly_XXX
position: 70
tilt_position: 50
@MelleD
Copy link

MelleD commented Aug 15, 2023

@lukasvice

Exactly I know your links and the problems :-)

I am currently creating the temporary data in a temp sensor because the data was always deleted from the original sensor. Maybe there's a better way. In addition, I create a wrapper cover for the blinds to call the scripts directly.

I can also share my script this week, if you want

I solved it by 6 conditions.

  1. last_action open & current_tilt_position = 0
  2. last_action close & current_tilt_position = 0
  3. last_action open & (current_tilt_position - new_tilt_position) > 0
  4. last_action close & (current_tilt_position - new_tilt_position) > 0
  5. last_action open & (current_tilt_position - new_tilt_position) < 0
  6. last_action close & (current_tilt_position - new_tilt_position) < 0

The only edge case that hasn't been resolved is when someone manually tilts the blinds.

My idea (untested), since I'm already overriding open_cover and stop_cover, I could set the current_tilt_positiona to 50 as soon as the position was offset by just 1%.

@lukasvice
Copy link
Author

@MelleD Sounds promising! Tbh, I didn't fully understand your approach, but I'm very curious to see your solution :)

I do have one question though: Imagine you have the blinds at 70% with a 50% tilt position. Now you move the blinds up to 71%. In reality, this only changes the tilt and not the position. How do you deal with this, or how do you know correct the state?

@MelleD
Copy link

MelleD commented Aug 16, 2023

@lukasvice

Here are currently my try:

Like you, you have to play around with the time a bit.
Maybe I need 2 times.

script:
  open_cover:
    mode: parallel
    fields:
      entity_id:
        description: "The orginal cover entity"
        example: "cover.X"
    sequence:
      - alias: "Set variables"
        variables:
          temp: "_temp"
          entity_id_temp: "{{ entity_id + temp }}"
          current_position: "{{ state_attr(entity_id, 'current_position') }}"
      - service: python_script.set_state_attribute
        data:
          entity_id: "{{entity_id_temp}}"
          state_attr: current_tilt_position
          state: 0
      - service: python_script.set_state_attribute
        data:
          entity_id: "{{entity_id_temp}}"
          state_attr: last_action
          state: "open_cover"
      - service: cover.open_cover
        target:
          entity_id: "{{entity_id}}"
        data: {}

  close_cover:
    mode: parallel
    fields:
      entity_id:
        description: "The orginal cover entity"
        example: "cover.X"
    sequence:
      - alias: "Set variables"
        variables:
          temp: "_temp"
          entity_id_temp: "{{ entity_id + temp }}"
      - service: python_script.set_state_attribute
        data:
          entity_id: "{{entity_id_temp}}"
          state_attr: current_tilt_position
          state: 0
      - service: python_script.set_state_attribute
        data:
          entity_id: "{{entity_id_temp}}"
          state_attr: last_action
          state: "close_cover"
      - service: cover.close_cover
        target:
          entity_id: "{{entity_id}}"
        data: {}

  set_cover_position_tilt:
    mode: parallel
    fields:
      entity_id:
        description: "The cover entity"
        example: "cover.X"
      tilt_position:
        description: "Tilt position"
        example: 100
    sequence:
      - alias: "Set variables"
        variables:
          tilt_full_time_ms: 1200
          temp: "_temp"
          entity_id_temp: "{{ entity_id + temp }}"
          current_position: "{{ state_attr(entity_id, 'current_position') }}"
          current_tilt_position: "{{state_attr(entity_id_temp, 'current_tilt_position')}}"
      - alias: "Continue only if blinds are not fully opened and tilt position is changed"
        condition: template
        value_template: "{{ current_position is not none and (current_position|int) < 100 and current_tilt_position is not none and (tilt_position|int) != current_tilt_position}}"
      - alias: "Move the blinds"
        choose:
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'open_cover' and current_tilt_position == 0}}"
            sequence:
              - service: cover.close_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * tilt_position|int }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - service: python_script.set_state_attribute
                data:
                  entity_id: "{{entity_id_temp}}"
                  state_attr: current_tilt_position
                  state: "{{tilt_position|int}}"
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'close_cover' and current_tilt_position == 0}}"
            sequence:
              - service: cover.open_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * tilt_position|int }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - service: python_script.set_state_attribute
                data:
                  entity_id: "{{entity_id_temp}}"
                  state_attr: current_tilt_position
                  state: "{{tilt_position|int}}"
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'open_cover' and current_position != 0 and (tilt_position-current_tilt_position)>0}}"
            sequence:
              - service: cover.close_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * (tilt_position-current_tilt_position) }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - service: python_script.set_state_attribute
                data:
                  entity_id: "{{entity_id_temp}}"
                  state_attr: current_tilt_position
                  state: "{{tilt_position|int}}"
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'open_cover' and current_position != 0 and (tilt_position-current_tilt_position)<0}}"
            sequence:
              - service: cover.open_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * ((tilt_position-current_tilt_position)*-1) }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'close_cover' and current_position != 0 and (tilt_position-current_tilt_position)>0}}"
            sequence:
              - service: cover.open_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * (tilt_position-current_tilt_position) }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - service: python_script.set_state_attribute
                data:
                  entity_id: "{{entity_id_temp}}"
                  state_attr: current_tilt_position
                  state: "{{tilt_position|int}}"
          - conditions: "{{ state_attr(entity_id_temp, 'last_action') == 'close_cover' and current_position != 0 and (tilt_position-current_tilt_position)<0}}"
            sequence:
              - service: cover.close_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - delay:
                  milliseconds: "{{ tilt_full_time_ms / 100 * ((tilt_position-current_tilt_position)*-1) }}"
              - service: cover.stop_cover
                data_template:
                  entity_id: "{{ entity_id }}"
              - service: python_script.set_state_attribute
                data:
                  entity_id: "{{entity_id_temp}}"
                  state_attr: current_tilt_position
                  state: "{{tilt_position|int}}"

I do have one question though: Imagine you have the blinds at 70% with a 50% tilt position. Now you move the blinds up to 71%. In reality, this only changes the tilt and not the position. How do you deal with this, or how do you know correct the state?

I would like to do the following if the position moves just 1%.

  1. If the current tilt position is 0 I would set it to 50%
  2. If the current tilt position != 0 I would subtract or add 50 (depending on the last action)

2a. If the current tilt position is then over 100% I would set it back to 0.

But here my technical knowledge is too weak how and where to find out that the position moved only 1%.

I'm aware that you can't get a 100% accurate solution with this, but I don't have the use case to change the tilt 4 or 5 times either ;).
Most of the time you do it once, maybe a second time to adjust. That works quite well. It would be important to show the manual status.

With the new Shelly Plus 2 PM firmware version 1.0.0 you can get the last_direction directly from the device

With this information, I can remove some settings but I didn't find this information into HA

@Monacoslo
Copy link

Can you please help me understand how exactly should I use this? Do you have any tutorial? I am also not familiar much about scripts.

I have several external venetian blinds using Shelly 2PM plus and I need to make tilt control in HA.

If I understand correctly, I have to create "script" in HA for each one, copy in it the content after "-script" and rename entity_id to my id of cover.shelly....
Then create automation with content under "-automation"

What next?

@xbmcgotham
Copy link

xbmcgotham commented Aug 23, 2023 via email

@FlyingDodo86
Copy link

FlyingDodo86 commented Oct 12, 2023

I freshly started with HA and Shellys but after hours of figuring out issues I made it work. Here a summary of my conclusions:

In order to get the position of the covers I had to remove the shellys from HA and Shelly App. Then re-integrate them via the Shelly app and the run the Calibration on the Shelly app (Settings->Calibration). Once completed, re-integrated the Shellys in HA. Based on above replies maybe one additional remark:
The cover_position_tilt.yaml does not need to be modified simply e.g. create a new folder "Packages" via Studio Code Server. Then create a new file in the packages folder with name cover_position_tilt.yaml and copy paste the script content. Next is to add the following in the configuration.yaml

homeassistant:
  packages: !include_dir_named packages/

Once done go in Developer Tools and click on Check Configuration. If everything is good restart HA. Testing of the script: In Developer Tools click on Services and paste the below and click on Call Service

service: script.cover_position_tilt
data:
  entity_id: cover.replace_with_your_shelly_entity_ID
  position: 10
  tilt_position: 40

@lukasvice
Copy link
Author

lukasvice commented Oct 12, 2023

@FlyingDodo86 Thanks for sharing.

Note that in the meantime, I have updated the script to exclude the "package" line to make it easier for new users to use it. I still use it as a package though.

@xbmcgotham
Copy link

Hi @lukasvice hope your doing great!, I see you have made great progress. I am just wondering, as I am not a developer, can you confirm to me if this script will do what I am expecting for my setup? :-)

I have the blind boxes installed and all wired up, but have not yet the blinds, so I can still adapt to the best situation.

Like shown on the drawing attached, this is the setup I like and have wired up for. A Shelly (Pro series I have now) unit that is controlled by a manual switch on the wall and the HA. I know that in the past the Shelly HA script needs tweaking to make the tilt work, and I am not sure how this currently works with the wall switches in parallel. I am happy to buy additional WAREMA control system if that would allow for easy integration. I do try to stay away from Wifi or RF and like all wired as some blinds are to far away to reach that way.

Hope you can get back to me.

Appreciated, thanks!

Screenshot 2023-10-24 at 10 12 20

@Kepro
Copy link

Kepro commented Dec 22, 2023

@xbmcgotham yes it will work, checking wiring diagram for shelly 2PM

@xbmcgotham
Copy link

xbmcgotham commented Dec 22, 2023 via email

@MatejMonika
Copy link

Does work with Shelly 2PM Plus

@u20p17
Copy link

u20p17 commented Apr 9, 2024

@FlyingDodo86 Thanks for sharing.

Note that in the meantime, I have updated the script to exclude the "package" line to make it easier for new users to use it. I still use it as a package though.

Hello @lukasvice, I created a package folder and inside this folder I copied the latest cover_position_tilt.yaml. When I add

homeassistant:
  packages: !include_dir_named packages/

to the configuration.yaml and then check the configuration I get the following warning:


 Konfigurationswarnungen
 Setup of package 'script' failed: Integration 'cover_position_tilt' not found.
 Setup of package 'automation' failed: Invalid package definition 'automation': expected a dictionary. Package will not be initialized

What's my issue?

@lukasvice
Copy link
Author

Hi @u20p17, your approach seems fine to me. It looks like script and automation are somehow being interpreted as package names. Are you sure you're not using !include_dir_merge_named (That would work a bit differently)?

You can also look at some examples here and try to compare your configurations: https://www.home-assistant.io/examples/#example-configurationyaml

@u20p17
Copy link

u20p17 commented Apr 9, 2024

@lukasvice, i indeed did use the !include_dir_merge_named… if i delete this line in the configuration.yaml and restart HA i do not see any error/warning, but i can also not see any new script/automation…
something i am doing wrong 🤗

@lukasvice
Copy link
Author

lukasvice commented Apr 9, 2024

@u20p17, there's a difference between !include_dir_named and !include_dir_merge_named. The merge one requires the package name at the beginning of the file. Try using the one without merge, as you wrote in your original comment. See also the documentation on this: https://www.home-assistant.io/docs/configuration/packages/#create-a-packages-folder.

@u20p17
Copy link

u20p17 commented Apr 10, 2024

danke, hatte es tatsächlich falsch^^ jetzt funktionierts (Y)

@u20p17
Copy link

u20p17 commented Apr 17, 2024

today i had some time to play with this script, but in my case it is not working as expected. the problem I think is that my venetian blinds do need different times for a full tilt upwards (1400ms) and downwards (1000ms). Do you have the same issue and just took the middle value? if you send tilt position 50 should the blinds stop at around 45deg?

@lukasvice
Copy link
Author

@u20p17 Hmm, my blinds always take the same amount of time regardless of the direction. Maybe you could modify the script so that you have two variables, tilt_time_opening_ms and tilt_time_closing_ms. You can then use these variables in the "opening" or "closing" condition of the script. This might work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment