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

I am wondering how a LiFePo4 Battery discharge curve could be configured in HA, so that the SOC (in Voltage/ Percent) can be calculated.

Dischage table for Lifepo4:

lifepo4-voltage-chart

The 24V Battery is 100% full at 27V, and 0% empty at 20V. Since the discharge curve isn’t linear, I am wondering how a sensor formula could look like?

Just add all the points to this:

Thanks Tom,

I have read the documentation and wonder if this is what I am looking for, probably since I do not understand this 100% yet.

I don’t want to change any of the voltage values, but basically have the percentages adapt to the non linear curve.

xLiFePO4-discharging.gif.pagespeed.ic.EQVEXWFoto

At the moment I am reading the SOC through a Shelly UNI (sensor.solarbattery_soc_adc). This sensor shouldn’t change, but the percentage sensor (sensor.solarbattery_soc_percent) should.

It is.

This replaces whatever config already you have for the percentage sensor.

compensation:
  solarbattery_soc_percent:
    source: sensor.solarbattery_soc_adc
    unit_of_measurement: '%'
    data_points:
      - [27.2, 100]
      - [26.8, 90]
      - [26.6, 80]
      - [26.4, 70]
      - [26.1, 50]
      - [26, 40]
      - [25.8, 30]
      - [25.6, 20]
      - [24, 10]
      - [20, 0]

Like this?

# Sensor for Battery SOC in Prozent v2
      - name: "Solarspeicher SOC (Prozent)"
        unique_id: "socv2"
        unit_of_measurement: '%'

compensation:
  solarspeicher_soc_prozent:
    source: sensor.solarspeicher_soc_adc
    unit_of_measurement: '%'
    data_points:
      - [27, 100]
      - [26.8, 99]
      - [26.6, 90]
      - [26.4, 70]
      - [26.26, 50]
      - [26.2, 40]
      - [25.8, 20]
      - [25.6, 10]
      - [21.6, 1]
      - [20, 0]

The values are for my battery.

Yes. It goes in your configuration.yaml file (not your sensors.yaml file) and you probably want to specify a degree of 6 instead of the default 1 (which will try to fit a straight line).

yes, I am doing all this in my configuration.yaml.

However, if I validate the yaml I am receiving errors so I didn’t apply any of these settings yet. Playing around with this in Templating does produce anything.

Do I need to attach the compensate statement somehow with the sensor definition? As I have it right now the code doesn’t work and the documentation isn’t clear about this too.

# Sensor für Batterie SOC in Prozent v2
  - sensor:
      - name: "Solarspeicher SOC (Prozent)"
        unique_id: socv2
        unit_of_measurement: '%'
        state:
        
compensation:
  solarspeicher_soc_prozent:
    source: sensor.solarspeicher_soc_adc
    unit_of_measurement: '%'
    degree: 6
    data_points:
      - [27, 100]
      - [26.8, 99]
      - [26.6, 90]
      - [26.4, 70]
      - [26.26, 50]
      - [26.2, 40]
      - [25.8, 20]
      - [25.6, 10]
      - [21.6, 1]
      - [20, 0]

You need to remove the existing sensor definition: the top six lines that you pasted above. The compensation filter replaces this altogether.

The compensation filter should be in configuration.yaml with compensation: not indented, fully left aligned.

You will need to restart your system once you have done this.

Assuming you meant “does not”, that’s because there are no templates in use here.

Thanks much Troon & Tom - this was to simple (once you get it of course :slight_smile: )

Working code I am using, so others can copy paste:
Note: This is for a 24V 100Ah Powerqueen LiFePo4 Battery.

compensation:
    solarbattery_soc_percent:
    unique_id: sollarbatterysocpercent
    source: sensor.solarbattery_soc_adc
    unit_of_measurement: '%'
    degree: 5
    data_points:     
       - [27, 100]
       - [26.8, 99]
       - [26.6, 90]
       - [26.4, 70] 
       - [26.2, 40]
       - [26.0, 30]
       - [25.8, 20]
       - [25.6, 10]
       - [21.6, 1]
       - [19, 0]

edit: changed degree to 5, since 6 gave me values above 100%, although the measured voltage was below 27V. Not sure if this has en effect, but for now the values make sense.

1 Like

I would love to develop a better SoC calculation.
Defining SoC from the battery voltage curve is not ideal, and I would prefer using a similar method as what Coulombmeter are using.
Using the actual battery bank capacity in Ah, define a Wh capacity (easy part), and monitor the Charge/Discharge Amps versus Voltage to define what’s actually left. Way more accurate and reliable.
I’ll start to work on that, maybe using Node-RED… I will post an update if I am successful!

FYI: You will need to take charge/discharge efficiency into account for that to be true. You never get as much out as you put in. It’s about 95% for Li and 80% for Pb. It will change as the battery ages though.

This is why your laptop has to do a full charge / discharge cycle every so often for calibration.

Efficiency = Full discharge Wh / Full Charge Wh.

That is correct, I have completed the flow yesterday and added those inefficiencies and Hybrid Inverter internal consumption (30Wh) into my calculations.

This is now a before and after.

Before: Note the correlation between Battery Voltage and SoC (bad way)


After: Now we finally have something that looks like a realistic SoC

And this is the way I have implemented this under NodeRed:

I am using the 100% from the Inverter as a trigger (it reaches it after the BoostCharge at the end of the float charge cycle on mine).
It then resets (almost as a daily self-calibration) the Calculated SoC and stores the current Charge and Discharge Total Accumulated values. Then I am monitoring the evolving values and calculate the SoC (including the inefficiency factor and Hybrid charger/inverter internal consumption that are not appearing in the returned value for some reason).

Hope this helps someone out there.

Note : I did this for both myself and a friend, screenshot are of his setup, where he has way more panels than storage capacity. I already used this method my own setup and I ended up doing a full charge/discharge cycle and the SoC was (surprisingly) very close to 1% accuracy. I hit 0% calculated SoC when I was at the end of the diving voltage curve at the same time my Inverter Bypass rule kicked in, at 10v per battery. Now I can refine the other rules to avoid over-discharge.

Next step would be to add some more math to define battery health overtime to adjust the actual battery capacity in kWh overtime.

Nice work. Could you also edit your post to include the exported code from Node Red so others can import it?

I’m still all new to NodeRed, started a week ago. This is a Work-in-Progress code, I already made some modifications for clarity.
Here is the current version of my NodeRed code, not sure if this is the format you are looking for, please let me know!
Also, you will notice I did not connect the “Disable/Enable”, which was originally disabling the Daily “Calibration” reset until 4AM, I decided to reset those every time I trigger 100%.

[
    {
        "id": "3efd09077d0129be",
        "type": "tab",
        "label": "SoC from Coulombmeter",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "0123ea899e205216",
        "type": "trigger-state",
        "z": "3efd09077d0129be",
        "name": "PowMr 100% Batt",
        "server": "17ce6379.07b5bd",
        "version": 4,
        "inputs": 1,
        "outputs": 2,
        "exposeAsEntityConfig": "",
        "entityId": "sensor.powmr_battery_soc",
        "entityIdType": "exact",
        "debugEnabled": false,
        "constraints": [
            {
                "targetType": "this_entity",
                "targetValue": "",
                "propertyType": "current_state",
                "propertyValue": "new_state.state",
                "comparatorType": "is",
                "comparatorValueDatatype": "num",
                "comparatorValue": "100"
            }
        ],
        "customOutputs": [],
        "outputInitially": false,
        "stateType": "num",
        "enableInput": true,
        "x": 210,
        "y": 300,
        "wires": [
            [
                "f6f644a7beb05859",
                "1c5612c3ec556e93",
                "a072f1bb911879a4",
                "d0e6d2bf7af1244e",
                "3140e88f1f06f953",
                "df5e43e91aa5c831"
            ],
            []
        ]
    },
    {
        "id": "a072f1bb911879a4",
        "type": "api-current-state",
        "z": "3efd09077d0129be",
        "name": "ResetSetCharge",
        "server": "17ce6379.07b5bd",
        "version": 3,
        "outputs": 2,
        "halt_if": "null",
        "halt_if_type": "str",
        "halt_if_compare": "is_not",
        "entity_id": "sensor.powmr_battery_charging_energy",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "setcharge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 520,
        "y": 260,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "e04ab094d7058f35",
        "type": "inject",
        "z": "3efd09077d0129be",
        "name": "ManualReset 100%",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 150,
        "y": 360,
        "wires": [
            [
                "a072f1bb911879a4",
                "d0e6d2bf7af1244e",
                "3140e88f1f06f953",
                "df5e43e91aa5c831"
            ]
        ]
    },
    {
        "id": "d0e6d2bf7af1244e",
        "type": "api-current-state",
        "z": "3efd09077d0129be",
        "name": "ResetSetDischarge",
        "server": "17ce6379.07b5bd",
        "version": 3,
        "outputs": 2,
        "halt_if": "null",
        "halt_if_type": "str",
        "halt_if_compare": "is_not",
        "entity_id": "sensor.powmr_battery_discharge_energy",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "setdischarge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 530,
        "y": 320,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "e8c8a526d160bc7f",
        "type": "function",
        "z": "3efd09077d0129be",
        "name": "Math (TallyToday)",
        "func": "var tday = (Number(flow.get(\"adjdischarge\"))) - (Number(flow.get(\"adjcharge\")));\nmsg.payload = tday;\nflow.set(\"tday\", tday);\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 650,
        "y": 660,
        "wires": [
            [
                "63797bfb2df3633a"
            ]
        ]
    },
    {
        "id": "23556dba05eadb49",
        "type": "inject",
        "z": "3efd09077d0129be",
        "name": "ManualPoll",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 240,
        "y": 540,
        "wires": [
            [
                "6fe9ad59b724bfdf"
            ]
        ]
    },
    {
        "id": "63797bfb2df3633a",
        "type": "function",
        "z": "3efd09077d0129be",
        "name": "Math (SoC)",
        "func": "var cap = (Number(flow.get(\"cap\")));\nvar tday = (Number(flow.get(\"tday\")));\n\nvar soc = (cap - tday) / cap * 100;\n\nif (soc > 100) {\n    soc = 100;\n} \n\nsoc = Math.round(soc * 100) / 100;\n\nflow.set(\"soc\", soc);\nmsg.payload = soc;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 750,
        "y": 720,
        "wires": [
            [
                "1e0679d6d4451ff1"
            ]
        ]
    },
    {
        "id": "1c5612c3ec556e93",
        "type": "api-current-state",
        "z": "3efd09077d0129be",
        "name": "Set Capacity & Ineff. Factors",
        "server": "17ce6379.07b5bd",
        "version": 3,
        "outputs": 2,
        "halt_if": "null",
        "halt_if_type": "str",
        "halt_if_compare": "is_not",
        "entity_id": "sensor.battery_capacity_kwh",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "cap",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "cineff",
                "propertyType": "flow",
                "value": "0.65",
                "valueType": "num"
            },
            {
                "property": "dineff",
                "propertyType": "flow",
                "value": "1.15",
                "valueType": "num"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 560,
        "y": 200,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "f6f644a7beb05859",
        "type": "change",
        "z": "3efd09077d0129be",
        "name": "Disable",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "disable",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 300,
        "y": 140,
        "wires": [
            [
                "fae461869872bdf9"
            ]
        ]
    },
    {
        "id": "40b41615af9839d2",
        "type": "change",
        "z": "3efd09077d0129be",
        "name": "Enable",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "enable",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 300,
        "y": 100,
        "wires": [
            [
                "fae461869872bdf9"
            ]
        ]
    },
    {
        "id": "39ce607439285035",
        "type": "inject",
        "z": "3efd09077d0129be",
        "name": "04:00",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "00 04 * * *",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 140,
        "y": 100,
        "wires": [
            [
                "40b41615af9839d2"
            ]
        ]
    },
    {
        "id": "fae461869872bdf9",
        "type": "link out",
        "z": "3efd09077d0129be",
        "name": "SoC",
        "mode": "link",
        "links": [
            "e6fe843f32415dc6"
        ],
        "x": 455,
        "y": 120,
        "wires": []
    },
    {
        "id": "e6fe843f32415dc6",
        "type": "link in",
        "z": "3efd09077d0129be",
        "name": "SoC",
        "links": [
            "fae461869872bdf9"
        ],
        "x": 75,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "5688a4bd163e2138",
        "type": "server-state-changed",
        "z": "3efd09077d0129be",
        "name": "PollCharge AutoUpdate",
        "server": "17ce6379.07b5bd",
        "version": 5,
        "outputs": 2,
        "exposeAsEntityConfig": "",
        "entityId": "sensor.powmr_battery_charging_energy",
        "entityIdType": "exact",
        "outputInitially": false,
        "stateType": "num",
        "ifState": "null",
        "ifStateType": "str",
        "ifStateOperator": "is_not",
        "outputOnlyOnStateChange": true,
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "ignorePrevStateNull": false,
        "ignorePrevStateUnknown": false,
        "ignorePrevStateUnavailable": false,
        "ignoreCurrentStateUnknown": false,
        "ignoreCurrentStateUnavailable": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "pollcharge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "x": 200,
        "y": 600,
        "wires": [
            [
                "6fe9ad59b724bfdf"
            ],
            []
        ]
    },
    {
        "id": "c0d957fccdc048be",
        "type": "server-state-changed",
        "z": "3efd09077d0129be",
        "name": "PollDischarge AutoUpdate",
        "server": "17ce6379.07b5bd",
        "version": 5,
        "outputs": 2,
        "exposeAsEntityConfig": "",
        "entityId": "sensor.powmr_battery_discharge_energy",
        "entityIdType": "exact",
        "outputInitially": false,
        "stateType": "num",
        "ifState": "null",
        "ifStateType": "str",
        "ifStateOperator": "is_not",
        "outputOnlyOnStateChange": true,
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "ignorePrevStateNull": false,
        "ignorePrevStateUnknown": false,
        "ignorePrevStateUnavailable": false,
        "ignoreCurrentStateUnknown": false,
        "ignoreCurrentStateUnavailable": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "polldischarge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "x": 210,
        "y": 660,
        "wires": [
            [
                "6fe9ad59b724bfdf"
            ],
            []
        ]
    },
    {
        "id": "1e0679d6d4451ff1",
        "type": "mqtt out",
        "z": "3efd09077d0129be",
        "name": "NR Battery SoC",
        "topic": "NodeRed/nr_batt_soc",
        "qos": "",
        "retain": "true",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "ed661f0c78cde115",
        "x": 960,
        "y": 660,
        "wires": []
    },
    {
        "id": "7c1e0357217856e5",
        "type": "mqtt out",
        "z": "3efd09077d0129be",
        "name": "Initial Setup (RunOnce)",
        "topic": "homeassistant/sensor/nr_batt_soc/config",
        "qos": "",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "ed661f0c78cde115",
        "x": 550,
        "y": 40,
        "wires": []
    },
    {
        "id": "c1899e557f21381a",
        "type": "inject",
        "z": "3efd09077d0129be",
        "name": "InitMQTT Sensor (RunONCE)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "{\"name\": \"Battery SoC\", \"object_id\": \"nr_batt_soc\", \"device_class\": \"battery\", \"unit_of_measurement\": \"%\", \"unique_id\": \"nr_batt_soc\", \"platform\": \"mqtt\", \"expire_after\": 3600, \"state_topic\": \"NodeRed/nr_batt_soc\", \"device\": {\"name\": \"Node Red\", \"identifiers\": \"NR_SOC_FLOW\"}}",
        "payloadType": "str",
        "x": 200,
        "y": 40,
        "wires": [
            [
                "7c1e0357217856e5"
            ]
        ]
    },
    {
        "id": "3140e88f1f06f953",
        "type": "api-current-state",
        "z": "3efd09077d0129be",
        "name": "ResetPollDischarge",
        "server": "17ce6379.07b5bd",
        "version": 3,
        "outputs": 2,
        "halt_if": "null",
        "halt_if_type": "str",
        "halt_if_compare": "is_not",
        "entity_id": "sensor.powmr_battery_discharge_energy",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "polldischarge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 530,
        "y": 440,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "df5e43e91aa5c831",
        "type": "api-current-state",
        "z": "3efd09077d0129be",
        "name": "ResetPollCharge",
        "server": "17ce6379.07b5bd",
        "version": 3,
        "outputs": 2,
        "halt_if": "null",
        "halt_if_type": "str",
        "halt_if_compare": "is_not",
        "entity_id": "sensor.powmr_battery_charging_energy",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "pollcharge",
                "propertyType": "flow",
                "value": "",
                "valueType": "entityState"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 530,
        "y": 380,
        "wires": [
            [],
            []
        ]
    },
    {
        "id": "6fe9ad59b724bfdf",
        "type": "function",
        "z": "3efd09077d0129be",
        "name": "Math - Deltas",
        "func": "var deltacharge = (Number(flow.get(\"pollcharge\"))) - (Number(flow.get(\"setcharge\")));\nvar deltadischarge = (Number(flow.get(\"polldischarge\"))) - (Number(flow.get(\"setdischarge\")));\n\nflow.set(\"deltacharge\", deltacharge);\nflow.set(\"deltadischarge\", deltadischarge);\n\nmsg.payload = \"NextStep\";\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 470,
        "y": 540,
        "wires": [
            [
                "1ee36c792884a552"
            ]
        ]
    },
    {
        "id": "1ee36c792884a552",
        "type": "function",
        "z": "3efd09077d0129be",
        "name": "Math (Ineff. Factor)",
        "func": "var adjcharge = (Number(flow.get(\"deltacharge\"))) * (Number(flow.get(\"cineff\")));\nvar adjdischarge = (Number(flow.get(\"deltadischarge\"))) * (Number(flow.get(\"dineff\")));\n\nflow.set(\"adjcharge\", adjcharge);\nflow.set(\"adjdischarge\", adjdischarge);\n\nmsg.payload = \"NextStep\";\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 570,
        "y": 600,
        "wires": [
            [
                "e8c8a526d160bc7f"
            ]
        ]
    },
    {
        "id": "87e7d9844bf2166c",
        "type": "inject",
        "z": "3efd09077d0129be",
        "name": "ManualReset Cap&Ineff",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 160,
        "y": 240,
        "wires": [
            [
                "1c5612c3ec556e93"
            ]
        ]
    },
    {
        "id": "17ce6379.07b5bd",
        "type": "server",
        "name": "Home Assistant",
        "addon": true
    },
    {
        "id": "ed661f0c78cde115",
        "type": "mqtt-broker",
        "name": "YourUserName",
        "broker": "localhost",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "false",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willRetain": "false",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]

We typically use “preformatted Text” to copy our code into a block that can be copied out.

image

Gotcha thanks :slight_smile:
Previous post edited, you’re right, more readable and collapsable

You can easily do it without NodeRed, I’ve detailed it here: https://www.reddit.com/r/batteries/comments/13xt3n2/soc_calculation_by_coulomb_counting_in/

Judging from voltage to SOC does not work, only when empty or full. Those tables do not work. Big problem is that voltage depends on load! But it is easy to detect the drop at empty and the rise at full SOC.

I did two different implementation, one that does reset at 0% SOC and one that resets at 100%. Totally depends on your scenario which is better.

Very nice ideas on methods to compute a “more accurate” SOC. I learned about the compensation sensor - which has simplified my coding for including the temperature compensation curve in my batteries (useful) capacity computation. I had figured out the best fit polynomials in excel, but now I’m just using the compensation sensors, with the degree set to 2. I had already figured out that a polynomial best fit has an R^2 of 0.9996, so a very good fit…

In my case, my batteries are located in a very remote cabin and the temperatures can vary from about -10F to 80F. Once I get to the cabin in winter and get some heating going where the batteries are stored, then the low end of the battery temperature is about 30F, which means I only have about 68% of the total battery capacity available to use at that temperature…