Using a function call to set value of 'state'

In order to track my power consumption and be able to properly display it in the Energy Configuration I have a sensor, provided by IotaWatt that gives me the power reading in Wh for each phase in my house i.e. Red, Yellow, Blue.

This reading is a total value and can hence be negative and positive. To correctly be displayed in the Energy Tab the import and export values must be positive, so I have templated each phase into two sensors based on the state change of the Main <phase>.wh sensor.

As per below:

- trigger:
    - trigger: state
      entity_id:
        - sensor.main_r_wh
      not_from:
        - unknown
        - unavailable
    - trigger: time
      at: "00:00:00"
  sensor:
    - name: "Main R Export.wh"
      unique_id: sensor.main_r_export_wh
      state_class: total_increasing
      device_class: energy
      icon: "mdi:lightning-bolt"
      unit_of_measurement: "Wh"
      state: >
        {% set last_state = this.state | float(0) %}
          {% if trigger.platform == 'time' %}
            {% set last_state = 0 %}
          {% elif not is_number(last_state) %}
            {% set trigger_seconds = int(as_timestamp(trigger.to_state.last_updated + timedelta(8/24))) % 86400 %}
            {% set last_state = states.sensor.sql_main_r_export_wh.state | float(0) %}
          {% endif %}
        {% set to_state = trigger.to_state.state | float(0) %}
        {% set from_state = trigger.from_state.state | float(0) %}
        {% if trigger_seconds > 20 and is_number(to_state) %}          
          {% set delta = to_state - from_state %}
        {% endif %}
        {{ last_state + delta | abs if is_number(delta) and delta < 0 else last_state | float(0) }}
    - name: "Main R Import.wh"
      unique_id: sensor.main_r_import_wh
      state_class: total_increasing
      device_class: energy
      icon: "mdi:lightning-bolt"
      unit_of_measurement: "Wh"
      state: >
        {% set last_state = this.state | float(0) %}
          {% if trigger.platform == 'time' %}
            {% set last_state = 0 %}
          {% elif not is_number(last_state) %}
            {% set trigger_seconds = int(as_timestamp(trigger.to_state.last_updated + timedelta(8/24))) % 86400 %}
            {% set last_state = states.sensor.sql_main_r_export_wh.state | float(0) %}
          {% endif %}
        {% set to_state = trigger.to_state.state | float(0) %}
        {% set from_state = trigger.from_state.state | float(0) %}
        {% if trigger_seconds > 20 and is_number(to_state) %}          
          {% set delta = to_state - from_state %}
        {% endif %}
        {{ last_state + delta | abs if is_number(delta) and delta >= 0 else last_state | float(0) }}

This works more or less ok. some filtering was needed around the midnight point to avoid the Main x export.wh value to give an excessive reading when the trigger state is reset to 0 at midnight, but it works.

What is ‘strange’ is that I have to copy the same state logic 3 times, once for each phase. Is there an option to do this with a function call?

i.e. something like the below in -more or less- JS, but then in Home Assistant

function getState(state: state, trigger: trigger, sqlState: state = null): number {
     let delta: number = 0;
     let last_state = state ?? 0
     if (trigger.platform == 'time') {
        last_state = 0
     }
     else if not (is_number(last_state)) {
         trigger_seconds = int(as_timestamp(trigger.to_state.last_updated + timedelta(8/24))) % 86400
         last_state = sqlState.state ?? 0
     }
     to_state = trigger.to_state.state
     from_state = trigger.from_state.state
     if (trigger_seconds > 20 && is_number(to_state))  {
       delta = to_state - from_state
     }
     return (is_number(delta) && delta < 0) ? last_state + Math.abs(delta) : last_state;

That way you only have the state logic once (or twice in this case, once for < 0 and once for >= 0)

Then the logic for the template would be much cleaner;

- trigger:
    - trigger: state
      entity_id:
        - sensor.main_r_wh
      not_from:
        - unknown
        - unavailable
    - trigger: time
      at: "00:00:00"
  sensor:
    - name: "Main R Export.wh"
      unique_id: sensor.main_r_export_wh
      state_class: total_increasing
      device_class: energy
      icon: "mdi:lightning-bolt"
      unit_of_measurement: "Wh"
      state: >
          {{ getState(this.state, trigger, states.sensor.sql_main_r_export_wh)

Yes, with reusable templates, otherwise known as macros:

Note that macros always return strings, so may need casting to numbers.

For those wondering how this is now resolved.
-with the kind suggestion by @Troon -

The below is the solution to the question.
The below statement:

state: >
  {% set last_state = this.state | float(0) %}
  {% if trigger.platform == 'time' %}
    {% set last_state = 0 %}
  {% elif not is_number(last_state) %}
    {% set trigger_seconds = int(as_timestamp(trigger.to_state.last_updated + timedelta(8/24))) % 86400 %}
    {% set last_state = states.sensor.sql_main_r_export_wh.state | float(0) %}
  {% endif %}
  {% set to_state = trigger.to_state.state | float(0) %}
  {% set from_state = trigger.from_state.state | float(0) %}
  {% if trigger_seconds > 20 and is_number(to_state) %}          
    {% set delta = to_state - from_state %}
  {% endif %}
  {{ last_state + delta | abs if is_number(delta) and delta >= 0 else last_state | float(0) }}

is now, for import energy (i.e. all positively increasing values):

state: >
  {% from 'energy_states.jinja' import energy_state_all %}
  {{ energy_state_all(this.entity_id, trigger, 1) }}

and for export energy (i.e. all negatively increasing values):

state: >
  {% from 'energy_states.jinja' import energy_state_all %}
  {{ energy_state_all(this.entity_id, trigger, -1) }}

The macro(s) in the energy_states.jinja file are as follows:

{% macro sign(state) %}
    {% if state < 0 %}
        -1
    {% elif state > 0 %}
        1
    {% else %}
        0
    {% endif %}
{% endmacro %}

{% macro energy_state_all(entity_id, trigger, dir) %}    
    {% set trigger_seconds = int(0) %}
    {% set delta = float(0) %}

    {# Depending on trigger.platform, either reset or calculate the delta value #}
    {% if trigger.platform == 'time' or trigger.platform == 'time_pattern' %}
        {% set last_state = 0|float(0) %}
    {% elif trigger.platform == 'state' %}
        {% set trigger_seconds = int(as_timestamp(states[trigger.entity_id].last_updated + timedelta(8/24))) % 86400 %}
        
        {# Verify if last_state is valid, else get from DB #}
        {% set sql_id = "sensor.sql_{}".format(entity_id.split(".")[1]) %}
        {% if has_value(entity_id)| bool %}
            {% set last_state = states[entity_id].state | float(0) %}
        {% elif has_value(sql_id)| bool %}
            {% set last_state = states[sql_id].state | float(0) %}
        {% endif %}
        
        {# Verify that trigger value is valid #}
        {% if has_value(trigger.entity_id) %}
            {% set to_state = trigger.to_state.state | float(0) %}
            {% set from_state = trigger.from_state.state | float(0) %}
            {% set delta = to_state - from_state if trigger_seconds > 20 else float(0) %}
        {% endif %}
    {% endif %}
    
    {# Return delta or last_state depending on direction value #}
    {{ last_state + delta | float | abs if (dir==sign(delta) | int) else last_state | float(0) }}
{% endmacro %}

proces:

  • On state change of sensor.main_r_wh or on timer-trigger
  • if platform is time then reset the last_state value (i.e. reset to 0 at midnight)
  • else if trigger.platform=='state' then:
  1. Set trigger_seconds as the number of seconds in that day of the last value update of the trigger sensor (i.e the main_r_wh sensor ins this case).
    This is used to ignore the reset step for main_r_wh at midnight as the delta between to_state and from_state at that point can be a high negative value so any change between 00:00:00 and 00:00:20 is ignored.
  2. Set the slq_id as the sensor id for retrieving the Database value
  3. Verify whether the entity (i.e. this.entity) has a value
    • set last_state if it does.
  4. if it doesn’t, then verify whether the SQL state can be retrieved (i.e. sql_id)
    (this uses SQL integration, this is an alternative approach to HA ‘value-retention’ which doesn’t require an ‘input_number’ sensor.)
  5. Verify whether the trigger value is valid
    (this is in essence a validity check on trigger.to_state.state)
    • calculate delta if boundary conditions are valid

Finally, determine whether the positive or negative value needs to be returned, using the sign macro which returns -1|0|1 depending on the value of delta

1 Like