Watt Hour Sensor

Post Reply
Dave Foster
Posts: 821
Joined: Thu Oct 13, 2022 7:21 pm

I wrote this sensor after ongoing discussions with Ryan Morgan about some of the issues he has experienced with his battery pack - one of the key metrics he needed to know was how it is performing and this came from a rough in the head calculation of watt hour / % SoC.

In an ideal world this would be whatever your expected battery pack capacity is in watts and divide by 100 - so assuming 6 HV2600 batteries (6*2,560wh) = 15,360 / 100 = 153.6 watts per % change in SoC.

As a precursor, it is worth pointing out that the battery SoC on LFP batteries is notoriously unreliable but when taken over a larger range (say 20%) accuracy improves and it will give you a reasonable representation of how the pack is performing over time.

Known issues - more a known problem of LFP batteries reporting SoC, but over a longer period of time (>8 hours) where there is patchy solar and repeated charge / discharge in the battery and the SoC neither goes up or down much ,the accuracy falls as efficiency and granularity errors creep in.

Clearly the best accuracy comes from the simple use case where you charge the batteries up and then discharge the batteries.

To minimise this effect the sensor resets itself at 100% and 10% SoC (the BMS itself resets some of its counters) and also where the difference in SoC since last measurement is zero and if the nett change in SoC is > 19% (so in effect the measurement is taken up to and over a 20% range). Any values greater than 25% above/below expected are rejected.

It requires 3 helpers, 4 sensors and an automation that checks for changes and calculates the watt hour display.

HELPERS
Last SoC - used to record last soc state and only update when state changes, set as follows -
helplastsoc.jpg
Starting SoC - used to record the starting position of SoC and is used as the divisor by the calculation, set as follows -
helpstartingsoc.jpg
Watt hour calc - the sensor that holds the currently calculated watt hour usage, set as follows -
helpwatthourcalc.jpg
Now onto the sensors
batt_watt_change - a rolling sensor that counts every watt of either charge (+) or discharge (-)

Code: Select all

  - sensor:
      - name: "batt_watt_change"
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        state: >
           {{ (( ( (states('sensor.battery_charge') | float(default=0) * 1) - states('sensor.battery_discharge') | float(default=0)) * 1000)) | round(0) }}
Batt_wh_test - a Riemann sum integral of the watt hour counter

Code: Select all

  - method: left
    name: batt_wh_test
    platform: integration
    round: 2
    source: sensor.batt_watt_change
    unit_time: h
Batt_wh_count - a utility meter used to hold the Riemann sum counter value, and is reset by the automation to start/stop measurement periods

Code: Select all

utility_meter:
  batt_wh_count:
    source: sensor.batt_wh_test
    cycle: monthly
    net_consumption: True
Battery_Wh_Display - a filtered version of batt_wh_calc that smooths steps as the SoC changes - this is the one to display on your dashboard.

Code: Select all


  - platform: filter
    name: "Battery Wh Display"
    entity_id: input_number.watt_hour_calc
    filters:
      - filter: time_simple_moving_average
        window_size: "00:30"
      - filter: lowpass
        time_constant: 20
And finally the Automation (note this has been set for a 6 pack HV2600 with expected 153wh/% change )

Code: Select all

alias: Check SoC
description: ""
trigger:
  - platform: time_pattern
    seconds: /30
condition:
  - condition: or
    conditions:
      - condition: numeric_state
        entity_id: sensor.battery_soc
        above: input_number.last_soc
      - condition: numeric_state
        entity_id: sensor.battery_soc
        below: input_number.last_soc
action:
  - if:
      - condition: template
        value_template: >-
          {{  (states('sensor.battery_soc') | int(0) == 100) or
          (states('sensor.battery_soc') | int(0) == 10 ) or
          (states('input_number.starting_soc')  | int(0) ==0 ) }}
    then:
      - service: input_number.set_value
        target:
          entity_id: input_number.starting_soc
        data:
          value: "{{ (states('sensor.battery_soc') | int(0)) }}"
      - if:
          - condition: numeric_state
            entity_id: sensor.battery_soc
            above: 99
        then:
          - service: utility_meter.calibrate
            data:
              value: "0"
            target:
              entity_id: sensor.batt_wh_count
          - service: input_number.set_value
            data:
              value: 154
            target:
              entity_id: input_number.watt_hour_calc
        else:
          - if:
              - condition: numeric_state
                entity_id: sensor.battery_soc
                below: 11
            then:
              - service: utility_meter.calibrate
                data:
                  value: "0"
                target:
                  entity_id: sensor.batt_wh_count
              - service: input_number.set_value
                data:
                  value: 154
                target:
                  entity_id: input_number.watt_hour_calc
            else:
              - service: utility_meter.calibrate
                data:
                  value: "0"
                target:
                  entity_id: sensor.batt_wh_count
              - service: input_number.set_value
                data:
                  value: 154
                target:
                  entity_id: input_number.watt_hour_calc
    else:
      - if:
          - condition: template
            value_template: >-
              {{ (( ((states('sensor.battery_soc') | int(0) - states('input_number.starting_soc')|int(0) ) ) == 0) or (states('sensor.battery_soc')|int(0)-states('input_number.starting_soc')|int(0))|abs > 19 ) }}
        then:
          - service: utility_meter.calibrate
            data:
              value: "0"
            target:
              entity_id: sensor.batt_wh_count
          - if:
              - condition: template
                value_template: >-
                  {{ (states('sensor.battery_soc')|int(0)-states('input_number.starting_soc')|int(0))|abs > 19 }}
            then:
              - service: input_number.set_value
                target:
                  entity_id: input_number.starting_soc
                data:
                  value: "{{ (states('sensor.battery_soc') | int(0)) }}"
      - service: input_number.set_value
        data:
          value: >-
            {% set expectwh = 154 %} {% set Batsoc = states('sensor.battery_soc')|int(0) - states('input_number.starting_soc')|int(0) %} {% if Batsoc > 0 %}
              {% set whtest = ((states('sensor.batt_wh_count')|int(0))|abs / Batsoc) | round(2) %}
            {% else %}
              {% if Batsoc == 0 %}
                {% set whtest = expectwh %}
              {% else %}
                {% set whtest = ((states('sensor.batt_wh_count')|int(0))|abs / Batsoc|abs) | round(2) %}
              {% endif %}
            {% endif %}
            {% if whtest > (expectwh*1.25) or whtest < (expectwh*0.75) %}
              {% set whtest = expectwh %}
            {% endif %} {{ whtest }}
        target:
          entity_id: input_number.watt_hour_calc
  - service: input_number.set_value
    data:
      value: "{% set Batsoc = states('sensor.battery_soc')|int(0) %} {{ Batsoc }}   "
    target:
      entity_id: input_number.last_soc
mode: single

The results look something like this over 24 and 12 hours ( for a 7 pack system ~ 180wh / % expected ) -
bwhdisp.jpg
bwhdisp1.jpg
Post Reply