Deprecated: Using ${var} in strings is deprecated, use {$var} instead in /home/public/blog/grav-admin/user/plugins/responsive-images/twig/ResponsiveImagesExtension.php on line 674
Home Assistant's Logic-Lacking Variables | Matthew Miner's Blog

Matthew Miner's Basic-ish BlogMatthew Miner's Blog

Sometimes I might say something

The whole point of home automation is to do things conditionally. Maybe you want to change the light color depending on the time of day. Maybe you want to play a noise when someone hits a button. Or, in my case, I want to adjust the thermostat based on the temperature and people's locations. Regardless of what you want to do, the go-to software is the mostly-lovely Home Assistant. It makes it easy to integrate all your devices with whatever setup you want and then lets you run automations to…automate them.

Typically automations are controlled by simple triggers. If a button is pushed, then do this. If the temperature changes, then do this. Home Assistant's GUI makes this fairly easy to set up, but it's also pretty limited. You can set triggers, conditions, and actions, but that doesn't let you really program things. It has a simple if-then action, but limited to this, it's hard to do things with more than a couple possibilities. It would be impractical to make many, many branches to do something like gradually change the color and brightness of a light based on the time of day.

Home Assistant's solution to this problem is its scripts. However, this name is misleading. It's not really a scripting language like one may think of like bash or Python or PHP or Javascript. For instance, to setup my thermostat to be 73° in the day, 3° cooler at night, and 10° less when we're not home, I would naturally write something like this:

from datetime import datetime

target_temperature = 73
if (datetime.now() > datetime.strptime("7:00AM", "%I:%M%p") and
    datetime.now() < datetime.strptime("10:00PM", "%I:%M%p")):
    target_temperature -= 3
if state.zones.home == 0:
    target_temperature -= 10
climate.set_temperature(target_temperature)

This is very simple and straight-forward logic. You set the temperature you want and then adjust it. It would be easy to put a conditional in there to set it to hot or cool depending on the current temperature and change the initial target temperature accordingly. You could easily add another intermediate zone with a different temperature change.

Scripts cannot easily do this. They are not very different from automations really. They're just YAML files with a series of commands and optionally conditions. One of these commands is setting a variable, but it has serious limitations. You might naturally think you could implement the same thing with the following:

sequence:
  - variables:
      target_temperature: 73
  - if:
      - condition: template
        value_template: "{{ now().hour >= 7 and now().hour < 22 }}"
    then:
      - target_temperature: "{{ target_temperature - 3 }}" 
  - if:
      - condition: template
        value_template: "{{ int(states('zone.home')) == 0 }}"
    then:
      - target_temperature: "{{ target_temperature - 10 }}"
  - service: climate.set_temperature
    data:
      temperature: "{{ float(target_temperature) }}"
    target:
      device_id: "{{ device_id }}"

Sure it's horribly ugly and a pain to write, but that should at least do what you want, right? Nope, that script will always set the temperature to 73° without even a warning. If you try setting target_temperature in an if statement, you'll get an error. How scripts handle scope is very unintuitive. Variables are only defined for their scope, and the then statement is considered its own scope. So any variables defined in a then statement can only be used therein. Furthermore, you cannot actually modify variables. If you set the same variable again, you're just creating a new variable, essentially shadowing the previously-defined version. Once the if-then block ends, so does your new variable.

My next thought was to create a separate script as a function to get the temperature differential I wanted for my complicated rules about zones. Then I figured I could define it at the first level in one script and return it from an if statement in another like this:

sequence:
  - service: script.get_temperature_drift
    data: {}
    response_variable: temperature_drift
  - variables:
      target_temperature: "{{ 73 - float(temperature_drift.value) }}"
sequence:
  - if:
      - condition: template
        value_template: "{{ int(states('zone.home')) == 0 }}"
    then:
      - variables:
          target_temperature:
            value: "-10"
      - stop: No one is home
        response_variable: temperature_drift

However, the other script will always return an empty dictionary ({}). I can't find any documentation of this behavior, and I assume it's a bug, but the point is pretty clear that Home Assistant's if-then statements are almost useless. They're for final evaluations of data only. You cannot exfiltrate any data out of them.

I worried that would be the end of my dream and that I would just have to write a program to generate a monstrous tree of if-then statements for every combination. However, I then noticed that Home Assistant does have (possibly unintentionally) a redundant if statement, which turns out to be actually useful. Somewhat horrifically this is Jinja2's if filter in its string templates.

So to put logic in a script, rather than saying "if x: y = z", you instead need to say "y = {{ z if x }}". It's a bit mind-wrinkling, but it is doable. To implement the original logic, you then need to do the following:

sequence:
  - variables:
      night_cool: "{{ 0 if now().hour >= 7 and now().hour < 22 else 3 }}"
      temperature_drift: "{{ 0 if int(states('zone.home')) > 0 else 10 }}"
      target_temperature: "{{ 73 - temp_drift - night_cool }}"

Essentially Jinja2 is the real programming language. It's admittedly succinct for simple cases. It grows very confusing though as your logic grows. For example, to add more zones and hot/cold, instead of repeating if-else statements, I instead had to write this monstrosity:

temperature_drift: >-
  {{ 0 if int(states('zone.home')) > 0 else 1 if int(states('zone.near_home')) > 0 else 3 if int(states('zone.city')) > 0 else 6 if int(states('zone.20_miles')) > 0 else 13 }}
night_cool: "{{ 0 if now().hour >= 7 and now().hour < 22 else 3 }}"
mode: "{{ \"heat\" if indoor_temperature < 74 - night_cool else \"cool\" }}"
target_temperature: >-
  {{ 73 - temperature_drift - night_cool if mode == 'heat' else 75 + temperature_drift - night_cool }}

Again, this technically works and is how you have to implement it. If you want to program your smart devices, you'll have to get used to it's scrambled thinking. To add salt to the wound, since Jinja2 is a string templating language, this means anytime you want to do anything in an automation or script, you essentially have to convert your data to strings, then cast it back. It's honestly a massive headache, and it's hard to see it as anything but a failure by Home Assistant's team.

Previous Post