Solar Batteries: How do you calculate SOC (Voltage in Percent)

I used the Template function to perform the calculations for me. Using the charts 1 cell value, I approximated the voltages to 2 decimal places.

Using this information, I created a template that would take the voltage, find the correct 10% divide, and calculate the approximate percentage accordingly:

{% set voltage = states('sensor.battery_voltage_voltmeter') | float %}
{% macro CalculatePercentageTenth(voltageLow, voltageHigh, offset) %}
  {{ (((voltage - voltageLow) / (voltageHigh - voltageLow) * 10) + offset) | round(1) }}
{% endmacro %}
{% if voltage <= 10 %}
  0
{% elif voltage <= 12 %}
  {{ CalculatePercentageTenth(10, 12, 0) }}
{% elif voltage <= 12.8 %}
  {{ CalculatePercentageTenth(12, 12.8, 10) }}
{% elif voltage <= 12.88 %}
  {{ CalculatePercentageTenth(12.8, 12.88, 20) }}
{% elif voltage <= 13.0 %}
  {{ CalculatePercentageTenth(12.88, 13, 30) }}
{% elif voltage <= 13.04 %}
  {{ CalculatePercentageTenth(13.0, 13.04, 40) }}
{% elif voltage <= 13.08 %}
  {{ CalculatePercentageTenth(13.04, 13.08, 50) }}
{% elif voltage <= 13.2 %}
  {{ CalculatePercentageTenth(13.08, 13.2, 60) }}
{% elif voltage <= 13.28 %}
  {{ CalculatePercentageTenth(13.2, 13.28, 70) }}
{% elif voltage <= 13.4 %}
  {{ CalculatePercentageTenth(13.28, 13.4, 80) }}
{% elif voltage <= 13.6 %}
  {{ CalculatePercentageTenth(13.4, 13.6, 90) }}
{% elif voltage > 13.6 %}
  100
{% endif %}

Create a Template Helper that is a Battery class with a % unit and this gives me what I needed!

This works somehow, but relies on false assumptions. Yet better than nothing.

16S LiFePO4 Voltage-Based SOC with Load Compensation

Here is a template sensor to estimate SOC for EVE 312Ah (16S) cells that remains accurate even under high loads.

The Logic:

Standard voltage-based SOC fluctuates wildly during charging/discharging due to internal resistance and connection impedance.

I implemented a Virtual Voltage calculation using Ohm’s Law:

$$V_{calc} = V_{raw} - (Current \times R_{total})$$

Calibration Details:

  • Hardware: 16S EVE 312Ah + JK BMS.
  • Compensation: I tuned the system resistance ($R_{total}$) to 0.008$\Omega$. This perfectly offsets the voltage rise seen at 60A (3kW) charging, keeping the SOC stable (e.g., accurate 75% instead of a fake 90%).
  • Curve: Custom mapping for EVE chemistry (50% SOC set at 3.28V/cell).
{% set v_raw = states('sensor.battery1_jk_bms_total_voltage') | float(0) %}
{% set power = states('sensor.battery1_jk_bms_power') | float(0) %}

{# --- FACTOR DE CORECȚIE RECALIBRAT --- #}
{# La 3300W (62A), scadem aprox 0.5V pentru a ajunge de la 90% la 75% #}
{% set r_system = 0.008 %}

{# Calcul curent (I = P / V). Verificam sa nu impartim la 0 #}
{% set current = power / v_raw if v_raw > 0 else 0 %}

{# Calcul Voltaj Corectat #}
{# Daca incarci (+), scadem voltajul fals. Daca descarci (-), adunam caderea. #}
{% set v_diff = current * r_system %}
{% set v = v_raw - v_diff %}

{# --- HARTA DE VOLTAJ (16S LiFePO4) --- #}
{% set points = [
  [44.8, 0], [48.0, 5], [49.6, 10], [50.8, 15], [51.2, 20],
  [51.68, 30], [52.16, 40], [52.48, 50], [52.80, 60],
  [52.96, 70], [53.28, 80], [53.44, 90], [53.76, 95], [54.40, 100]
] %}

{# --- PLASĂ DE SIGURANȚĂ PENTRU ÎNCĂRCARE --- #}
{# Daca voltajul brut e sub 54.0V, nu lasa SOC-ul sa sara de 98% #}
{# Asta previne afisarea de 100% prematur cand incarci cu curent mare #}
{% if v_raw < 54.0 and v > 54.0 %}
  {% set v = 53.9 %} 
{% endif %}

{# Limite #}
{% if v > 54.5 %} {% set v = 54.5 %} {% endif %}
{% if v < 44.0 %} {% set v = 44.0 %} {% endif %}

{# Interpolare #}
{% if v >= points[-1][0] %}
  100
{% elif v <= points[0][0] %}
  0
{% else %}
  {% set ns = namespace(found=false, result=0) %}
  {% for i in range(points | length - 1) %}
    {% if not ns.found and v >= points[i][0] and v < points[i+1][0] %}
      {% set v1 = points[i][0] %}
      {% set p1 = points[i][1] %}
      {% set v2 = points[i+1][0] %}
      {% set p2 = points[i+1][1] %}
      {% set soc = p1 + (v - v1) * (p2 - p1) / (v2 - v1) %}
      {% set ns.result = soc | round(1) %}
      {% set ns.found = true %}
    {% endif %}
  {% endfor %}
  {{ ns.result }}
{% endif %}