Here is a proposal to fix an issue we can have with some Curtain/blind switches (TS130F). The set_cover_position does not work properly : the cover does a full up or down operation whatever the % we ask.
This automation works fine with Z2M integration (position and calibration time are exposed)
Just put the list of MQTT ids in the conditions/value_template/eid list.
Principle : the set_cover_position is intercepted, the motor running duration is calculated from the current positiion, the calibration time and the requested position. If a new command on the same cover arrives during the timeout, the STOP is not done.
automation:
- alias: "Cover – Proportional Stop Auto (multi-cover, filtered wait)"
description: >
Automatically stops Loratap/Tuya covers after proportional travel time.
Works around devices that always run full calibration time on set_cover_position.
Filters events so simultaneous covers do not interfere.
mode: parallel
max: 20
trigger:
- platform: event
event_type: call_service
event_data:
domain: cover
service: set_cover_position
# Only handle specific covers and intermediate positions (1–99%)
condition:
- condition: template
value_template: >
{% set eid = trigger.event.data.service_data.entity_id %}
{% set pos = trigger.event.data.service_data.position | int(0) %}
{% set allowed = [
'cover.0xa4c1384b6ed93c6f',
'cover.0xa4c138e49308dd17'
] %}
{{ eid in allowed and 0 < pos < 100 }}
variables:
cover_entity: "{{ trigger.event.data.service_data.entity_id }}"
target: "{{ trigger.event.data.service_data.position | int }}"
current: "{{ state_attr(cover_entity, 'current_position') | int(0) }}"
delta: "{{ (target - current) | abs }}"
# Fetch calibration time from Zigbee2MQTT sensor (fallback to 17s)
cover_obj: "{{ cover_entity.split('.')[1] }}"
sensor_calib: "{{ 'sensor.' ~ cover_obj ~ '_calibration_time' }}"
travel_time: >
{% set s = states(sensor_calib) %}
{% if s not in ['unknown','unavailable', None, ''] %}
{{ s | float(17) }}
{% else %}17{% endif %}
# Clamp duration between 0.3s and travel_time
duration: >
{% set d = travel_time | float * delta | float / 100 %}
{{ [ [ d, 0.3 ] | max, travel_time | float ] | min }}
action:
# Wait either for a new command (on same cover) or timeout
- wait_for_trigger:
- platform: event
event_type: call_service
event_data:
domain: cover
service: set_cover_position
service_data:
entity_id: "{{ cover_entity }}"
- platform: event
event_type: call_service
event_data:
domain: cover
service: stop_cover
service_data:
entity_id: "{{ cover_entity }}"
- platform: event
event_type: call_service
event_data:
domain: cover
service: open_cover
service_data:
entity_id: "{{ cover_entity }}"
- platform: event
event_type: call_service
event_data:
domain: cover
service: close_cover
service_data:
entity_id: "{{ cover_entity }}"
timeout:
seconds: "{{ duration }}"
continue_on_timeout: true
# If we reached timeout → stop cover
# If a new command was received for the same cover → do nothing
- if: "{{ not wait.completed }}"
then:
- service: cover.stop_cover
target:
entity_id: "{{ cover_entity }}"