Mapping list of entities to entity_id in automation produces huge list of Undefined

I’m trying to pass a list of entity_ids to the target field of an action, but am running into a very basic problem:

actions:
  - variables:
      thermostats: |
        {{ [ states.climate.mbr_thermostat,
             states.climate.mbr_bath_thermostat ] }}
  - variables:
      active_thermostats: "{{ thermostats | map(attribute='entity_id') | list }}"
  - action: climate.set_fan_mode
    metadata: {}
    data:
      fan_mode: Low
    target: "{{ {'entity_id': active_thermostats } }}"

I split the variable definitions into two different actions so I could see each step in the trace. The first step produces the trace output I would expect:

thermostats: >-
  [<template TemplateState(<state climate.mbr_thermostat=cool;
  hvac_modes=[<HVACMode.OFF: 'off'>, <HVACMode.HEAT: 'heat'>, <HVACMode.COOL:
  'cool'>], min_temp=32.0, max_temp=122.0, fan_modes=['Auto low', 'Low',
  'Circulation'], preset_modes=['none', 'Energy heat', 'Energy cool'],
  current_temperature=74, temperature=75, current_humidity=47, fan_mode=Low,
  hvac_action=idle, preset_mode=none, fan_state=Running / running low,
  friendly_name=Master Bedroom Bedroom Thermostat , supported_features=409 @
  2025-05-17T12:39:46.987462-04:00>)>, <template TemplateState(<state
  climate.mbr_bath_thermostat=off; hvac_modes=[<HVACMode.OFF: 'off'>,
  <HVACMode.HEAT: 'heat'>, <HVACMode.COOL: 'cool'>], min_temp=32.0,
  max_temp=122.0, fan_modes=['Auto low', 'Low', 'Circulation'],
  preset_modes=['none', 'Energy heat', 'Energy cool'], current_temperature=74,
  temperature=None, current_humidity=48, fan_mode=Low, hvac_action=idle,
  preset_mode=none, fan_state=Running / running low, friendly_name=Master
  Bedroom Bathroom Thermostat , supported_features=409 @
  2025-05-17T12:39:46.973856-04:00>)>]

but the second step gives

active_thermostats: >-
  [Undefined, Undefined, Undefined, Undefined, Undefined, Undefined, Undefined,
  Undefined, Undefined, Undefined, Undefined, Undefined, Undefined, Undefined,
  <snip 150 more lines of the same>

If my second variable is just active_thermostats: "{{ thermostats }}", that produces a second copy of the list as you would expect, so it’s able to access the thermostats variable. And if I tack on | map(attribute='entity_id') | list to my first variable, it produces the list entity_ids I expect. When I split them out, though, I get the huge mass of Undefineds.

  1. What’s going on here?
  2. Is there a better way to pass a list to the target of an action?

Is the code you posted what you ultimately want to do or is it something contrived for example purposes?

The reason why I ask is because the templates perform unnecessary steps in their attempt to produce a list of entity_ids

BTW, script variables are particular about what kind of data types they can store. For example, if you attempt to store a datetime object in a script variable,
it won’t be preserved as a datetime object. The same thing is happening in your example. The value of thermostats isn’t preserved as a list of objects so that’s why when it’s referenced in active_thermostats the result is a verbose failure.

To illustrate what I mean, test this version. It also has unnecessary steps but avoids storing an object in a script variable (it uses a list of strings).

actions:
  - variables:
      thermostats: |
        {{ [ 'climate.mbr_thermostat',
             'climate.mbr_bath_thermostat' ] }}
  - variables:
      active_thermostats: "{{ thermostats | expand | map(attribute='entity_id') | list }}"
  - action: climate.set_fan_mode
    metadata: {}
    data:
      fan_mode: Low
    target:
      entity_id: "{{ active_thermostats }}"

Using a list of entity_ids and expanding it works! I appreciate the help, I never would have discovered that on my own. It was working just fine in dev tools, but breaking as soon as I adapted it for the automation. Utterly maddening.

The example was heavily reduced from what I was actually trying to do. Here’s my full (now working, thanks to you!) automation. Suggestions gratefully accepted! I’m a programmer, but fairly new to Home Assistant.

For context, when I set my thermostats fan mode to “Low”, that causes the damper on the zone to open iff none of them are actively calling for conditioned air. However, just because none of them are calling for conditioned air does not mean the air handler isn’t producing it, so I am looking to open and close the vents to direct the air flow to the rooms that need it most.

“Auto low” causes the damper to close (again, iff there’s no active demand). If all thermostats are set to “Auto low”, the zone board will open all of the dampers.

actions:
  - variables:
      thermostats: |
        {{ [ 'climate.mbr_thermostat',
             'climate.mbr_bath_thermostat' ] }}
      active_thermostats: >
        {% set cooling = thermostats
           | expand
           | selectattr('state', 'equalto', 'cool')
           | list %}
        {% set heating = thermostats
           | expand
           | selectattr('state', 'equalto', 'heat')
           | list %}
        {% set ns = namespace(items=[]) %}

        {# Calculate how far away each thermostat is from its setpoint. #}
        {# Positive numbers represent unsatisfied demand. Negative numbers represent overshoot. #}
        {% if cooling | length != 0 %}
          {% for thermostat in cooling %}
            {% set ns.items = ns.items + [ {'thermostat': thermostat, 'differential': thermostat.attributes.current_temperature - thermostat.attributes.temperature } ] %}
          {% endfor %}
        {% elif heating | length != 0 %}
          {% for thermostat in heating %}
            {% set ns.items = ns.items + [ {'thermostat': thermostat, 'differential': thermostat.attributes.temperature - thermostat.attributes.current_temperature } ] %}
          {% endfor %}
        {% else %}
          {% for thermostat in thermostats | expand %}
            {% set ns.items = ns.items + [ {'thermostat': thermostat, 'differential': 0 } ] %}
          {% endfor %}
        {% endif %}

        {# Find the largest unsatisfied demand. #}
        {# We allow thermostats at their setpoint, as ideally all thermostats will be here. #}
        {% set max_differential =
             ns.items
             | sort(attribute='differential', reverse=True)
             | map(attribute='differential')
             | select('ge', 0)
             | first
        %}
        {# If all thermostats have overshot, this will be the empty list #}
        {{ ns.items
           | selectattr('differential', 'equalto', max_differential)
           | map(attribute='thermostat')
           | map(attribute='entity_id')
           | list
        }}
      passive_thermostats: |
        {{ thermostats
           | expand
           | rejectattr('entity_id', 'in', active_thermostats)
           | map(attribute='entity_id')
           | list
        }}
  - action: climate.set_fan_mode
    metadata: {}
    data:
      fan_mode: Low
    target:
      entity_id: "{{ active_thermostats }}"
  - action: climate.set_fan_mode
    metadata: {}
    data:
      fan_mode: Auto low
    target:
      entity_id: "{{ passive_thermostats }}"

A few suggestions:

You can define the value of the thermostats variable like this (as a list defined in YAML as opposed to Jinja2).

actions:
  - variables:
      thermostats:
        - climate.mbr_thermostat
        - climate.mbr_bath_thermostat

You can simplify the templates for cooling and heating variables.

        {% set cooling = thermostats
           | select('is_state', 'cool')
           | list %}
        {% set heating = thermostats
           | select('is_state', 'heat')
           | list %}
         ... etc ...

The template for passive_thermostats can be reduced to this:

      passive_thermostats: |
        {{ difference(thermostats, active_thermostats) }}

Only the suggestion for changing the definition of thermostats worked for me out of the box.

Changing cooling and heating gave me the following error in the trace log: Error: UndefinedError: 'annotatedyaml.objects.NodeStrClass object' has no attribute 'attributes' . Adding expand to the mix (which does seem to still be necessary) gives Error: TypeError: unhashable type: 'TemplateState'.

Similarly, changing passive_thermostats gave me Error: TypeError: difference expected a list, got str. After some fiddling, I got {{ difference(thermostats | expand | map(attribute='entity_id'), active_thermostats) }} to work.

All three of suggestions were tested on my system with sample data and confirmed to work.

The UndefinedError refers to attributes but what I suggested doesn’t refer to attributes. It’s like you didn’t feed the template a list of entity_ids but something else.

You can’t use expand there.

Regarding TypeError, that suggestion was designed to compare the differences between two lists. You supplied it with a string. Once again, something isn’t right with the data being fed into the suggested templates.

The final example you posted of what works is the same redundancy you had used originally. It’s like doing 1 × 1 + 1 × 1 = 2 to simply add 1 to 1.

Anyways, if it works, all is well.

I found the problem. Template not working when select('is_state', 'on') is used - #2 by michaelblight clued me in to the fact that is_state was expecting an entity_id. I think you meant to suggest

actions:
  - variables:
      thermostats:
        - 'climate.mbr_thermostat'
        - 'climate.mbr_bath_thermostat'

? I think that’s the case – that would be consistent with your previous suggestion to use entity_ids. I was able to make your is_state and difference suggestions work with that.

There were only two small additional changes required

  1. Updating my temperature difference calculation to thermostat | state_attr('current_temperature') - thermostat | state_attr('temperature'), since I was now operating on an entity_id rather than the thermostat itself.
  2. Removing the | map(attribute='entity_id') in the value template.

So I ended up with this:

variables:
  thermostats:
    - 'climate.mbr_thermostat'
    - 'climate.mbr_bath_thermostat'
  active_thermostats: >
    {% set cooling = thermostats | select('is_state', 'cool') | list %}
    {% set heating = thermostats | select('is_state', 'heat') | list %}
    {% set ns = namespace(items=[]) %}

    {# Calculate how far away each thermostat is from its setpoint.
       Positive numbers represent unsatisfied demand. Negative numbers represent overshoot. #}
    {% if cooling | length != 0 %}
      {% for thermostat in cooling %}
        {% set ns.items = ns.items + [ {'thermostat': thermostat, 'differential': thermostat | state_attr('current_temperature') - thermostat | state_attr('temperature') } ] %}
      {% endfor %}
    {% elif heating | length != 0 %}
      {% for thermostat in heating %}
        {% set ns.items = ns.items + [ {'thermostat': thermostat, 'differential': thermostat | state_attr('temperature') - thermostat | state_attr('current_temperature') } ] %}
      {% endfor %}
    {% endif %}

    {# Find the largest unsatisfied demand.
       We allow thermostats at their setpoint, as ideally all thermostats will be here. #}
    {% set max_differential =
         ns.items
         | sort(attribute='differential', reverse=True)
         | map(attribute='differential')
         | select('ge', -2)
         | first
    %}
    {# If all thermostats have overshot, this will be the empty list #}
    {{ ns.items
       | selectattr('differential', 'equalto', max_differential)
       | map(attribute='thermostat')
       | list
    }}
  passive_thermostats: |
    {{ difference(thermostats, active_thermostats) }}

No, it isn’t necessary to wrap the entity_ids in quotes.

The entity_id begin with an alphabetical character so the YAML processor readily understands that it’s a string.

FWIW, I tested the suggested thermostats variable and, when referenced by a template, it was successfully handled as a list of strings

You are correct, of course. It’s all working now without the quotes. Not sure what I was doing wrong earlier. Really appreciate all the help and explanation.

1 Like