The order of template sensor self-references

This section of the documentation is relevant here: Template - Home Assistant (self-referencing).

Note specifically how this can have a latest value from attributes that can be referenced in the state, but not the other way around. I find this odd.

Here’s why: I just defined this helper sensor for my irrigation and I want to separate state values from what’s displayed on the UI. I need to define this in the sensor, as there’s no way with built-in cards to map from states to UI strings.

template:
  - trigger:
      - platform: state
        entity_id: switch.irrigation_main
    sensor:
      - name: "Irrigation State"
        unique_id: "795ac0b2-1b4e-4bfa-91c5-b055072b1135"
        icon: mdi:state-machine
        state: >-
          {% if this.state == 'Not Running' %}
            not_running
          {% elif this.state == 'Automatic Cycle' %}
            automatic_cycle
          {% elif this.state == 'Manual Cycle' %}
            manual_cycle
          {% else %}
            manual_zone
          {% endif %}
        attributes:
          ui_state: >-
            {% set durations = [
              'number.irrigation_front_beds_run_duration',
              'number.irrigation_front_lawn_run_duration',
              'number.irrigation_rear_beds_run_duration',
              'number.irrigation_rear_lawn_run_duration'] %}
            {% set multiplier = states('number.irrigation_multiplier') | float(0) %}
            {% set cycle_duration = durations | map('states') | map('int') | sum  * multiplier %}
            {% set automatic_cycle_start = states('input_datetime.irrigation_start_time') | today_at %}
            {% set automatic_cycle_end = (automatic_cycle_start | as_timestamp + cycle_duration * 60) | as_datetime | as_local %}
            {% set is_within_watering_window = automatic_cycle_start <= now() <= automatic_cycle_end %}
            {% set is_watering_day = is_state('input_boolean.irrigation_' ~ now().strftime('%A') | lower, 'on') %}
            {% if is_state('switch.irrigation_main', 'off') %}
              Not Running
            {% elif is_watering_day and is_within_watering_window %}
              Automatic Cycle
            {% elif is_state('switch.irrigation_auto_advance', 'on') %}
              Manual Cycle
            {% else %}
              Manual Zone
            {% endif %}

Before I read the documentation with more attention, I naturally thought it would work the other way around: Compute your new state, and have that available for use in icon and attribute fields.

I was wondering whether there’s a particular reason it would’ve been implemented this way. Attributes are secondary to the state, but this gives it a weird kind of primary status.

I don’t see how your state template will ever return anything but manual_zone
this.state will return the existing state of sensor.irrigation_state at the time of the trigger. However, none of the values you are testing against match values your template will return.

        state: >-
          {% if this.state == 'Not Running' %}
            not_running
          {% elif this.state == 'Automatic Cycle' %}
            automatic_cycle
          {% elif this.state == 'Manual Cycle' %}
            manual_cycle
          {% else %}
            manual_zone
          {% endif %}

“Not Running” != “not_running”

1 Like

Actually not (and my sensor is definitely working as expected). :slight_smile:

(The above is false: I must’ve forgotten to reload/restart.)

Check this example from the docs (mine follows the same pattern):

template:
  - sensor:
      - name: test
        state: "{{ this.attributes.test | default('Value when missing') }}"
        # not: "{{ state_attr('sensor.test', 'test') }}"
        attributes:
          test: "{{ now() }}"

The state uses the now() from the attribute from the same event it’s updated from. In other words, in this case, sensor.test.state == sensor.test.attributes.test.

EDIT: Extra note: My state template merely “translates” the “internal state value” to one I can use on the UI. (Correction: It’s the reverse.)

The example from the docs has two major difference from the original post; the state template references an attribute of this not it’s state, and it is not trigger-based.

The order of operations for a trigger-based template sensor is trigger > instantiate this and trigger variables > render state > render attributes…

If sensor.test from the docs were trigger-based (and the templates were fixed to handle the initial case where this is undefined) the state value would always be the value of the test attribute saved at the last firing… there is no cyclic re-rendering of state based on the value of test because test is not a trigger.

1 Like

Your template could potentially return the following states:

  • not_running
  • automatic_cycle
  • manual_cycle
  • manual_zone

So something like this.state == 'Not Running' will never be True as that is no possible output for this.state

I think you meant to use this.attributes.ui_state == 'Not Running'

But what you now have as a state template will indeed always retrun manual_zone

2 Likes

I thought that too, but the trigger is a switch…

Yeah, I changed it when I realized that. I only see this.attributes.ui_state as on option now :slight_smile:

I made a small test:

template:
  - trigger:
      - platform: state
        entity_id: input_boolean.this_is_a_test
        from: null
    sensor:
      - name: This Is A Test
        unique_id: 961b6c02-7b7e-416e-913e-8ee10fb73901
        state: "{{ trigger.to_state.state }}"
        attributes:
          test: "The input boolean is {{ this.state }}"
      - name: This Is A Test 2
        unique_id: 67fa65f3-0cfb-4795-8a3e-45f95f42fef1
        state: "The input boolean is {{ this.attributes.test | default() }}"
        attributes:
          test: "{{ trigger.to_state.state }}"
  - sensor:
      - name: This Is A Test 3
        unique_id: a82e6278-7db6-4961-908c-0eb4514eccf3
        state: "{{ states('input_boolean.this_is_a_test') }}"
        attributes:
          test: "The input boolean is {{ this.state }}"
      - name: This Is A Test 4
        unique_id: 841692d6-d3ef-47e2-bd07-81b4f9c55e09
        state: "The input boolean is {{ this.attributes.test | default() }}"
        attributes:
          test: "{{ states('input_boolean.this_is_a_test')}}"

results in:

So, from this I conclude that the this object doesn’t change for trigger based template sensors after the trigger.

For non triggered sensors it does change during rendering of the template, or maybe it listens to state changes of itself. Not completely sure about that, I would need to dig in the code for that, and I’m not sure if I already have enough Python knowledge for that.

1 Like

I see now what you originally meant, @Didgeridrew. It’s just strange, because the sensor does change state as expected when turning on and off that switch.

I can only think it must be due to this hypothesis then:

@TheFes your testing is very helpful, thanks.

I will make some changes tomorrow and test this again.

Just to highlight my original point though: The example in the docs seem to say you can change an attribute and then reference that current (new) value through this in the state. The one strange thing I’ve mentioned is that if something like that were to work, I would’ve though the state would be set first and then one can use that value in attributes. Secondly, I always thought (since its introduction) that this is always the previous (last) state.

Yes, you can do that, but not when you use a trigger. If you use a trigger, the this object is generated at the moment of the trigger, and it doesn’t change (exactly the same as it does for an automation).

1 Like

That only works with the example because the attribute template will spark a state change for the main state template. As soon as you add a trigger to the mix, that behavior changes because the trigger determines the updates.

Without a trigger, templates update based on the entities that the template is listening to.

With a trigger, the trigger causes the templates to execute, nothing else.

1 Like

Thank you both.

I find this all very opaque. Your explanations are great. I’m referring to how one would get to learn this purely from the docs.

Is this documented somewhere that I have missed, or is it just the fact that one has to understand the implied subtleties in detail? Like the given example: unless there’s a second example, it’s very hard to spot what parts make the difference. At least for me that is.

Is it stands, I feel like rather duplicating my logic, because from a maintenance point of view I won’t need to remember any of the discussion a yest from now. All I wanted to do was to avoid the duplication. Perhaps I shall just use a Jinja macro instead.

I’d be happy to submit changes for the docs (as I have before), but I can only do it when I really understand what’s going on.

Honestly, I’m not ranting here. :slight_smile:

EDIT: I read about 6 forum posts about this and 2 GitHub issues of which one was an actual bug (when this was added), but I really didn’t pick up on any of this. Not saying the info wasn’t there though.

@Didgeridrew @TheFes @petro thank you for all your help and clarifications, and I need to apologise, because I certainly stuffed something up during my testing. I think I deployed the code I posted initially, but never reloaded/restarted (I can write a long story as an explanation, but basically it involves a baby and the lack of sleep). :smiley:

Without making any changes, I simply reran it and got the incorrect results:

I opted for a version using trigger variables instead, which fulfills all of my goals.

template:
  - trigger:
      - platform: state
        entity_id: switch.irrigation_main
        variables:
          state: >-
            {% set durations = [
              'number.irrigation_front_beds_run_duration',
              'number.irrigation_front_lawn_run_duration',
              'number.irrigation_rear_beds_run_duration',
              'number.irrigation_rear_lawn_run_duration'] %}
            {% set multiplier = states('number.irrigation_multiplier') | float(0) %}
            {% set cycle_duration = durations | map('states') | map('int') | sum  * multiplier %}
            {% set automatic_cycle_start = states('input_datetime.irrigation_start_time') | today_at %}
            {% set automatic_cycle_end = (automatic_cycle_start | as_timestamp + cycle_duration * 60) | as_datetime | as_local %}
            {% set is_within_watering_window = automatic_cycle_start <= now() <= automatic_cycle_end %}
            {% set is_watering_day = is_state('input_boolean.irrigation_' ~ now().strftime('%A') | lower, 'on') %}
            {% if is_state('switch.irrigation_main', 'off') %}
              not_running
            {% elif is_watering_day and is_within_watering_window %}
              automatic_cycle
            {% elif is_state('switch.irrigation_auto_advance', 'on') %}
              manual_cycle
            {% else %}
              manual_zone
            {% endif %}
    sensor:
      - name: "Irrigation State"
        unique_id: "795ac0b2-1b4e-4bfa-91c5-b055072b1135"
        icon: mdi:state-machine
        state: >-
          {{ state }}
        attributes:
          ui_state: >-
            {% set mapping = {
              'not_running': 'Not Running',
              'automatic_cycle': 'Automatic Cycle',
              'manual_cycle': 'Manual Cycle',
              'manual_zone': 'Manual Zone'
              } %}
            {{ mapping[state] }}

If there are any othe suggestions to optimise or simplify this, I’m all ears.