Assisted Top-Balancing with Node-RED

Hi all,

i’d like to share a Node-RED flow, that may be helpful for one or another user if needed. I’d like to add, that this is not a Victron Node, just a community-user 2 community-user share. So, handle with care and think along :slight_smile:


Story: Last week I extended my battery storage. Until now, I had 8x Pylontech US3000C and extended them with 8x Pylontech US5000. The modules that arrived measured 49.7V, so I brought the old batteries to 49.7V as well before connecting them - knowing that this still could be a capacity deviation of quite a bunch, as LiFePos have a very flat voltage curve.

After assembling everything and charging the stack up I however encountered another Issue beside the expected Module-Voltage-Deviation: One of the US5000 arrived terrible balanced, with a cell voltage difference of ~ 150mV (3.36V vs approx. 3.49V)

Unfortunately my old US3000C were the ones with a slightly higher “actual soc”, causing them to runaway to 3.54V when “this” 5000 basically was balancing. Total difference then was shown at 180mV, top-level module blocking charging, balancing of the offending 5000 stopped.

So, I decided to design a Node-Red flow that handles both: Module-Equalisation and Cell-Voltage-Balancing:

  • The Pylontech batteries have a passive balancing, starting at 3.36V up to 3.55, only bleeding of 50-100 mA (depending on model) of the highest cell(s) during charging. Addressing the US5000s roughly 150mV imbalance with that - during regular operation - would be days to months.
  • Recommended Balancing Current is 0.03C to 0.06C, which for my “Stack” is 500 to 1000W.
  • To have effective Module-Equalization I needed them to charge/discharge, while their Module Voltage is different, to ensure the weaker module charges a bit more and the stronger module discharges a bit more.
  • so both to be handled in the top-voltage range, bit above the 3.36V Balancing voltage

The node red does the following:

  • Whenever the highest cell reports 3.5V, it alters the grid-setpoint in a way to apply a 2000W discharge.
  • this is kept up down to 3.4V highest cell voltage.
  • Then, it reverts to apply a 2000W (now 1000W) charge rate, until the strongest cell hits 3.5V again.

Here a screenshot showing it in action, the flow json is attached bellow:

After “only” 18 Iterations during the day, I was able to bring the weakest cell up towards the highest cell. At the end, switching to a 2000W discharge and 1000W discharge. Thought: Bypass-Resistors are only active on the “way up”, so stretch that a little longer, make the top-cell able to bleed of more bypass current. (But it could as well just be half the amount for double the time, I have no technical insights on that)

Then, I had to temporary stop, cause Grid-Prices :smiley:

So, this kinda equals 18 days of unattended balancing so far. Most important for seeing “it works” are the indicator lines added (purple, blue) showing the “tension upwards” of the weakest cell(s):

Intermediate Result at that time was 3.42 vs 3.5V, so 80mV - knowing the Pylontech balancing requires 30mV difference at least, that’s 50mV to go to restore “best health”. BMS by now has resumed to report 100% soc instead of only 99%.

When reaching Top-Level-Voltage, the Min/Max Cell-ID now remains within the same Module-Number, which means, we are no longer “fighting” module differences, but only the delivery-imbalance of the US5000 at this point. Any further module-level differences can resolve on it’s own, whenever roundtripping top-level-voltage.

Round 2:
After grid prices returned to somewhat normal again, I charged the battery up to 99% and redeployed the node. I’ve noted that the weakest cell now also started to show it’s top level spiky behaviour, which means, we are almost there.

After the night, we are down to a cell voltage difference of 40 mV, which is fine.

Execution-Hints:

  • After stopping the Node, don’t forget to re-adjust your Grid-Setpoint, to your usual default :slight_smile:
  • The Node automatically starts, when deployed, then runs forever, until undeployed.
  • Runs best with no solar and stable consumption due to the very conservative grid-set-point update (10% of delta), but also works a bit lazy in other cases. I’ve chosen a very gentle adjustment rate, because the BMS reported dc power has a high delay, and otherwise the over/undershooting is to heavy.
Node-Red

If you need to modify the 1000/2000 according to your battery size, that is burried in the “Switch-Direction?”-Node:

[
    {
        "id": "864beb193e8e156f",
        "type": "tab",
        "label": "Assisted Module Balancing",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "2f646e86ef5a4ed9",
        "type": "debug",
        "z": "864beb193e8e156f",
        "name": "Assisted Module Balancing starting 3.4V",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 200,
        "y": 60,
        "wires": []
    },
    {
        "id": "323cae0914417967",
        "type": "victron-input-custom",
        "z": "864beb193e8e156f",
        "service": "com.victronenergy.battery/512",
        "path": "/System/MaxCellVoltage",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "Pylontech-Stack (512)"
        },
        "pathObj": {
            "path": "/System/MaxCellVoltage",
            "name": "/System/MaxCellVoltage",
            "type": "number",
            "value": 3.321000099182129
        },
        "name": "MaxCellVoltage",
        "onlyChanges": true,
        "roundValues": "no",
        "rateLimit": 0,
        "conditionalMode": false,
        "condition1HysteresisEnabled": false,
        "condition1HysteresisThreshold": "",
        "outputTrue": "true",
        "outputFalse": "false",
        "debounce": "2000",
        "x": 120,
        "y": 120,
        "wires": [
            [
                "535113ca9862ce81",
                "f92f5c3d78bf5238"
            ]
        ]
    },
    {
        "id": "535113ca9862ce81",
        "type": "function",
        "z": "864beb193e8e156f",
        "name": "Switch Direction?",
        "func": "if (msg.topic == \"MaxCellVoltage\"){\n    if (msg.payload >= 3.5){\n        flow.set(\"opmode\", \"controlledDischarge\");\n    }\n    \n    else if (msg.payload < 3.4){\n        flow.set(\"opmode\", \"controlledCharge\");\n    }\n}\n\nif (msg.topic == \"dc_power\"){\n    if (flow.get(\"opmode\") == \"controlledCharge\"){\n        let delta = (msg.payload - 1000);\n        let new_setpoint = flow.get(\"setpoint\") - (delta * 0.1);\n        flow.set(\"setpoint\", new_setpoint);\n    }\n    \n    else if (flow.get(\"opmode\") == \"controlledDischarge\"){\n        let delta = (msg.payload + 2000);\n        let new_setpoint = flow.get(\"setpoint\") - (delta * 0.1);\n        flow.set(\"setpoint\", new_setpoint);\n    }\n    \n    else{\n        flow.set(\"setpoint\", -10);\n    }\n    \n    msg.payload = flow.get(\"setpoint\");\n    return msg;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "// Der Code hier wird ausgeführt,\n// wenn der Node gestartet wird\nflow.set(\"setpoint\", 0);\nflow.set(\"opmode\", \"none\");",
        "finalize": "",
        "libs": [],
        "x": 350,
        "y": 240,
        "wires": [
            [
                "37c0484181102b15"
            ]
        ]
    },
    {
        "id": "37c0484181102b15",
        "type": "victron-output-custom",
        "z": "864beb193e8e156f",
        "service": "com.victronenergy.settings",
        "path": "/Settings/CGwacs/AcPowerSetPoint",
        "serviceObj": {
            "service": "com.victronenergy.settings",
            "name": "com.victronenergy.settings"
        },
        "pathObj": {
            "path": "/Settings/CGwacs/AcPowerSetPoint",
            "name": "/Settings/CGwacs/AcPowerSetPoint",
            "type": "number",
            "value": -10
        },
        "name": "Setpoint",
        "onlyChanges": false,
        "roundValues": "no",
        "rateLimit": 0,
        "conditionalMode": false,
        "condition1Operator": ">",
        "condition1HysteresisEnabled": false,
        "condition1HysteresisThreshold": "",
        "condition2Enabled": false,
        "condition2Service": "",
        "condition2Path": "",
        "condition2Operator": ">",
        "logicOperator": "AND",
        "outputTrue": "true",
        "outputFalse": "false",
        "outputOnChange": false,
        "debounce": 2000,
        "x": 560,
        "y": 240,
        "wires": []
    },
    {
        "id": "9db00bdd7ae60674",
        "type": "victron-input-custom",
        "z": "864beb193e8e156f",
        "service": "com.victronenergy.battery/512",
        "path": "/Dc/0/Current",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "Pylontech-Stack (512)"
        },
        "pathObj": {
            "path": "/Dc/0/Current",
            "name": "/Dc/0/Current",
            "type": "number",
            "value": 33.900001525878906
        },
        "name": "dc_current",
        "onlyChanges": true,
        "roundValues": "no",
        "rateLimit": 0,
        "conditionalMode": false,
        "condition1HysteresisEnabled": false,
        "condition1HysteresisThreshold": "",
        "outputTrue": "true",
        "outputFalse": "false",
        "debounce": "2000",
        "x": 100,
        "y": 360,
        "wires": [
            []
        ]
    },
    {
        "id": "66422b4c72d4c8ef",
        "type": "victron-input-custom",
        "z": "864beb193e8e156f",
        "service": "com.victronenergy.battery/512",
        "path": "/Dc/0/Power",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "Pylontech-Stack (512)"
        },
        "pathObj": {
            "path": "/Dc/0/Power",
            "name": "/Dc/0/Power",
            "type": "number",
            "value": 1553
        },
        "name": "dc_power",
        "onlyChanges": true,
        "roundValues": "no",
        "rateLimit": 0,
        "conditionalMode": false,
        "condition1HysteresisEnabled": false,
        "condition1HysteresisThreshold": "",
        "outputTrue": "true",
        "outputFalse": "false",
        "debounce": "2000",
        "x": 100,
        "y": 300,
        "wires": [
            [
                "535113ca9862ce81"
            ]
        ]
    },
    {
        "id": "eb8fe4d40e10d9e8",
        "type": "victron-input-custom",
        "z": "864beb193e8e156f",
        "service": "com.victronenergy.battery/512",
        "path": "/System/MinCellVoltage",
        "serviceObj": {
            "service": "com.victronenergy.battery/512",
            "name": "Pylontech-Stack (512)"
        },
        "pathObj": {
            "path": "/System/MinCellVoltage",
            "name": "/System/MinCellVoltage",
            "type": "number",
            "value": 3.319000005722046
        },
        "name": "MinCellVoltage",
        "onlyChanges": true,
        "roundValues": "no",
        "rateLimit": 0,
        "conditionalMode": false,
        "condition1HysteresisEnabled": false,
        "condition1HysteresisThreshold": "",
        "outputTrue": "true",
        "outputFalse": "false",
        "debounce": "2000",
        "x": 120,
        "y": 240,
        "wires": [
            [
                "f92f5c3d78bf5238"
            ]
        ]
    },
    {
        "id": "f92f5c3d78bf5238",
        "type": "function",
        "z": "864beb193e8e156f",
        "name": "Calc Diff",
        "func": "if (msg.topic == \"MaxCellVoltage\"){\n    flow.set(\"max\", msg.payload);\n}\n\nif (msg.topic == \"MinCellVoltage\"){\n    flow.set(\"min\", msg.payload);\n}\n\nif (flow.get(\"min\") && flow.get(\"max\")){\n    msg.payload = Math.round((flow.get(\"max\")-flow.get(\"min\")) * 100000) / 100.0;\n    flow.set(\"deltaMv\", msg.payload);\n    return msg;\n}",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 320,
        "y": 120,
        "wires": [
            [
                "e04814a14b4feac2",
                "4722e15e19ebf92f"
            ]
        ]
    },
    {
        "id": "e04814a14b4feac2",
        "type": "debug",
        "z": "864beb193e8e156f",
        "name": "Cell Difference mV",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 590,
        "y": 120,
        "wires": []
    },
    {
        "id": "c8ebc3f17c2deef6",
        "type": "inject",
        "z": "864beb193e8e156f",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": true,
        "onceDelay": "5",
        "topic": "",
        "payload": "opmode",
        "payloadType": "flow",
        "x": 340,
        "y": 300,
        "wires": [
            [
                "e711ee716bf9f53d"
            ]
        ]
    },
    {
        "id": "e711ee716bf9f53d",
        "type": "debug",
        "z": "864beb193e8e156f",
        "name": "Operation Mode",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 580,
        "y": 300,
        "wires": []
    },
    {
        "id": "e4e4677f46e523ac",
        "type": "debug",
        "z": "864beb193e8e156f",
        "name": "Estimated Module Difference V",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "payload",
        "statusType": "auto",
        "x": 630,
        "y": 180,
        "wires": []
    },
    {
        "id": "4722e15e19ebf92f",
        "type": "function",
        "z": "864beb193e8e156f",
        "name": "*15/1000",
        "func": "msg.payload = Math.round(msg.payload * 15) / 1000;\nflow.set(\"deltaV\", msg.payload);\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 420,
        "y": 180,
        "wires": [
            [
                "e4e4677f46e523ac"
            ]
        ]
    },
    {
        "id": "b69d62b15a802526",
        "type": "global-config",
        "env": [],
        "modules": {
            "@victronenergy/node-red-contrib-victron": "1.7.13"
        }
    }
]
1 Like