Skip to content

Instantly share code, notes, and snippets.

@Dianoga
Created April 28, 2026 23:04
Show Gist options
  • Select an option

  • Save Dianoga/9e04441593865810203be36179fefb8e to your computer and use it in GitHub Desktop.

Select an option

Save Dianoga/9e04441593865810203be36179fefb8e to your computer and use it in GitHub Desktop.
Zigbee2MQTT AM25 Blind Watchdog
blueprint:
name: Zigbee2MQTT AM25 Blind Watchdog
description: >
Watches a Zigbee2MQTT AM25 blind command topic. If the cover does not appear
to respond within the timeout, sends a Zigbee2MQTT device reconfigure request
and optionally replays the original command after a successful reconfigure.
domain: automation
author: local
input:
z2m_friendly_name:
name: Zigbee2MQTT friendly name
description: >
The exact Zigbee2MQTT friendly_name for this blind.
Example: Living Room Left Blind
selector:
text:
cover_entity:
name: Home Assistant cover entity
description: >
The Home Assistant cover entity created by Zigbee2MQTT discovery.
selector:
entity:
filter:
domain: cover
command_topic:
name: Blind command topic
description: >
The MQTT topic Home Assistant/Zigbee2MQTT uses to command this blind.
Usually zigbee2mqtt/FRIENDLY_NAME/set.
default: zigbee2mqtt/CHANGE_ME/set
selector:
text:
configure_request_topic:
name: Zigbee2MQTT configure request topic
description: >
Usually zigbee2mqtt/bridge/request/device/configure.
default: zigbee2mqtt/bridge/request/device/configure
selector:
text:
configure_response_topic:
name: Zigbee2MQTT configure response topic
description: >
Usually zigbee2mqtt/bridge/response/device/configure.
default: zigbee2mqtt/bridge/response/device/configure
selector:
text:
response_timeout_seconds:
name: Response timeout
description: >
How long to wait for the blind entity to show movement, target position,
or state change before declaring the command failed.
default: 25
selector:
number:
min: 5
max: 180
step: 1
unit_of_measurement: seconds
mode: slider
configure_timeout_seconds:
name: Reconfigure timeout
description: >
How long to wait for Zigbee2MQTT to confirm the device reconfigure.
default: 90
selector:
number:
min: 10
max: 300
step: 5
unit_of_measurement: seconds
mode: slider
movement_threshold:
name: Movement threshold
description: >
Minimum current_position change that counts as a response.
default: 2
selector:
number:
min: 1
max: 20
step: 1
unit_of_measurement: "%"
mode: slider
position_tolerance:
name: Position tolerance
description: >
How close current_position must be to the target position to count as success.
default: 3
selector:
number:
min: 0
max: 15
step: 1
unit_of_measurement: "%"
mode: slider
replay_original_command:
name: Replay original command
description: >
After a successful reconfigure, publish the original blind command again.
default: true
selector:
boolean:
watch_stop_commands:
name: Watch STOP commands
description: >
STOP commands are harder to verify because a successful STOP may not
produce a state or position update. Leave this off unless you specifically
want STOP failures to trigger reconfigure.
default: false
selector:
boolean:
notify_on_failure:
name: Notify if recovery fails
description: >
Create a persistent Home Assistant notification if the blind does not
respond and Zigbee2MQTT does not confirm a successful reconfigure.
default: true
selector:
boolean:
mode: single
max_exceeded: silent
triggers:
- trigger: mqtt
topic: !input command_topic
variables:
z2m_friendly_name: !input z2m_friendly_name
cover_entity: !input cover_entity
command_topic: !input command_topic
configure_request_topic: !input configure_request_topic
configure_response_topic: !input configure_response_topic
response_timeout_seconds: !input response_timeout_seconds
configure_timeout_seconds: !input configure_timeout_seconds
movement_threshold: !input movement_threshold
position_tolerance: !input position_tolerance
replay_original_command: !input replay_original_command
watch_stop_commands: !input watch_stop_commands
notify_on_failure: !input notify_on_failure
original_payload: "{{ trigger.payload }}"
command: "{{ trigger.payload_json | default({}) }}"
command_state: "{{ command.state | default('') | upper }}"
is_cover_command: >-
{{
command.position is defined
or command_state in ['OPEN', 'CLOSE']
or (watch_stop_commands | bool and command_state == 'STOP')
}}
before_state: "{{ states(cover_entity) }}"
before_position: "{{ state_attr(cover_entity, 'current_position') }}"
target_position: >-
{% if command.position is defined %}
{{ command.position | int }}
{% elif command_state == 'OPEN' %}
100
{% elif command_state == 'CLOSE' %}
0
{% else %}
{{ none }}
{% endif %}
has_target_position: >-
{{ target_position not in [none, 'none', 'None', ''] }}
already_at_target: >-
{% if has_target_position and before_position is number %}
{{ ((before_position | int) - (target_position | int)) | abs <= (position_tolerance | int) }}
{% else %}
false
{% endif %}
transaction_id: >-
watchdog_{{ z2m_friendly_name
| replace(' ', '_')
| replace('/', '_')
| replace('-', '_') }}_{{ now().timestamp() | int }}
conditions:
- condition: template
value_template: "{{ is_cover_command }}"
# Do not treat "set to the position it is already at" as a failure.
- condition: template
value_template: "{{ not already_at_target }}"
actions:
- action: system_log.write
data:
level: info
message: >-
AM25 watchdog armed for {{ z2m_friendly_name }}.
Command={{ original_payload }},
before_state={{ before_state }},
before_position={{ before_position }},
target_position={{ target_position }}.
- wait_template: >-
{% set cur = state_attr(cover_entity, 'current_position') %}
{% set st = states(cover_entity) %}
{% if command_state == 'STOP' %}
{{ st != before_state }}
{% elif has_target_position and cur is number and before_position is number %}
{{
((cur | int) - (target_position | int)) | abs <= (position_tolerance | int)
or
((cur | int) - (before_position | int)) | abs >= (movement_threshold | int)
}}
{% elif has_target_position and cur is number %}
{{ ((cur | int) - (target_position | int)) | abs <= (position_tolerance | int) }}
{% else %}
{{ st != before_state }}
{% endif %}
timeout:
seconds: "{{ response_timeout_seconds | int }}"
continue_on_timeout: true
- choose:
- conditions:
- condition: template
value_template: "{{ wait.completed }}"
sequence:
- action: system_log.write
data:
level: info
message: >-
AM25 watchdog saw a response from {{ z2m_friendly_name }};
no reconfigure needed.
default:
- action: system_log.write
data:
level: warning
message: >-
AM25 watchdog timeout for {{ z2m_friendly_name }};
sending Zigbee2MQTT reconfigure request.
- action: mqtt.publish
data:
topic: "{{ configure_request_topic }}"
payload: >-
{{
{
"id": z2m_friendly_name,
"transaction": transaction_id
} | to_json
}}
qos: 0
retain: false
- wait_for_trigger:
- trigger: mqtt
topic: !input configure_response_topic
payload: "{{ transaction_id }}"
value_template: "{{ value_json.transaction | default('') }}"
timeout:
seconds: "{{ configure_timeout_seconds | int }}"
continue_on_timeout: true
- choose:
- conditions:
- condition: template
value_template: >-
{{
wait.completed
and wait.trigger.payload_json.status | default('') == 'ok'
}}
sequence:
- action: system_log.write
data:
level: warning
message: >-
Zigbee2MQTT reconfigure completed for {{ z2m_friendly_name }}.
- choose:
- conditions:
- condition: template
value_template: "{{ replay_original_command | bool }}"
sequence:
- delay: "00:00:02"
- action: system_log.write
data:
level: warning
message: >-
Replaying original command for {{ z2m_friendly_name }}:
{{ original_payload }}
- action: mqtt.publish
data:
topic: "{{ command_topic }}"
payload: "{{ original_payload }}"
qos: 0
retain: false
# Keep the automation running briefly so mode: single drops
# the replay-triggered copy of itself.
- delay: "00:00:05"
default:
- action: system_log.write
data:
level: error
message: >-
AM25 watchdog recovery failed for {{ z2m_friendly_name }}.
No successful Zigbee2MQTT configure response was received.
- choose:
- conditions:
- condition: template
value_template: "{{ notify_on_failure | bool }}"
sequence:
- action: persistent_notification.create
data:
title: "Blind watchdog failed"
message: >-
{{ z2m_friendly_name }} did not respond to command
{{ original_payload }}, and Zigbee2MQTT did not confirm
a successful reconfigure within
{{ configure_timeout_seconds }} seconds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment