Skip to content

Instantly share code, notes, and snippets.

@lukasvice
Last active January 19, 2025 15:53
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
@Lowrider614
Copy link

Hi Lukas,

thanks for your work. Somehow the line value_template: "{% if state_attr(entity_id, 'current_position') != _original_position %}true{% endif %}" didn't work in my setup. The related trigger wasn't activated and run in a timeout. I changed it to value_template: {% if is_state(entity_id, 'open') or is_state(entity_id, 'closed') %}true{% endif %} which also indicates that the blinds are not moving anymore.

This approach works for me. Just for information - if you or anyone else has some similiar problems.

I had the exact same issue and your hint solved it. Thanks!

Just for curiosity: Does anyone have an idea, why this is happening? I am running HA in Docker on a Synology NAS.

BR Tim

@maxschloegl
Copy link

Hi @lukasvice!

This is a nice script.
But what does the following addition you posted do?
If I understand correctly, you save the last position and the tilt position, but for what? :-)
Why I ask: It would be nice to control the position and tilt position separately.
For example, if I move the blind to position 50 and tilt it to 50 and then move it to position 70 without tilt, it would be nice to remember the last tilt and tilt it back to 50.
Is this possible?

@MelleD,

I'm pretty sure you've seen my blog post about this script - if not, it's linked here :)

You can split up your scripts, sounds reasonable. With the new Shelly Plus 2 PM firmware version 1.0.0 you can get the last_direction directly from the device. But even with this information, you can't be sure if it's fully tilted or if it's only moved for 0.2 seconds, so maybe it's half tilted. Maybe I'm missing something, but I haven't found a way how to use this information. Anyone with good ideas is welcome! :)

To store the last position and tilt position, I use a trigger-based template sensor that stores the data of each cover as JSON. This script can be triggered by two events:

* Custom (manual): This event is fired by the end of the `cover_position_tilt` script and the `entity_id`, `position`, and `tilt_position` are passed as arguments

* A cover changes to `opening` or `closing`: This resets the stored position in the data to `unknown`

At the beginning of the cover_position_tilt script, I read the stored position from the JSON and compare it to the desired position. If it's the same, the script stops, otherwise it continues as normal.

I don't think this is the prettiest approach, but I couldn't think of a better one - and it works :) As I said, I'm open to any ideas and improvements.

Here's the template sensor:

template:
  - trigger:
      - id: "cover_position_tilt"
        platform: event
        event_type: "cover_position_tilt"
      - platform: state
        entity_id:
          - cover.shelly_2_5_COVER_ID
          - cover.shelly_2_5_COVER_ID
          - cover.shelly_2_5_COVER_ID
        to:
          - "opening"
          - "closing"
    sensor:
      - name: "cover_states"
        state: >
          {% set current = ('{}' if states('sensor.cover_states') == 'unknown' else states('sensor.cover_states')) | from_json %}
          {% if trigger.id == "cover_position_tilt" -%}
            {% set new = {
              trigger.event.data.entity_id: {
                'position': trigger.event.data.position,
                'tilt_position': trigger.event.data.tilt_position
              }
            } %}
          {%- else -%}
            {% set new = {
              trigger.entity_id: 'unknown'
            } %}
          {%- endif %}
          {{ dict(current, **new) | to_json }}

This is how to trigger it manually at the end of the open and close movements in the script:

- event: cover_position_tilt
  event_data:
    entity_id: "{{ entity_id }}"
    position: "{{ position }}"
    tilt_position: "{{ tilt_position }}"

And this is the comparison at the beginning of the script:

- alias: "Set variables"
  variables:
    _last_position: >
      {% set cover_states = ('{}' if states('sensor.cover_states') == 'unknown' else states('sensor.cover_states')) | from_json %}
      {{ cover_states[entity_id].position if entity_id in cover_states and 'position' in cover_states[entity_id] else 'unknown' }}
    _last_tilt_position: >
      {% set cover_states = ('{}' if states('sensor.cover_states') == 'unknown' else states('sensor.cover_states')) | from_json %}
      {{ cover_states[entity_id].tilt_position if entity_id in cover_states and 'tilt_position' in cover_states[entity_id] else 'unknown' }}
- condition: template
  value_template: >
     {{ _last_position == 'unknown' or _last_tilt_position == 'unknown' or position != _last_position or tilt_position != _last_tilt_position }}

@lukasvice
Copy link
Author

Hi @maxschloegl, I save the last position/tilt so that the blinds do not move when I request the same position/tilt again. This is useful if you run automations that want to move the blinds even if they are already in that position. I see what you're trying to do, and yes, you could use the saved tilt position if no tilt is provided as a parameter, so it wouldn't change. Personally, I have not come across this use case yet.

@lukasvice
Copy link
Author

Hi everyone, Shelly has announced the new 2PM Gen3 with drum roll blind angle control! Let's hope they got it right and this is the feature we've been waiting for!

@Lowrider614
Copy link

Hi everyone, Shelly has announced the new 2PM Gen3 with drum roll blind angle control! Let's hope they got it right and this is the feature we've been waiting for!

It actually is. I raised a ticket with them a few weeks ago where they told me that i should stay tuned for this feature to come. The 3rd Gen Pro devices already have that feature. Now HomeAssistant integration needs an update I guess and than things should work.

@okos111
Copy link

okos111 commented Nov 24, 2024

Hi. Nice script. I'm trying to make an action where at a specified time 4 blinds (4 x Shelly 2PM) are set to a certain position and no way I can get it to work. Only maybe 2 of the 4 always respond. Or some don't set the tilt position. It doesn't even work when I enter actions in succession or in parallel. If I make an action on just one blinds, it works.
Any idea?

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