How can I creating multiple attributes from one value template?

Good day all.

I’ve got a little question, I’m trying to build a sensor, the main state is just a simple on/off switch basically but with different words, but there’s like 50 lines of code inside the Value-template. I would like some of these data to be visible as a attribute of the entity. But I’m unable to vind how to reference from a attribute_template back to the original value-template. Ofcourse I’m able to copy/paste the complete code inside the attribute_template, but this kind of defeats the purpose for me as this would mean if I change one thing I need to copy/past it to all the attribute_templates also.

- platform: template
  sensors:
    start_charge_time:
      friendly_name: Start charge time
      value_template: >
        {% set hours_left = states('sensor.battery_hours_left') | int %}
        {% set current_hour = now().hour %}
        {% set latest_hour = current_hour + hours_left %}

        {% set today_price = states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.today %}
        {% set tomorrow_price = states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.tomorrow %}
        {% set battery_size = states('input_number.battery_capacity') | int  %}
        {% set target_charge_level = states('input_number.charge_target') | int %}
        {% set battery_charge_speed = states('input_number.charge_capacity') | int %}
        {% set charge_kwh = (battery_size / 100 * (target_charge_level - states('sensor.battery_level') | float))  %}
        {% set charge_time_minutes = ((charge_kwh / battery_charge_speed) * 60) | round(0) %}
        {% set charge_time_hours = (charge_time_minutes / 60) |  round(0, 'ceil') %}

        {% set future_price = today_price[now().hour:] + tomorrow_price %} 
        {% set inteval_start_tomorrow = 0 %}
        {% set interval_start_hour =  now().hour + 1 %}
        {% set interval_end_tomorrow = 1 %}
        {% set interval_end_hour = 23 %}

        {% set low_interval_length = charge_time_hours %}
        {% set all_price = today_price + tomorrow_price %}

        {% set interval_price = all_price[interval_start_hour + 24 * inteval_start_tomorrow : interval_end_hour + 24 * interval_end_tomorrow] %}
        {% set last_considered_interval_index = hours_left%}
        {% set ns = namespace(current_best_average=1000, current_best_index=-1) %}  

        {% for price in interval_price[:last_considered_interval_index] %}
          {% set current_average =  interval_price[loop.index0:loop.index0+low_interval_length] | average %}
          {% if current_average < ns.current_best_average %}
            {% set ns.current_best_average = current_average %}
            {% set ns.current_best_index = loop.index0 %}
          {% endif %}
        {% endfor %}

        {% set start_index = ns.current_best_index %}
        {% set best_interval_price = interval_price[start_index:start_index + low_interval_length] %}
        {% set best_interval_start_hour =  24 * inteval_start_tomorrow + interval_start_hour + start_index %}
        {% set best_interval_price = interval_price[start_index:start_index + low_interval_length] %}
        {% set start_charge_datetime =  (now() + timedelta(hours=best_interval_start_hour - now().hour)) - timedelta(minutes = now().minute) %}
        {% set stop_charge_datetime =  start_charge_datetime + timedelta(minutes=charge_time_minutes+10) %}
        {% set start_charge_time = start_charge_datetime.strftime('%Y-%m-%dT%H:%M:%S') %}
        {% set stop_charge_time = stop_charge_datetime.strftime('%Y-%m-%dT%H:%M:%S') %}
        
        {% if start_charge_datetime - timedelta(minutes=10) <= now() %}
        Start
        {% else %}
        Wait
        {% endif %}
        
      attribute_templates:
        time: >
          {{ start_charge_datetime }}

If you convert your sensor to the current state-based configuration (instead of the legacy method your sensor is currently using) you can use the this variable to access attributes… i.e. {{ this.attributes.charge_time_hours}}

Aaah, sounds good, i’ve got no clue what this means at the moment, or how to accomplish it, but I’ll dive into it!

But for now, this means you create one code inside a sensor and are able to derive the main state and multiple attributes from that single code part right?

Thnx for the input!

The variables you define within one option’s template cannot be referenced in another option’s template. The variable’s scope is limited to the option where it’s defined.

In other words, the variable start_charge_datetime you defined in the value_template option cannot be referenced outside of that option (like you attempted to do in the template for time).

It’s not quite that simple… From what I have tried, it seems that it is currently best practice to use the full template when defining the state. If you try to define attributes then use the this variable in the state template you end up with errors and non-functional sensors. But, you can define attributes and use the this variable to refer to those values when defining other attributes.

Self-referencing

Basically, not useful for your existing Template Sensor unless you substantially re-design how it employs templated attributes.

Thanks for the input! I’ll try to convert this script to the new format and see what I’m able to come up with. I’ll report back soon.

Move everything in the value_template, except the last five lines, into the time attribute to compute the charging start time. Then modify the first line in value_template (i.e. state in modern format) to reference the time attribute.

That’ll conform to the documentation’s example which shows a sensor’s state template referencing its own attributes via this.

This indeed would make the start time visible, but I would like to make the steps from within the code visibleAs attributes and show a simple ‘charge’ or ‘wait’ as the main sensor. So I converted the code to the new format, and managed to make it all work and make the start time also visible on the main sensor, referenced from the state to the attribute.

But I’m running into 2 issues with this code, when I try to implement the if/else statement to convert the time to an easy to read format it won’t go, and makes the whole sensor unavailable.
and the last attribute: “stop_charge” won’t calculate. it just show’s a “-”

Could you take a look? :grimacing: I feel like it is a really obvious thing, but I can’t seem to see it…

  - sensor:
      - name: "test template sensor"
        state: >
          {% if this.attributes.start_charge - timedelta(minutes=10) <= now() %}
            Start
          {% else %}
            Wait
          {% endif %}


        attributes:
          hours_left: "{{ states('sensor.battery_time_left') | int }}"
          current_hour: "{{ now().hour }}"
          latest_hour: "{{ now().hour + this.attributes.hours_left | int }}"
          today_price: "{{ states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.today }}"
          tomorrow_price: "{{ states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.tomorrow }}"
          battery_size: "{{ states('input_number.battery_capacity') | int }}"
          target_charge_level: "{{ 100 }}"
          battery_charge_speed: "{{ states('input_number.charge_capacity') | int | round(1) }}"
          charge_kwh: "{{ (this.attributes.battery_size / 100 * (this.attributes.target_charge_level - states('sensor.battery_level') | float)) }}"
          charge_time_minutes: "{{((this.attributes.charge_kwh / this.attributes.battery_charge_speed) * 60) | round(0) }}"
          charge_time_hours: "{{ (this.attributes.charge_time_minutes / 60) |  round(0, 'ceil') }}"
          future_price: "{{ this.attributes.today_price[now().hour:] + this.attributes.tomorrow_price }}"
          interval_start_tomorrow: "{{ 0 }}"
          interval_start_hour: "{{ now().hour + 1 }}"
          interval_end_tomorrow: "{{ 1 }}"
          interval_end_hour: "{{ 23 }}"
          low_interval_length: "{{ this.attributes.charge_time_hours }}"
          all_price: "{{ this.attributes.today_price + this.attributes.tomorrow_price }}"
          interval_price: "{{ this.attributes.all_price[this.attributes.interval_start_hour + 24 * this.attributes.interval_start_tomorrow : this.attributes.interval_end_hour + 24 * this.attributes.interval_end_tomorrow] }}"
          last_considered_interval_index: "{{ this.attributes.hours_left }}"
          start_index : >
            {% set ns = namespace(current_best_average=1000, current_best_index=-1) %}  
            {% for price in this.attributes.interval_price[:this.attributes.last_considered_interval_index] %}
              {% set current_average = this.attributes.interval_price[loop.index0:loop.index0+this.attributes.low_interval_length] | average %}
              {% if current_average < ns.current_best_average %}
                {% set ns.current_best_average = current_average %}
                {% set ns.current_best_index = loop.index0 %}
              {% endif %}
            {% endfor %}
            {{ ns.current_best_index }}
        
          best_interval_price: "{{ this.attributes.interval_price[this.attributes.start_index:this.attributes.start_index + this.attributes.low_interval_length] }}"
          best_interval_start_hour: "{{ 24 * this.attributes.interval_start_tomorrow + this.attributes.interval_start_hour + this.attributes.start_index }}"
          start_charge: "{{ (now() + timedelta(hours=this.attributes.best_interval_start_hour - now().hour)) - timedelta(minutes = now().minute) }}"
          stop_charge: "{{ this.attributes.start_charge + timedelta(minutes=this.attributes.charge_time_minutes+10) }}"

ow, wait, my problem is exactly what you were describing right? is that the reason why my if/else statement fails?

The funny thing is I’m able to return the state of an attribute back to the main state and display it, but I’m not able to use it with a if/else statement. seems kind of strange to me.

If the value of start_charge is a string containing nothing more than a single hyphen - then the calculation performed in the state option’s if-else will fail. You can’t subtract a timedelta object from a string.

Mmmm, sound logical, but the start_charge attribute returns a datetime component, not only a single hyphen (I think) And these exact calculations work with the old format. Both for the stop_charge calculation and the if/else statement.

Aah, so I think I solved it. I removed the attribute reference and copied the calculations into a separate attribute, so all the calculations are done inside the attributes, and the “state” only reference directly to a attributes.

  - sensor:
      - name: "test template sensor"
        state: >
          {{ this.attributes.start_trigger | default('Value when missing') }}


        attributes:
          hours_left: "{{ states('sensor.battery_time_left') | int }}"
          current_hour: "{{ now().hour }}"
          latest_hour: "{{ now().hour + this.attributes.hours_left | int }}"
          today_price: "{{ states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.today }}"
          tomorrow_price: "{{ states.sensor.nordpool_kwh_nl_eur_3_095_021.attributes.tomorrow }}"
          battery_size: "{{ states('input_number.battery_capacity') | int }}"
          target_charge_level: "{{ 100 }}"
          battery_charge_speed: "{{ states('input_number.charge_capacity') | int | round(1) }}"
          charge_kwh: "{{ (this.attributes.battery_size / 100 * (this.attributes.target_charge_level - states('sensor.battery_level') | float)) }}"
          charge_time_minutes: "{{ ((this.attributes.charge_kwh / this.attributes.battery_charge_speed) * 60) | round(0) }}"
          charge_time_hours: "{{ (this.attributes.charge_time_minutes / 60) |  round(0, 'ceil') }}"
          future_price: "{{ this.attributes.today_price[now().hour:] + this.attributes.tomorrow_price }}"
          interval_start_tomorrow: "{{ 0 }}"
          interval_start_hour: "{{ now().hour + 1 }}"
          interval_end_tomorrow: "{{ 1 }}"
          interval_end_hour: "{{ 23 }}"
          low_interval_length: "{{ this.attributes.charge_time_hours }}"
          all_price: "{{ this.attributes.today_price + this.attributes.tomorrow_price }}"
          interval_price: "{{ this.attributes.all_price[this.attributes.interval_start_hour + 24 * this.attributes.interval_start_tomorrow : this.attributes.interval_end_hour + 24 * this.attributes.interval_end_tomorrow] }}"
          last_considered_interval_index: "{{ this.attributes.hours_left }}"
          start_index : >
            {% set ns = namespace(current_best_average=1000, current_best_index=-1) %}  
            {% for price in this.attributes.interval_price[:this.attributes.last_considered_interval_index] %}
              {% set current_average = this.attributes.interval_price[loop.index0:loop.index0+this.attributes.low_interval_length] | average %}
              {% if current_average < ns.current_best_average %}
                {% set ns.current_best_average = current_average %}
                {% set ns.current_best_index = loop.index0 %}
              {% endif %}
            {% endfor %}
            {{ ns.current_best_index }}
        
          best_interval_price: "{{ this.attributes.interval_price[this.attributes.start_index:this.attributes.start_index + this.attributes.low_interval_length] }}"
          best_interval_start_hour: "{{ 24 * this.attributes.interval_start_tomorrow + this.attributes.interval_start_hour + this.attributes.start_index }}"
          start_charge: "{{ (now() + timedelta(hours=this.attributes.best_interval_start_hour - now().hour)) - timedelta(minutes = now().minute) }}"
          stop_charge: "{{ (now() + timedelta(hours=this.attributes.best_interval_start_hour - now().hour + this.attributes.charge_time_hours)) - timedelta(minutes = now().minute)}}"
          start_trigger: >
            {% set start = (now() + timedelta(hours=this.attributes.best_interval_start_hour - now().hour)) - timedelta(minutes = now().minute) %}
            {% set start_trigger = start - timedelta(minutes=15) %}
            {% if start_trigger < now() %}
              Start
            {% else %}
              Wait
            {% endif %}

If you recall, that’s what I suggested you do in my previous post (2 days ago). It conforms to the example posted in the documentation.

You are absolutely correct! it needed to click in my head, now it does!

Thank you for all the help!

1 Like