Template sensor reacts quicker than automation

I’m monitoring the EV battery state of charge (SoC) during charging. It’s a BMW and the BMW API only updates the SoC with relatively long intervals, so I’m attempting to estimate the SoC between BMW API updates.

Approach:

  • Feed charging power to a utility_meter sensor (to get the charging energy) and add that charging energy to the latest API SoC.
  • When the API SoC changes, reset the utility meter to zero and continue

So the estimated SoC sensor is defined as this:

template:
  - sensor:
      - name: VENSTRE estimated battery charge
        unique_id: venstre_estimated_battery_charge
        device_class: battery
        state_class: measurement
        unit_of_measurement: "%"
        state: >
          {% set soc = state_attr('sensor.venstre_car','remaining_battery_charge') | float(0.0) %}
          {% set battery_capacity = state_attr('sensor.venstre_car','battery_capacity') | float(0.0) %}
          {% set delta_charge = states('sensor.venstre_battery_charging_estimator_energy') | float(0.0) %}
          {% set estimated_soc = (((soc * battery_capacity / 100.0) + delta_charge) / battery_capacity) * 100.0 %}
          {{ estimated_soc | round(2) }}

The utility meter reset is automated like this (triggering when the API SoC changes and when charging stops):

automation:
  - id: venstre_reset_battery_soc_estimator
    alias: VENSTRE reset battery SoC estimator
    trace:
      stored_traces: 150
    trigger: 
      - trigger: state
        entity_id: sensor.venstre_car
        attribute: remaining_battery_charge
        id: "soc_updated"
      - trigger: state
        entity_id: sensor.venstre_status
        from: "charging"
        id: "charging_stopped"
    action:
      - choose:
          - conditions: 
              - condition: template
                value_template: >
                  {{ (trigger.id == "soc_updated") or
                     ((trigger.id == "charging_stopped") and 
                      not(now().hour in state_attr('binary_sensor.venstre_charging_control','charging_hours'))) }}
            sequence:
              - action: utility_meter.calibrate
                data:
                  value: 0
                target:
                  entity_id: sensor.venstre_battery_charging_estimator_energy

This works well, but when the API SoC updates, the template sensor acts before the automation has time to reset the utility meter sensor, causing a short duration peak in the estimated SoC sensor:

Although not really a problem, it’s a nuisance that I don’t get a nicer result…

Any ideas, other than for example setting up a helper to delay the estimate a second, or even half a second?

If all you need is a delay to the calculation, you could turn your template sensor into a trigger-based sensor:

template:
  - triggers:
      - trigger: state
        entity_id: sensor.venstre_car
        for:
          seconds: 1
    sensor:
      - name: VENSTRE estimated battery charge
        unique_id: venstre_estimated_battery_charge
        device_class: battery
        state_class: measurement
        unit_of_measurement: "%"
        state: >
          {% set soc = state_attr('sensor.venstre_car','remaining_battery_charge') | float(0.0) %}
          {% set battery_capacity = state_attr('sensor.venstre_car','battery_capacity') | float(0.0) %}
          {% set delta_charge = states('sensor.venstre_battery_charging_estimator_energy') | float(0.0) %}
          {% set estimated_soc = (((soc * battery_capacity / 100.0) + delta_charge) / battery_capacity) * 100.0 %}
          {{ estimated_soc | round(2) }}
1 Like

Do you even need a utility meter and an automation here?

What if the template triggered on both changes to the SoC from the BMW api and also from changes in your power meter.

When the SoC changes you update the state and also store the current power meter reading in an attribute.

And then when the power meter changes you use the difference from current power to what is saved in the attribute to add to the state. Plus, handling the case of the attribute not being set.

Thanks, that’s a good idea. I had to add also a trigger for sensor.venstre_battery_charging_estimator_energy, but it worked well.

I’m intrigued, but to estimate the state of charge, I need to calculate the amount of energy put into the battery - not only the power - and the utility meter path is a straight-forward way to calculate that and at the same time providing an easy way to reset the «delta» energy each time the API SoC updates.

By storing the power and timestamps in attributes and triggering on power changes, I might be able to calculate the delta energy without the utility meter, but I suspect the template sensor will be more complex to configure.

So, I’ll stay with @Troon’s suggested solution for now, but keep your idea in mind. It’s an interesting challenge :wink:

Yeah, I meant energy. There’s always more than one way to do things. I’m aways cautious of using delays to fix a timing issue.

I assume what you have is:

  1. the most recent SoC percent from BMW’s API
  2. the total capacity of the car’s battrey
  3. an energy monitor that reports not only in kW but kWh.
Here's kind of what I was imagining

Just barely tested…

~/ha/config/packages$ cat soc.yaml
recorder:
  exclude:
    entities:
      - sensor.bmw_capacity
      - sensor.bmw_percent_full
      - sensor.local_meter_energy
      # Probably want to keep this one....
      - sensor.my_bmw_soc


template:

  # Mock sensors
  - sensor:
      - name: BMW Battery Size
        default_entity_id: sensor.bmw_capacity
        device_class: energy
        state: 200
        unit_of_measurement: kWh

  - sensor:
      - name: BMW Reported Percent Full
        default_entity_id: sensor.bmw_percent_full
        device_class: battery
        unit_of_measurement: '%'
        state: "{{ 0 if this.state|float(0) + 15 > 100 else this.state|float(0) + 15 }}"
    triggers:
      - trigger: time_pattern
        seconds: '/20'

  - sensor:
      - name: BMW Energy Meter
        default_entity_id: sensor.local_meter_energy
        device_class: energy
        state: "{{ this.state|float(0) + 1 }}"
        unit_of_measurement: kWh
    triggers:
      - trigger: time_pattern
        seconds: '/1'



  # Estimate percent between BMW reports

  - triggers:
      - trigger: state
        entity_id: sensor.bmw_percent_full
        id: soc_updated
      - trigger: state
        entity_id: sensor.local_meter_energy
    variables:
      capacity:     "{{ states('sensor.bmw_capacity') | float(70) }}"
      bmw_percent:  "{{ states('sensor.bmw_percent_full') | float(0) }}"
      last_kwh:     "{{ state_attr('sensor.my_bmw_soc', 'last_kwh') | float(0) }}"
      cur_kwh:      "{{ states('sensor.local_meter_energy')|float(0) }}"
      delta_percent: "{{ ((cur_kwh - last_kwh) / capacity * 100) | round(3) }}"
    sensor:
      - name: Calculated SOC
        default_entity_id: sensor.my_bmw_soc
        device_class: battery
        unit_of_measurement: '%'
        state: "{{  bmw_percent if trigger.id == 'soc_updated' else bmw_percent + delta_percent }}"
        attributes:
          last_kwh: "{{ cur_kwh if trigger.id == 'soc_updated' else last_kwh }}"
1 Like

Great, thanks. I have tested this as follows:

template:
  - trigger:
      - trigger: state
        entity_id: sensor.venstre_car
        attribute: remaining_battery_charge
        id: 'soc_updated'
      - trigger: state
        entity_id: sensor.venstre_charging_battery_energy
        id: 'energy_updated'
    variables:
      battery_capacity: "{{ state_attr('sensor.venstre_car','battery_capacity') | float(70.0) }}"
      soc_percent:      "{{ state_attr('sensor.venstre_car','remaining_battery_charge') | float(0.0) }}"
      last_kwh:         "{{ state_attr('sensor.venstre_estimated_battery_charge_2', 'last_kwh') | float(0.0) }}"
      current_kwh:      "{{ states('sensor.venstre_charging_battery_energy') | float(0.0) }}"
    sensor:
      - name: VENSTRE estimated battery charge 2
        unique_id: venstre_estimated_battery_charge_2
        device_class: battery
        state_class: measurement
        unit_of_measurement: "%"
        state: >
          {% set delta_percent = ((current_kwh - last_kwh) / battery_capacity * 100.0) %}
          {{ (soc_percent | round(2)) if trigger.id == 'soc_updated' else ((soc_percent + delta_percent) | round(2)) }}
        attributes:
          last_kwh: >
            {% set delta_percent = ((current_kwh - last_kwh) / battery_capacity * 100.0) %}
            {{ current_kwh if trigger.id == 'soc_updated' else last_kwh }}

It gives the same results as the utility meter, delayed template sensor and automation approach as suggested by @Troon. The yellow line in the graph below is the @Troon style sensor. It hides the blue line which is the @busman style template sensor. The red line is the SoC reported by BMW.

(It would probably have been nicer to calculate delta_percent as a variable as suggested by @busman, but I was confused by a DisallowedExtraPropWarning in the Studio Code Server editor, so it ended up in the state and attribute templates.)

EDIT: On the first attempt, I mixed up the yellow and blue lines in the graph.