Advice on writing complex trigger-condition pair automations

A question on programming logic more than anythign HA specific:

I keep finding that many of my automations constantly try to deal with the same structure: I typically want to trigger off something, (e.g. A – temp above 25), but then only act on it if some condition is satisfied (e.g. B – someone is home).

But then immediately this leads to the complement of this pair: I would like to execute the automation sequence if B is triggered while A is true (someone gets back home and it’s already hot).

So I write this as two separate triggers and then repeat both the triggers as conditions. A bit of repetition, but simple enough.

Once you begin to add multiple triggers this rapidly escalates to a nightmare. But it feels like this (trigger A, condition B, trigger B condition A) pair is completementary in a well defined way, so there should be a better way of writing this set of logical requirements.

Is there some smarter way to do this?

The only way I know to achieve this sort of logic is by using a template trigger.

Here is one I use as an example:

  trigger:
  - platform: template
    value_template: '{{ is_state(''binary_sensor.garage_door_contact'', ''on'')and
      is_state(''sun.sun'', ''below_horizon'') and is_state(''group.garden_lights'',
      ''off'') }}'

It depends a bit on how many conditions/triggers you want to combine and how to use, since you are only providing ‘e.g.’ it is not that easy to come with more
If there are a lot then I would create binary sensors or input_boolean for each and use them along.
Have a look at this one too
Help converting multiple elif’s to something less ugly - Configuration - Home Assistant Community (home-assistant.io)

Indeed I’ve ended up with some binary sensors for conditions which can be interpreted as schedules and the like which has simplified some of this. I should use this more!

Using template triggers is something I hadn’t quite thought through before. It indeed achieves the trigger and condition in one and you can package such triggers with their conditions nicely so that you don’t need to pollute the choose statements below too much.

They do become a bit hard to read I guess so a bit more commenting is called for.

1 Like

Can’t you just list all the trigger:s that you would want to consider; then or your list of condition:s to exactly specify every situation when the automation should trigger?

Alternatively you can achieve the same thing (more neatly in complex situations) with a template sensor that:

  • you define the set of triggers as above as to when to re-evaluate the sensor value.
  • duplicate the logic of the list of conditions above in the template calculation.
  • return a binary value (that provides a simple platform: stateto: True trigger for your automation).
    If you want the automation to trigger again when the trigger conditions are already met, but then are met again, then your template would have to include a timed reset (to force a state change).

Well, in case anyone would prefer to recommend something specific for a non-general example, here is a somewhat extreme automation that is typical in that I wanted to add a condition to the final choose clause that the automation shouldn’t trigger when the sensor.home_weather_dni is below 400, but this then requires me to add a whole new trigger for when this sensor goes above 400 and a bunch of other conditions are true.

I’ve ended up combining everything into one automation and maybe this is not the most sensible, but it keeps my automations somewhat orderly in the UI.

It seems like rossk’s suggestion of template triggers might indeed be the best at shrinking the logic here.

- alias: "Blind: Sunscreener -- Balcony"
  description: "Move blacony blinds according to sun_screener, but take into account rain!"
  id: "urergdffdbddpteirytw3gtg"
  trigger:
    - id: sun_in_window
      platform: state
      entity_id:
        - binary_sensor.blind_balcony_right_optimal_state
        - binary_sensor.blind_balcony_left_optimal_state
      to: "off"
      variables:
        blind: "{{trigger.to_state.object_id.removesuffix('_optimal_state')}}"
        action: "close"
    - id: sun_off_window
      platform: state
      entity_id:
        - binary_sensor.blind_balcony_right_optimal_state
        - binary_sensor.blind_balcony_left_optimal_state
      to: "on"
      variables:
        blind: "{{trigger.to_state.object_id.removesuffix('_optimal_state')}}"
        action: "open"
    - id: sun_on_balcony
      platform: state
      entity_id:
        - binary_sensor.blind_balcony_right_copy_optimal_state
        - binary_sensor.blind_balcony_left_copy_optimal_state
      from: "on"
      to: "off"
      variables:
        blind: "{{trigger.to_state.object_id.removesuffix('_copy_optimal_state')}}"
        action: "close"
    - id: sun_off_balcony
      platform: state
      entity_id:
        - binary_sensor.blind_balcony_right_copy_optimal_state
        - binary_sensor.blind_balcony_left_copy_optimal_state
      from: "off"
      to: "on"
      variables:
        blind: "{{trigger.to_state.object_id.removesuffix('_copy_optimal_state')}}"
        action: "open"
    - id: sun_bright
      platform: numeric_state
      entity_id: sensor.home_weather_dni
      above: 400
    - id: balcony_door_opened
      platform: state
      entity_id:
        - binary_sensor.aqara_bedroom_balcony_door
        - binary_sensor.aqara_salon_balcony_door_2
      from: "off"
      to: "on"

    - id: TimerIdle
      platform: state
      entity_id:
        - timer.blind_balcony_right
        - timer.blind_balcony_left
      from: "active"
      to: "idle"
      variables:
        blind: "{{trigger.to_state.object_id}}"
        action:
          "{{'close' if (is_state('binary_sensor.' ~ blind ~ '_optimal_state','off') or
          (is_state('binary_sensor.' ~ blind ~'_copy_optimal_state','off') and (is_state('binary_sensor.aqara_salon_balcony_door_2','off') or is_state('binary_sensor.aqara_bedroom_balcony_door','off') )))
          else 'open'}}"
    - id: "rain"
      platform: numeric_state
      entity_id: sensor.wu_holesovice_precipitation_rate
      above: "0.09"
    - id: "rain_stop"
      platform: numeric_state
      entity_id: sensor.wu_holesovice_precipitation_rate
      below: "0.09"
      for: "0:05:00"
  condition:
    - or:
        - condition: trigger
          id: "rain"
        - and:
            - condition: state
              entity_id: input_boolean.blind_sunscreener_enable
              state: "on"
  action:
    - choose:
        - alias: "It started raining: retract covers"
          conditions:
            - condition: trigger
              id: rain
          sequence:
            - service: cover.open_cover
              target:
                entity_id:
                  - cover.blind_balcony_left
                  - cover.blind_balcony_right
            - service: timer.start
              target:
                entity_id:
                  - timer.blind_balcony_right
                  - timer.blind_balcony_left
              data:
                duration: "00:04:55"
        - alias: "It stopped raining: close covers if sun in house, or on balcony and doors open"
          conditions:
            - condition: trigger
              id: "rain_stop"
          sequence:
            - repeat:
                for_each:
                  - "blind_balcony_right"
                  - "blind_balcony_left"
                sequence:
                  - if:
                      - "{{ is_state('timer.'~repeat.item,'idle')}}"
                      - or:
                          - "{{ is_state('binary_sensor.' ~ repeat.item ~ '_optimal_state','off')}}"
                          - and:
                              - "{{ is_state('binary_sensor.' ~ repeat.item ~ '_copy_optimal_state','off')}}"
                              - "{{ is_state('binary_sensor.aqara_bedroom_balcony_door', 'on') or is_state('binary_sensor.aqara_salon_balcony_door_2','on')}}"
                    then:
                      - service: cover.close_cover
                        target:
                          entity_id: "cover.{{repeat.item}}"
                      - service: timer.start
                        target:
                          entity_id: "time.{{repeat.item}}"
                        data:
                          duration: "00:30"
        - alias: Close covers if strong sun on balcony and balcony door has been opened
          conditions:
            - or:
                - and:
                    - condition: trigger
                      id: balcony_door_opened
                    - condition: numeric_state
                      entity_id: sensor.wu_home_weather_dni
                      above: 400
                - and:
                    - condition: trigger
                      id: sun_bright
                    - condition: state
                      match: any
                      entity_id:
                        - binary_sensor.aqara_bedroom_balcony_door
                        - binary_sensor.aqara_salon_balcony_door_2
                      state: "off"
          sequence:
            - repeat:
                for_each:
                  - "blind_balcony_right"
                  - "blind_balcony_left"
                sequence:
                  - if:
                      - "{{ is_state('binary_sensor.' ~ repeat.item ~ '_copy_optimal_state','off')}}"
                      - "{{ is_state('timer.' ~ repeat.item, 'idle')}}"
                    then:
                      - service: cover.close_cover
                        target:
                          entity_id: "cover.{{repeat.item}}"
                      - service: timer.start
                        target:
                          entity_id: "time.{{repeat.item}}"
                        data:
                          duration: "00:30"
        - alias: Otherwise, operate covers according to optimal state
          conditions:
            - "{{is_state('timer.' ~ blind, 'off')}}"
            - not:
                - or:
                    - alias: "But -- do not close covers if triggered by sun on balcony but doors closed or sun weak"
                      and:
                        - condition: trigger
                          id: sun_on_balcony
                        - or:
                            - "{{ is_state('binary_sensor.aqara_bedroom_balcony_door', 'off') and is_state('binary_sensor.aqara_salon_balcony_door_2','off')}}"
                            - condition: numeric_state
                              entity_id: sensor.home_weather_dni
                              below: 400
                    - alias: "Do not open covers if sun_off_window but still shining on balcony"
                      and:
                        - condition: trigger
                          id: sun_off_window
                        - "{{ is_state('binary_sensor.aqara_bedroom_balcony_door', 'off') or is_state('binary_sensor.aqara_salon_balcony_door_2','off')}}"
                        - "{{states('sensor.weather_home_dni')|float('not number')>400}}"
                        - "{{is_state('cover.'~blind ~ '_copy_optimal_state','off')}}"

          sequence:
            - service: "cover.{{action}}_cover"
              target:
                entity_id: "cover.{{blind}}"
            - service: timer.start
              target:
                entity_id: "timer.{{ blind }}"
              data:
                duration: "00:30"
  mode: parallel

Sorry to abuse the goodwill of the forums, but I keep getting myself into the same level of mess with any complicated automation. Following you advice above, I’ve managed to rewrite the complicated condition-sequence part using template triggers, leaving me with a simple sequence but very convoluted trigger conditions.

This is ok if I do it once, but now the price is that I cannot think how to iterate over the covers – in this example there are two covers, left and right. Then, as far as I can see, I need to have separate triggers for sun on and sun off (they are the same condition with a not pushed through, apart from the timer requirement to be idle which is the same for both).

Seems like there should be something smarter one could do?

- alias: "Blind: Sunscreener -- Balcony"
  description: "Move balcony blinds according to sun_screener, but take into account rain!"
  id: "urergdffdbddpteirytw3gtg"
  trigger:
    - id: sun_on_right
      platform: template
      value_template: >
        {{( 
            (is_state('binary_sensor.blind_balcony_right_optimal_state','off') 
                  and states('sensor.home_weather_dni')|float(0) > 200
            )
            or 
            ( (states('binary_sensor.aqara_bedroom_balcony_door','on') or states('binary_sensor.aqara_salon_balcony_door_2','on') ) 
              and is_state('binary_sensor.blind_balcony_right_copy_optimal_state','off') 
              and states('sensor.home_weather_dni')|float(0) > 400
            )
          )
          and states('sensor.wu_holesovice_precipitation_rate')|float(0) < 0.09
          and is_state('timer.blind_balcony_right','idle') }}
      variables:
        blind: "blind_balcony_right"
        action: "close"

    - id: sun_off_right
      platform: template
      value_template: >
        {{( 
            (is_state('binary_sensor.blind_balcony_right_optimal_state','on') 
                  or states('sensor.home_weather_dni')|float(300) < 200
            )
            and 
            (  (states('binary_sensor.aqara_bedroom_balcony_door','off') and states('binary_sensor.aqara_salon_balcony_door_2','off') ) 
              or is_state('binary_sensor.blind_balcony_right_copy_optimal_state','on') 
              and states('sensor.home_weather_dni')|float(500) <= 400
            )
          )
          and is_state('timer.blind_balcony_right','idle') }}
      variables:
        blind: "blind_balcony_right"
        action: "open"

    - id: sun_on_left
      platform: template
      value_template: >
        {{( 
            (is_state('binary_sensor.blind_balcony_left_optimal_state','off') 
                  and states('sensor.home_weather_dni')|float(0) > 200
            )
            or 
            ( (states('binary_sensor.aqara_bedroom_balcony_door','on') or states('binary_sensor.aqara_salon_balcony_door_2','on') ) 
              and is_state('binary_sensor.blind_balcony_left_copy_optimal_state','off') 
              and states('sensor.home_weather_dni')|float(0) > 400
            )
          )
          and states('sensor.wu_holesovice_precipitation_rate')|float(0) < 0.09
          and is_state('timer.blind_balcony_left','idle') }}
      variables:
        blind: "blind_balcony_left"
        action: "close"

    - id: sun_off_left
      platform: template
      value_template: >
        {{( 
            (is_state('binary_sensor.blind_balcony_left_optimal_state','on') 
                  or states('sensor.home_weather_dni')|float(300) < 200
            )
            and 
            (  (states('binary_sensor.aqara_bedroom_balcony_door','off') and states('binary_sensor.aqara_salon_balcony_door_2','off') ) 
              or is_state('binary_sensor.blind_balcony_left_copy_optimal_state','on') 
              and states('sensor.home_weather_dni')|float(500) <= 400
            )
          )
          and is_state('timer.blind_balcony_left','idle') }}
      variables:
        blind: "blind_balcony_left"
        action: "open"
    - id: "rain"
      platform: numeric_state
      entity_id: sensor.wu_holesovice_precipitation_rate
      above: "0.09"
  condition:
    - or:
        - condition: trigger
          id: "rain"
        - condition: state
          entity_id: input_boolean.blind_sunscreener_enable
          state: "on"
  action:
    - choose:
        - alias: "It started raining: retract covers"
          conditions:
            - condition: trigger
              id: rain
          sequence:
            - service: cover.open_cover
              target:
                entity_id:
                  - cover.blind_balcony_left
                  - cover.blind_balcony_right
            - service: timer.start
              target:
                entity_id:
                  - timer.blind_balcony_right
                  - timer.blind_balcony_left
              data:
                duration: "00:05"
      default:
        - alias: Sun status changed, move covers
          service: "cover.{{action}}_cover"
          target:
            entity_id: "cover.{{blind}}"
        - alias: start timers
          service: timer.start
          target:
            entity_id: "timer.{{ blind }}"
          data:
            duration: "00:30"
  mode: parallel