Avoid duplicating template sensors using YAML anchors and this.entity_id

I recently extracted a small pattern from my Home Assistant YAML configuration that might be useful to others.

Self-parameterized template sensors

When writing template sensors for multiple rooms, it’s common to end up duplicating the same logic:

- name: Temperature Level Bedroom
  state: >
    {% set t = states('sensor.temperature_bedroom') | float(none) %}
    {% if t is none %} unknown
    {% elif t < 18 %} cold
    {% elif t < 24 %} ok
    {% elif t < 27 %} warm
    {% else %} hot {% endif %}

- name: Temperature Level Living Room
  state: >
    {% set t = states('sensor.temperature_living_room') | float(none) %}
    {% if t is none %} unknown
    {% elif t < 18 %} cold
    ...

As soon as the logic becomes slightly more complex, this duplication becomes hard to maintain.


The pattern

Instead of hardcoding the source entity, the template derives it from its own entity_id:

entity name → determines dependencies → runs generic logic

This effectively turns the entity naming convention into configuration.

{% set room = this.entity_id | replace('sensor.temperature_', '') | replace('_level', '') %}
{% set t = states('sensor.temperature_' ~ room) | float(none) %}

{% if t is none %}    unknown
{% elif t < 18 %}     cold
{% elif t < 24 %}     ok
{% elif t < 27 %}     warm
{% else %}            hot
{% endif %}

Combined with a YAML anchor, this logic is written once. The result:

sensor.temperature_bedroom_level
sensor.temperature_living_room_level
sensor.temperature_office_level
→ same logic, no duplication

Each additional room is just a trigger block referencing the anchor. The template itself never changes.


Why this matters

  • Fix the logic once — it’s fixed everywhere
  • Add a room without touching the template
  • Consistent behavior guaranteed across the entire sensor family

The same approach works for any repeated template logic: CO₂ status, humidity comfort, power classification, diagnostics.


Trade-off

The naming convention becomes part of the configuration contract. If it breaks, the template silently targets wrong sources.


Full explanation and working example:

Curious if others are using similar self-parameterized patterns, or if there are alternative approaches to avoid template duplication.

This has been around for quite a while.
I use a couple of anchors here and there.

Thanks for the link.

YAML anchors are definitely useful for reducing duplication in configuration blocks.

The approach here is a bit different though. Anchors still require hardcoding the source entities.

What I’m experimenting with is closer to a self-parameterized template sensor: the template derives its source entities from its own name via this.entity_id.

{% set suffix = this.entity_id | replace('sensor.temperature_', '') %}

So sensor.temperature_sejour would automatically know to aggregate sensor.temperature_sejour_1, sensor.temperature_sejour_2, etc. — without writing separate template logic for each room.

Anchors help reduce YAML duplication, while this pattern tries to keep the logic written once and rely on naming conventions for scaling.

Has anyone run into limitations when using this.entity_id this way inside template sensors?

FWIW, I believe the following example used the same principle. Each entity uses part of its own name (self-referenced) to compute a value.

Good catch — yes, it’s the same underlying idea.

One difference between this.name and this.entity_id is worth noting though: name can be a free-form label (with spaces, capitals, etc.), while entity_id follows a strict snake_case convention. That makes string manipulation and entity lookups more predictable.

That’s why I used entity_id here: it’s easier to derive related sensors deterministically from it.

But the core idea is identical — the entity introspects part of its own identity to configure itself.

Not necessarily. The choice of this.name or this.entity_id largely depends on the intended application (i.e. how the extracted string will be used).

  • In my 3 year-old example, I needed the extracted string (an area’s name) to use with the area_entities filter. The area’s name cannot be a slug (i.e. “snake case”).

  • In your example, you need to use the extracted strings to create a new entity_id. In this case, it is preferable if the strings are slugs.


Here are several ways to extract strings for your application. Most use this.entity_id because it’s best for your specific application. However, I included two that use this.name to demonstrate that it’s not all that more difficult, even for this situation.

Click to reveal code
{% set this = {
  'name': 'Temperature Level Living Room',
  'entity_id': 'sensor.temperature_level_living_room'
  } %}

{% set t = 'sensor.temperature_' ~ this.entity_id | replace('sensor.temperature_level_', '') %}
{{ t }}

{% set t = 'sensor.temperature_' ~ this.entity_id[25:] %}
{{ t }}

{% set t = 'sensor.temperature_' ~ this.name[18:] | slugify %}
{{ t }}

{% set t = 'sensor.' ~ this.name | slugify | replace('level_', '') %}
{{ t }}

{% set t = this.entity_id | replace('level_', '') %}
{{ t }}


EDIT

Just in case the order of the words temperature, level and the room’s name can vary, this example attempts to handle that situation.

Click to reveal code
{% set this = {
  'name': 'Living Room Temperature Level',
  'entity_id': 'sensor.living_room_temperature_level'
  } %}

{% set t = 'sensor.temperature_' ~ this.entity_id 
 | regex_replace('sensor|level|temperature|\.', '') | slugify %}
{{ t }}

Good point — you’re right that the choice depends on the intended application.

Here’s a concrete example from my setup where this.entity_id is the natural fit.

I use the same logic across multiple rooms to compute a color state from a room’s absolute humidity, a low threshold, and a high threshold. The sensor derives all related entity IDs from its own name, so the logic only needs to be written once.

- trigger:
    - platform: state
      entity_id:
        - sensor.humidite_absolue_chambre_arnaud
        - sensor.humidite_absolue_seuil_bas_chambre_arnaud
        - sensor.humidite_absolue_seuil_haut_chambre_arnaud
        - input_number.humidite_absolue_couleur_zone_verte
    - platform: homeassistant
      event: start

  sensor:
    - name: "Couleur Humidité absolue Chambre Arnaud"
      unique_id: couleur_humidite_absolue_chambre_arnaud
      state: &couleur_humidite_absolue_logic >
        {% set suffixe = this.entity_id | replace('sensor.couleur_humidite_absolue_', '') %}
        {% set src = 'sensor.humidite_absolue_' ~ suffixe %}
        {% set src_bas = 'sensor.humidite_absolue_seuil_bas_' ~ suffixe %}
        {% set src_haut = 'sensor.humidite_absolue_seuil_haut_' ~ suffixe %}

        {% set raw = states(src) %}
        {% set h = raw | replace(',', '.') | float(999) %}
        {% set bas = states(src_bas) | replace(',', '.') | float(0) %}
        {% set haut = states(src_haut) | replace(',', '.') | float(0) %}

        {% if raw in ['unknown', 'unavailable', None] or h == 999 %}
          grey
        {% elif bas == 0 or haut == 0 or bas >= haut %}
          grey
        {% elif h < bas %}
          blue
        {% elif h <= haut %}
          green
        {% else %}
          red
        {% endif %}

- trigger:
    - platform: state
      entity_id:
        - sensor.humidite_absolue_chambre_matthieu
        - sensor.humidite_absolue_seuil_bas_chambre_matthieu
        - sensor.humidite_absolue_seuil_haut_chambre_matthieu
        - input_number.humidite_absolue_couleur_zone_verte
    - platform: homeassistant
      event: start

  sensor:
    - name: "Couleur Humidité absolue Chambre Matthieu"
      unique_id: couleur_humidite_absolue_chambre_matthieu
      state: *couleur_humidite_absolue_logic

In this case, the related entities are already slug-based (humidite_absolue_*, seuil_bas_*, seuil_haut_*), so entity_id is the natural choice — no slugify step needed.

The anchors handle YAML deduplication, but the more interesting part is that the template logic derives its own related entities from naming conventions and can be reused across rooms without changing the template itself.

Correct; for this particular application. As was this.name the “natural choice” for the other example.


NOTE

The naming scheme in your entity’s object_id follows an ordered pattern which makes it amenable to use string slicing.

With string slicing, we can easily extract the desired suffix and prefix from the entity’s entity_id.

{% set prefixe = this.entity_id[:32] | replace('couleur_', '') %}
{% set suffixe = this.entity_id[32:] %}
{% set src = prefixe ~ suffixe %}
{% set src_bas = prefixe ~ 'seuil_bas_' ~ suffixe %}
{% set src_haut = prefixe ~ 'seuil_haut_' ~ suffixe %}

Since the original post I’ve pushed the pattern a bit further and built a full
self-parameterized sensor pipeline.

The idea is still the same:

this.entity_id → derive suffix → derive dependencies

But the pattern can also scale to multi-stage sensors.

Example:

temperature_min_daily_room_X
temperature_max_daily_room_X


temperature_minmax_daily_room_X


color_temperature_min_daily_room_X
color_temperature_max_daily_room_X


color_temperature_minmax_daily_room_X

Each stage is generic and derives its sources from its own this.entity_id.
Adding a new room requires no changes to the template logic.

I added a full working example here:

FYI this type of code will only work in yaml, you’ll be able to use it in the UI but you’ll get incorrect results. this in the ui is populate with a fake state because the entity doesn’t exist at that time. Once you create the entity though, it will work as expected. It’s only a fake state when you’re in the process of creating the entity.

On top of that, you can also use yaml anchors to get the job done with the introduction of variables at the template entity level. The benefit here is that you don’t need to care what your entity_id is for the string manipulation.

E.g.

- name: Temperature Level Bedroom
  variables:
    source: sensor.temperature_bedroom
  <<: &temperature_feel
    # Add any other common fields here
    state: >
      {% set t = states(source) | float(none) %}
      {% if t is none %} unknown
      {% elif t < 18 %} cold
      {% elif t < 24 %} ok
      {% elif t < 27 %} warm
      {% else %} hot {% endif %}

- name: Temperature Level Living Room
  variables:
    source: sensor.temperature_living_room
  <<: *temperature_feel
1 Like