Defining variable once

I’m trying to improve my templating skills, so this is more a general templating question than a specific issue to resolve.

My questions are:

  1. How to defined a list of entities to check in one place
  2. Understand how HA determines what entities to “listen” to for changes.

I created a binary_sensor template to tell me if my house is all locked up. It’s a “lock” device_class and has a “message” attribute will say what sensors are unlocked:

When a doors are opened:

But, I’m hard-coding the entities I want to check - twice.

I had to replace a sensor and I only updated one of the lists by mistake and didn’t notice. Bad practice to define the same thing in multiple places, of course.

In my template I define an array of entities to check (and the state to check against):

binary_sensor template:
  - binary_sensor:
    - name: House Locked
      device_class: lock
      state: >
        {%- set sensors = [
          ['binary_sensor.front_door',                  'off'],
          ['binary_sensor.family_sliding_door',         'off'],
          ['binary_sensor.living_sliding_door',         'off'],
          ['binary_sensor.kitchen_deck_door',           'off'],
          ['binary_sensor.primary_bed_sliding_door',    'off'],
          ['binary_sensor.single_garage_open_intrusion','off'],
          ['binary_sensor.double_garage_open_intrusion','off'],
          ['lock.front_door_deadbolt',                  'locked'],
          ['cover.ratgdov25i_f615a8_door',              'closed'],
          ['cover.ratgdov25i_fa8ef5_door',              'closed']
        ] -%}
        {%- set ns = namespace(locked='off') -%}
        {% for entity in sensors -%}
          {% if states( entity[0] ) != entity[1] -%}
            {% set ns.locked = 'on' -%}
          {% endif -%}
        {% endfor %}
        {{ ns.locked }}
      attributes:
        message: >-
          {% set sensors = [
            ['binary_sensor.front_door',                  'off'],
            ['binary_sensor.family_sliding_door',         'off'],
            ['binary_sensor.living_sliding_door',         'off'],
            ['binary_sensor.kitchen_deck_door',           'off'],
            ['binary_sensor.primary_bed_sliding_door',    'off'],
            ['binary_sensor.single_garage_open_intrusion','off'],
            ['binary_sensor.double_garage_open_intrusion','off'],
            ['lock.front_door_deadbolt',                  'locked'],
            ['cover.ratgdov25i_f615a8_door',              'closed'],
            ['cover.ratgdov25i_fa8ef5_door',              'closed']
          ] -%}
          {% set ns = namespace(unlocked=[]) -%}
          {% for entity in sensors -%}
            {% if states( entity[0] ) != entity[1] -%}
              {% set ns.unlocked =
                ns.unlocked
                + [state_attr(entity[0], 'friendly_name')|trim
                + ': '
                + state_translated(entity[0])] -%}
            {% endif -%}
          {% endfor -%}
          {% if ns.unlocked|length -%}
            {{ ns.unlocked| join(', ') }}
          {% else -%}
            House is locked
          {% endif -%}

What’s a good way to define my set of entites to test but in just one place?

I’m a bit surprised, but it seems HA figures out which entities to watch for. Does it work the same when I define the binary_sensor in configuration.yaml?

I thought about doing something crazy like find all the entities by looking at their device_class, but the developer templates says This template listens for all state changed events. which seems like a very bad approach. I assume that means any state change re-runs this template code.

Searching for entities by device_class
{% set ns = namespace(entities=[]) -%}
{% for entity_type in [
  ['door','off'],
  ['garage_door','off'],
  ['garage','closed']
]  -%}
  {% for entity in 
      states
      |selectattr('attributes.device_class', 'eq', entity_type[0])
      |map(attribute='entity_id') -%}
    {% if states( entity ) != entity_type[1] %}
      {% set ns.entities = ns.entities + 
        [
          state_attr(entity, 'friendly_name')|trim
          + ': '
          + state_translated(entity)
        ] -%}
    {% endif %}
  {% endfor -%}
{% endfor -%}
---> 
{{ ns.entities|join("\n") }}

This also seems to confuse the developer UI as the right side will revert to some previous templating after a few seconds. Plus, I have a lock entity that doesn’t have a device_class.

Is there a better approach when I need to know the entity and the state to test against?

Thanks,

Is this any help?

Yes, that does help. Doesn’t seem like returning a data structure is possible (w/o maybe serializing it into json), but can accomplish having the list of entities in one place.

Perhaps there’s a cleaner way, but my template sensor now looks like:

 - binary_sensor:
    - name: House Locked
      device_class: lock
      state: >
        {% from 'door_sensors.jinja' import door_state %}
        {{ door_state() }}
      attributes:
        message: >
          {% from 'door_sensors.jinja' import door_message %}
          {{ door_message() }}

Those are “helper” macros that simply do:

{% macro door_state() %}
  {{ sensor_list(true) }}
{% endmacro %}

{% macro door_message() %}
  {{ sensor_list(false) }}
{% endmacro %}

And then finally, I just combined the code for the state and the attribute into one macro:

Combined Macro
{% macro sensor_list(set_state) %}
  {%- set sensors = [
    ['binary_sensor.front_door',                  'off'],
    ['binary_sensor.family_sliding_door',         'off'],
    ['binary_sensor.living_sliding_door',         'off'],
    ['binary_sensor.kitchen_deck_door',           'off'],
    ['binary_sensor.primary_bed_sliding_door',    'off'],
    ['binary_sensor.single_garage_open_intrusion','off'],
    ['binary_sensor.double_garage_open_intrusion','off'],
    ['lock.front_door_deadbolt',                  'locked'],
    ['cover.ratgdov25i_f615a8_door',              'closed'],
    ['cover.ratgdov25i_fa8ef5_door',              'closed']
  ] -%}
  {% if set_state -%}
    {%- set ns = namespace(locked='off') -%}
    {% for entity in sensors -%}
      {% if states( entity[0] ) != entity[1] -%}
        {% set ns.locked = 'on' -%}
      {% endif -%}
    {% endfor %}
    {{ ns.locked }}
  {% else -%}
    {% set ns = namespace(unlocked=[]) -%}
    {% for entity in sensors -%}
      {% if states( entity[0] ) != entity[1] -%}
        {% set ns.unlocked =
          ns.unlocked
          + [state_attr(entity[0], 'friendly_name')|trim
          + ': '
          + state_translated(entity[0])] -%}
      {% endif -%}
    {% endfor -%}
    {% if ns.unlocked|length -%}
      {{ ns.unlocked| join(', ') }}
    {% else -%}
      House is locked
    {% endif -%}
  {% endif %}
{% endmacro %}

There’s probably a cleaner approach, but that’s for another time.

Thanks.

Yeah, that is an inefficient approach, and unnecessary. There are dozens of threads where other users have come looking for a similar “set it and forget it”, dynamic entity list but in my experience they usually end up requiring just as much code management… you just spend the time adding entities to the “reject” list instead of the “select” list or assigning custom attributes.

Here's one approach using a group and two template sensors
group:
  kitchen:
    name: "Door Lock Group"
    entities:
      - binary_sensor.front_door
      - binary_sensor.family_sliding_door
      - binary_sensor.living_sliding_door
      - binary_sensor.kitchen_deck_door
      - binary_sensor.primary_bed_sliding_door
      - binary_sensor.single_garage_open_intrusion
      - binary_sensor.double_garage_open_intrusion
      - lock.front_door_deadbolt
      - cover.ratgdov25i_f615a8_door
      - cover.ratgdov25i_fa8ef5_door

template:
  - sensor:
      - name: House Locked Count
        state: |
          {{ state_attr('group.door_lock_group', 'entity_id') 
          | select('is_state', ['off' ,'closed', 'locked']) | list | count }}
        availability: "{{ has_value('group.door_lock_group') }}"
  - trigger:
      - platform: state
        entity_id: sensor.house_locked_count
        not_to:
          - unknown
          - unavailable
        variables:
          ent_list: "{{ state_attr('group.door_lock_group', 'entity_id') }}"
          locked: "{{ ent_list | select('is_state', ['off' , 'closed', 'locked']) | list }}" 
    binary_sensor: 
      - name: House Locked
        device_class: lock
        state: |
          {{ locked | count == ent_list | count  }}
        attributes:
          message: |
            {% if locked | count == ent_list | count %}
              House is locked
            {% else %} 
              {% set ex_open = expand(ent_list | reject('in', locked) | list ) %}
              {% set ns = namespace(unlocked=[]) -%}
              {% for entity in ex_open -%}
                {% set ns.unlocked = ns.unlocked + [ (entity.attributes.friendly_name | trim, state_translated(entity.entity_id) )] -%}
              {% endfor -%}
              {{ dict(ns.unlocked) }}
            {% endif -%}

You still have to update the group members manually, but only in one place.

This is great. It really helps to see more advanced configuration examples and then revisit the documentation again. Thank you! I’ll have time in a few days to dig into this again.

My earliest iteration of this was to check for things that were “unlocked” (etc.) but quickly realized what I really want to know is if things are locked.

If I understand correctly, having the trigger fire except when it changes to unknown or unavailable will result in the sensor not updating when that happens, correct?

In my case the important thing is knowing all listed sensors are reporting locked or closed. In that case I would want to trigger on all changes. Or does that not_too: server another purpose?

Thanks again!

It is an error avoidance method.

In this case the entity being watched for the trigger is an intermediary between the actual locks and doors and the final “Is everything locked” binary sensor. At restart, you’re likely to get a state change from “unknown” to “unavailable” which would trigger the binary sensor, but the entities referenced in the template aren’t necessarily going to be loaded. And, since it is a state-based template sensor, the “unavailable” state should be transitory and, IMO, adds limited value as a trigger.