I’ve been writing a blueprint to keep a given switch & multiple lights in sync. This ends up being somewhat complex because the behavior I want is for whichever changed first - switch (e.g. through a physical tap) or light (e.g. through voice) - to cause the other to change. (There’s clarifying comments about the desired behavior in the full blueprint below.)
One issue I’m trying to resolve from an initial, simpler version of the blueprint is ignoring state updates from the non-initially-triggered entities until the initial change is done. So for example, if the switch changes brightness from 20 to 100, I want to take no action on light state changes until the lights reach 100 brightness. This would fix an occasional bug I observed in the simpler automation, which I believe was due to the light states happening to sync up partway through a switch-initiated change (say at 50 brightness), causing the automation to light.turn_on the switch back to 50.
I’m trying to use the single automation mode plus an action that has a repeat-until loop with wait_for_trigger to keep the automation running until the change is done, but it seems like it’s missing state triggers and getting stuck.
The pseudocode is basically:
action:
- repeat:
until: light & switch brightness equal
sequence:
- if repeat.first or wait.trigger same as original trigger:
- send light.turn_on/off to non-triggered entities
- wait_for_trigger: switch or light state change (same as original triggers)
But the wait_for_trigger seems to wait forever unless I add a timeout, as if it’s missing the state updates caused by the light.turn_on/off above it. With a short timeout the action completes after the timeout, so the condition is true by that point. Is it unreliable to use wait_for_trigger to monitor for the change caused by a service call to finish in this way?
I found a much simpler solution to ignore state changes made by the last run of an automation: use the event’s context values. I’d thought about this originally but the documentation for context doesn’t go into a lot of detail about exactly how the context values are set, so it was unclear exactly how it behaves. What I observed through traces and monitoring events (i.e. this could be subtly wrong or incomplete) was that an automation’s this.context.id value seems to update when the action is triggered. So I added a condition to my automation that trigger.to_state.context.id != this.context.id, i.e. only trigger the action again if the trigger event occurred for a reason other than the last run of the automation.
The full, much simpler blueprint is now this:
blueprint:
name: Synchronize Inovelli VZM31 Switch & Lights V3
description: Keep defined switch & lights states' synchronized
domain: automation
input:
switch:
name: Switch
selector:
entity:
domain: light
integration: zha
lights:
name: Lights
selector:
entity:
domain: light
multiple: yes
trace:
stored_traces: 20
mode: restart
trigger:
- platform: state
id: switch
entity_id: !input switch
- platform: state
id: lights
entity_id: !input lights
variables:
switch: !input switch
lights: !input lights
condition:
- '{{ states(trigger.entity_id) in ["on", "off"] }}'
# Filter out state changes caused by the last run of this automation
- '{{ trigger.to_state.context.id != this.context.id }}'
# Action only needed if switch & lights not in same state
- >
{{ lights | map('states') |
select('ne', states(switch)) |
list | count > 0
or
lights | map('state_attr', 'brightness') |
select('ne', state_attr(switch, 'brightness')) |
list | count > 0 }}
- or:
# Always change lights on switch trigger
- condition: trigger
id: switch
# All lights must have same state & brightness to change switch
# TODO: Maybe more natural for switch to follow max of lights instead?
- and:
- >
{{ lights |
reject('eq', trigger.entity_id) |
map('states') |
select('ne', states(trigger.entity_id)) |
list | count == 0 }}
- >
{{ lights |
reject('eq', trigger.entity_id) |
map('state_attr', 'brightness') |
select('ne', state_attr(trigger.entity_id, 'brightness')) |
list | count == 0 }}
action:
- service: light.turn_{{ states(trigger.entity_id) }}
target:
entity_id: |
{% if trigger.id == 'lights' %}
{{ switch }}
{% else %}
{{ lights }}
{% endif %}
data: |
{% if is_state(trigger.entity_id, 'on') %}
{
"brightness": {{ state_attr(trigger.entity_id, 'brightness') }},
"transition": 0.5
}
{% else %}
{}
{% endif %}