The real challenge in home automation is not YAML, Jinja, Helpers or Flow editors — it is complexity

Posted identically in the Homey Community

The more I work on sophisticated automations, the more I realise that neither YAML, Jinja, Helpers, Home Assistant’s visual editor nor Homey’s Advanced Flow are the real challenge.

The real challenge is complexity — and trying to foresee every possible interaction and edge case.

My roller shutter system is a good example.

It has several operating modes:

  • Manual

  • Manual (temporary)

  • Sun protection

  • TV

  • Sun protection + TV

And each mode behaves differently.

The sun-protection logic doesn’t simply check whether the sun is shining. Depending on the window, it evaluates solar azimuth, solar elevation and luminance to determine whether that window actually needs protection.

On colder days, it becomes more complex. The system compares the temperature measured on the sun-exposed facade with a sensor in the shade. From the temperature difference, it estimates whether the sun currently has enough heating power to justify closing the shutter.

In bedrooms, the logic changes again depending on whether the window is open or closed.

With the window closed, the shutter primarily provides sun protection.

With the window open, indoor and outdoor temperatures are compared and the shutter effectively becomes part of the ventilation control, adjusting how much air exchange is possible.

There is also winter protection: if the room gets too cold while a window is open, the shutter closes completely to slow further cooling.

Then there are exceptional situations.

If a smoke alarm is triggered, all shutters open to clear possible escape routes. A central override prevents any other automation from closing them again while the alarm is active.

Manual operation also has priority. If somebody operates a shutter using the physical switch, that window changes to Manual (temporary) and normal automations leave it alone. At 03:00, it automatically returns to its previous mode.

All of this is already quite complex, and I honestly don’t think I could have built and maintained the complete logic without AI helping me analyse it.

But then my son found an edge case that neither I nor the AI had considered.

He was doing homework when the sun-protection automation started lowering the shutter. He didn’t want it closed, so he stopped it while it was moving and then raised it again manually.

A few minutes later, the shutter went down again.

Normally, manually moving a shutter should activate Manual (temporary) and prevent sun protection from touching it again.

So what happened?

The automation had marked the downward movement as an automatic movement. That flag was supposed to be cleared when the shutter reached its target position.

But my son interrupted the movement before it arrived there.

The flag therefore remained set.

When he subsequently raised the shutter manually, the system still believed that the movement belonged to the automation and did not activate Manual (temporary).

Technically, every part of the logic worked exactly as designed.

The problem was that we had never considered the sequence:

Automatic movement → manually interrupted → manual movement in the opposite direction.

We changed the logic so that interrupted movements are handled correctly as well.

And that brought me to what I now think is the real difficulty of sophisticated home automation:

Building the automation is not necessarily the hardest part. Foreseeing every possible state, interruption, timing issue and interaction between automations is.

AI helps enormously, but even AI doesn’t automatically think of every real-world situation that someone in the house may create.

So I’m curious how others approach this.

Do you also build increasingly sophisticated automation logic and deal with these edge cases as they appear?

Or do you deliberately keep your automations simpler and accept giving up some of the “ultimate” automation experience in exchange for easier maintenance and predictability?

On the rare occasion when an edge case becomes significant I adjust the automation to take that into account.

Doing too much in a single automation does complicate this but then so does having a number of smaller interacting automations. I have a mix of both. Some are appallingly written from my early days of Home Assistant. I occasionally revise them. Creating template sensors or binary sensors can help simplify some things.

One thing is for sure, graphical editing very complex automations makes this harder not easier. For me YAML is easier for more complex automating.

Agreed!

I use a standardised algorithm to decide the position of my shades based on sun intensity, outdoor temp, brightness, window open/closed etc etc. It’s a custom_template called by a sensor (one for each shade) which then triggers the simplest of automations.

The custom_template looks like this:

{% macro shade_level(shade, dir, contact) %}
  {%- set open    = is_state('binary_sensor.' ~ shade ~ '_' ~ contact ~ '_contact','on') %}
  {#- night #}
  {%- set dark    = is_state('sensor.outside_light_30s','Night') %}
  {%- set asleep  = is_state('input_select.house_mode','Asleep') %}
  {%- set sundown = is_state('binary_sensor.dusk_to_dawn','on') %}
  {# v hot #}
  {%- set vhot    = states('sensor.weather_temp_max')|float(15) >= states('input_number.shades_temp_2')|int %}
  {%- set high_UV = states('sensor.weather_uv_index')|int(4) >= states('input_number.shades_uv_trigger')|int %}
  {%- set bright  = is_state('input_boolean.shades_bright','on') %}
  {%- set in_sun  = is_state('binary_sensor.shades_' ~ dir ~ '_has_sun','on') %}
  {# hot #}
  {%- set hot     = states('sensor.weather_temp_max')|float(15) >= states('input_number.shades_temp_1')|int %}
  {#- set clear_sky  = not is_state('sensor.weather_condition',['partly cloudy','cloudy']) #}
  
  {#- Close if 'night' or asleep with sundown lock for torches/headlights #}
  {%- if dark or asleep or sundown %}                                   {{ iif(open,50,0) }}

  {#- v hot: Close when in sun #}
  {%- elif vhot and in_sun and high_UV and bright %}                    {{ iif(open,50,0) }}

  {#- hot: Only south closes when in sun #}
  {%- elif hot and in_sun and high_UV and bright and dir == 'south' %}  {{ iif(open,50,0) }}

  {%- else %}  100
  {%- endif %}
{% endmacro %}

Each sensor calls it like this and provides its room, aspect & window sensor:

      - name: "Kitchen shade target position"
        unit_of_measurement: "%"
        state: >
          {% from 'shades.jinja' import shade_level %}
          {{ shade_level('kitchen', 'south', 'window') }}

…which then triggers the simplest of automation:

  - alias: "Shades - control"
    id: shades_open_close
    description: ''
    mode: parallel
    triggers:
      - trigger: state
        entity_id:
          - sensor.bedroom_1_shade_target_position
          - sensor.kitchen_shade_target_position
          - sensor.livingroom_shade_target_position
          - sensor.orangery_ctr_shade_target_position
          - sensor.orangery_lh_shade_target_position
          - sensor.orangery_rh_shade_target_position
          - sensor.study_shade_target_position
        not_from:
          - unknown
          - unavailable
        not_to:
          - unknown
          - available
        for:
          seconds: 1 # remove blips % spikes
    conditions:
      - "{{ states('binary_sensor.restarting') == 'off' }}"
      - "{{ states('binary_sensor.reloading') == 'off' }}"
      - "{{ states('input_select.house_mode') != 'Manual' }}"
    actions:
      - action: cover.set_cover_position
        target:
          entity_id: cover.{{ trigger.entity_id[7:-16] }}
        data:
          position: "{{ trigger.to_state.state }}"

It requires the following entities to exist for each shade (where x is the shade name):

cover.x_shade # the shade itself
binary_sensor.x_contact # associated door/window contact sensor
input_boolean.x_shade_automation_enabled. # disable any given shade automation
input_number.x_shade_night_position # night shade position
sensor.x_shade_reported_position # as reported by the shade
sensor.x_shade_target_position

As a result, I haven’t had to touch a shade in years, but it took a little thinking through to get right. There is one change to one shade for when the TV is on to ensure light does not reflect on it as follows:

      - name: "Orangery RH shade target position"
        unit_of_measurement: "%"
        state: >
          {% from 'shades.jinja' import shade_level %}
          {% set shade_level = shade_level('orangery_rh', 'south', 'door')|int(0) %}
          {% set apple_tv = states('media_player.livingroom_appletv') %}
          {% set apple_tv_privacy = states('input_boolean.livingroom_appletv_privacy_control') %}
          {% if apple_tv == 'off' %} {{ shade_level }}
          {% elif apple_tv_privacy == 'on' and shade_level > 70 %} 70
          {% else %} {{ shade_level }}
          {% endif %}

The custom_template also needs to know if the sun is on any given window so I have 3 of the following (South, West, North):

# South is 150°±60° = 90°➜210° ➜ 90°➜245°
- name: "shades south has sun"
  state: >
    {{
      states('sensor.home_sun_azimuth')|float(150) >= 90
      and states('sensor.home_sun_azimuth')|float(150) <= 245
      and states('sensor.home_sun_elevation')|float(45) >5
    }}
  icon: "{{ iif(states('binary_sensor.shades_south_has_sun') == 'on', 'mdi:weather-sunny', 'mdi:umbrella-outline') }}"

It’s the only approch I see. As foreseeing every edge case is even not possible with AI :wink:

I have a lot of different automations.

  • One for every window to make them maintainable.
  • One for Smoke detection.
  • A script to set automatic moves and distinguish them from manual moves.
  • Many helpers, like Temperature in the shadow, and many other template helpers
  • One for daily opening and closing in the morning or the evening
  • Many others :wink:

That looks great.

I also use separate automations per window and some template helpers.

My approach is probably a little different from yours, though. I try to avoid YAML and Jinja as much as reasonably possible and keep the logic in the visual editor.

I only use YAML or Jinja when the purely visual approach would become unnecessarily complex or would require too many Helpers just to achieve the same result.

1 Like

yes, I rather enjoy using yaml for everything. Horses for courses.

I have a pretty useful approach that works for me: abstraction.

Plus, I expect that as my automation tasks get more involved there’s going to be edge cases I miss.

This is programming so follow good programming techniques. Single function and short automations.

I think in terms of triggers (events) which have ideally one action. After all, there’s nothing to do if nothing changes. Then use a condition to limit that trigger.

I use template sensors (often many) to build the condition to use. Triggered template sensors act as mini automations where their state is the resulting action. If a template gets complex (nesting is the big red flag) I split up.

Sometimes actions get complex and then I move that into a script (keeps automations to a single action) and scripts are easier to test.

I use a lot of timers for delaying actions. Automations rarely wait - mostly limited to testing if an action really worked.

Everything in as few lines as possible.

The I use packages to group all that closely related code together in one place. Packages are key to keeping it sane.

Just like if you were writing complex code.