Noob needs help to convert voltage to percentage

Hi All!

I am a rookie in home assistant, and trying to figure out this:

Using “Battery State Card / Entity Row” card, it works perfectly, but one of the entities has battery level as voltage, not percentage, so it looks like this now:

Capture

That is 2,78 volt, the max is 3 volt.
How can I configure the card to show the battery percentage?

Config is:

type: custom:battery-state-card
title: Battery levels
entities:
  - entity: sensor.sonoff_a48001d0a6_temperature
    name: Kids room temperature sensor
  - entity: binary_sensor.sonoff_a48001d0a8
    name: Laundry room motion sensor
  - entity: binary_sensor.sonoff_livingroom
    name: Living room window sensor

Thank you in advance! :grinning:

You’ll need to configure a template sensor in your configuration.yaml.

The following post shows an example template that may apply to your situation:

Regards,
Chris

Cheers Chris,

Thank you for your reply. I already found that thread, but wasn’t able to get anything out of it that works :confused:
Could you give me any parts of a template to start with? Sorry, I’m dumber than I thought…

My 3d printer has a sensor that reports the estimated finish time (sensor.mk3s_02_estimated_finish_time) but by default it displays a long UTC timestamp such as 2021-11-08T00:21:02.135422+00:00. I use a template sensor to make it more human-readable in the UI.


Before creating your custom sensor, you can test your templates within the Dev Tools Template Editor.


For example, I use the following template to display the printer’s finish time in 24-hour format instead of the long UTC timestamp:

{{ as_timestamp(states('sensor.mk3s_02_estimated_finish_time')) | timestamp_custom('%H:%M')}}	  

Here's what the output looks like in the Template Editor before and after template is applied to the sensor:


What's your current template code and what are you seeing in the Template Editor results?

Regards,
Chris

Try something like this:

sensor battery:
  - platform: template
    sensors:
      battery_on_some_device:
        friendly_name: "Battery level on device"
        value_template: >-
          {{ ((state_attr('binary_sensor.guestroom_thermometer_low_bat','voltage')-2.8)/0.2)*100 }}
        icon_template: >-
          {% if ((state_attr('binary_sensor.guestroom_thermometer_low_bat','voltage')|float) >= 3 ) %}
              mdi:battery-high
          {% elif ((state_attr('binary_sensor.guestroom_thermometer_low_bat','voltage')|float) >= 2.9 ) %}
              mdi:battery-low
          {% else %}
              mdi:battery-outline
          {% endif %}
        unit_of_measurement: "%"

The problem with batteries are the way the discharge.
They are made to keep their voltage up for a long time and then suddenly die, so you will see hardly no change between 90% full and 20% full and at 15% the voltage might be so low already that there is not enough power to drive the electronic, so the true 0% is actually when the battery have 15% capacity left.

A typical graph for a battery discharging would be like this:
Discharge Voltage Curve Typical

2 Likes

Hey Wally.

Thank for your reply. Your post helped me understand how relations in templates work, this is what I got at the end:

This just calculates the percentage based on the actual voltage data of the sensor.
This is the sensor I made:

It works perfectly:

3

Thank you for the help for all of you, I learned something new today! :grinning:

Looks good. Congratulations!

You might have to do a trial and error on how low voltage can be before you loose the connection, but it’s a good start.

I have a LIFEPO4 battery.
Charging min 23.9V max 28.8V
0% 23.9V
100% 27.2V
I used a sample template, but after exceeding the voltage of 27.2V, the value is 0%. Up to 27.2V works well.
Can you advise?

  - platform: template
    sensors:
      baterie_fv_procent:
        friendly_name: Baterie FV procenta
        value_template: >-
          {% if states('sensor.reg_napeti') | float(0) > 27.2 %}
          {% else %}{{ ((states('sensor.reg_napeti') | float(0)-23.9) / 0.033) | round(0) }}
          {% endif %}
        unit_of_measurement: "%"
        icon_template: "mdi:battery-charging-high"

Your if statement on above 27.2 is empty

          {% if states('sensor.reg_napeti') | float(0) > 27.2 %}

          {% else %}
              {{ ((states('sensor.reg_napeti') | float(0)-23.9) / 0.033) | round(0) }}
          {% endif %}

I expect you want that to be 100?

          {% if states('sensor.reg_napeti') | float(0) > 27.2 %}
              100
          {% else %}
              {{ ((states('sensor.reg_napeti') | float(0)-23.9) / 0.033) | round(0) }}
          {% endif %}

Thank you very much. Yes, I needed to set it to 100. I finally understood how this template works. Works perfectly. Thank you once again.

I took a bit of a different approach… Instead of the if/else statements, I created a filtered version of the voltage entity that enforces the upper and lower bounds and also smoothes the voltage out by averaging it over a time window. The new voltage is used for the percentage calculation which now cannot go above 100% or below 0%.

sensor:

  - platform: filter
    name: "filtered battery voltage"
    entity_id: sensor.battery_voltage
    filters:
      - filter: range
        lower_bound: 10.8
        upper_bound: 12.2
      - filter: time_simple_moving_average
        window_size: "00:00:30"

  - platform: template
    sensors:
      battery_charge:
        value_template: "{{ ((states('sensor.filtered_battery_voltage')|float - 10.8) / ((12.2 - 10.8) / 100))|round(0) }}"
        device_class: battery
        unit_of_measurement: "%"

Hi guys,

I’ve been having this problem too. I was using a giant if else structure but I knew it wasn’t inferring the real percentages correctly. After doing a lot of research, and I’ll confess, enquiring with chat GPT to help me figure out that maths, I’ve managed to create a template that takes the known data points for a battery (in my case a LiFePO₄ battery) and use a Linear Interpolation algorithm to infer the correct percentage from any point in the defined data. Obviously the more known points in the voltage, the better the accuracy, but I’m working with the chart provided from the manufacturer of my battery and putting my trust in the inferences made using this algorithm for any in-between points.

Please do feel free to copy this template, and if anyone is clever enough to turn this into an integration so we can quickly specify the curve and input entity faster in future, go for it! :slight_smile:

template:
  - sensor:
    - name: "Prism Battery Percentage"
      state_class: measurement
      availability: states.sensor.prism_bus_voltage_1
      state: >
        {% set voltage = (states.sensor.prism_bus_voltage_1.state | float) %}
        {% set curve = [
            { "voltage": 10,   "percentage": 0 },
            { "voltage": 12,   "percentage": 9 },
            { "voltage": 12.5, "percentage": 14 },
            { "voltage": 12.8, "percentage": 17 },
            { "voltage": 12.9, "percentage": 20 },
            { "voltage": 13,   "percentage": 30 },
            { "voltage": 13.1, "percentage": 40 },
            { "voltage": 13.2, "percentage": 70 },
            { "voltage": 13.3, "percentage": 90 },
            { "voltage": 13.4, "percentage": 99 },
            { "voltage": 13.6, "percentage": 100 }
          ]
        %}
        {% set minVoltage = curve[0].voltage %}
        {% set maxVoltage = curve[curve|length-1].voltage %}
        {% if voltage < minVoltage %}
          0
        {% elif voltage > maxVoltage %}
          100
        {% else %}
          {% for i in range(curve|length-1) %}
            {% set x1 = curve[i].voltage %}
            {% set y1 = curve[i].percentage %}
            {% set x2 = curve[i + 1].voltage %}
            {% set y2 = curve[i + 1].percentage %}
            {% if voltage >= x1 and voltage <= x2 %}
              {{ (y1 + (y2 - y1) * ((voltage - x1) / (x2 - x1))) | round(1) }}
            {% endif %}
          {% endfor %}
        {% endif %}
      unit_of_measurement: "%"

Please note that I have very specifically designed this to show 0% minimum and 100% maximum. If your voltage drops above or below these values, don’t expect it to start showing negative values or values above 100% :slight_smile:

Oh and here’s it all in javascript, if anyone should find it useful:

function inferPercentage(data, voltage, decimalPlaces = 1) {
    let x1 = data[0].voltage, y1 = data[0].percentage, x2 = data[1].voltage, y2 = data[1].percentage;
    let minVoltage = data[0].voltage;
    let maxVoltage = data[data.length-1].voltage;
    if(voltage<minVoltage) return 0;
    if(voltage>maxVoltage) return 100;
    for (let i = 0; i < data.length - 1; i++) {
        x1 = data[i].voltage;
        y1 = data[i].percentage;
        x2 = data[i + 1].voltage;
        y2 = data[i + 1].percentage;
        if (voltage >= x1 && voltage <= x2) {
            return parseFloat((y1 + (y2 - y1) * ((voltage - x1) / (x2 - x1))).toFixed(decimalPlaces));
        }
    }
}

const data = [
  { voltage: 10, percentage: 0 },
  { voltage: 12, percentage: 9 },
  { voltage: 12.5, percentage: 14 },
  { voltage: 12.8, percentage: 17 },
  { voltage: 12.9, percentage: 20 },
  { voltage: 13, percentage: 30 },
  { voltage: 13.1, percentage: 40 },
  { voltage: 13.2, percentage: 70 },
  { voltage: 13.3, percentage: 90 },
  { voltage: 13.4, percentage: 99 },
  { voltage: 13.6, percentage: 100 }
];
console.log(inferPercentage(data, 12.63, 1))

There’s a built in compensation integration that does this for you without needing the math.

2 Likes

I can’t begin to describe the pain of how much googling and discussion in support groups I have had with no one able to provide help with this problem, only to finally create a solution and you point to a built in integration that does it all for us. Eugh.

Next time just ask by creating a topic! Plenty of integrations out there that hardly anyone uses that just work.

Just keep in mind that this does not use interpolation, it uses a polynomial fit. So it’s a different algorithm than what you’re using.