Experimental DESS algorithm using linear programing

This is very interesting. Can you modify that to allow an additional SoC setpoint in the (near, with known price schedule timeframe) future? I have been begging for a port of that function from Node-RED DESS to VRM DESS for ages. Where do start if I’d want to port your code to Node-RED?

PS: weekend bonus flow (Automatic Battery Capacity detecting High Precision SoC% flow for BMV Smartshunts)

[
    {
        "id": "c164cb7636efd2df",
        "type": "group",
        "z": "aca9a41baba8e498",
        "name": "UpCycle Electric :  Battery Capacity and Precision SoC%",
        "style": {
            "label": true
        },
        "nodes": [
            "082878b0754a90a8",
            "f81e692fc62703d7",
            "2866f23c9718c311",
            "80a55421dc061bb4",
            "6d2f544603bebb6e",
            "064a69f1ecebd72d"
        ],
        "x": 34,
        "y": 39,
        "w": 462,
        "h": 142
    },
    {
        "id": "082878b0754a90a8",
        "type": "victron-input-battery",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "service": "com.victronenergy.battery/288",
        "path": "/ConsumedAmphours",
        "serviceObj": {
            "service": "com.victronenergy.battery/288",
            "name": "BMV Battery Monitor"
        },
        "pathObj": {
            "path": "/ConsumedAmphours",
            "type": "float",
            "name": "Consumed Amphours (Ah)"
        },
        "name": "consumedah",
        "onlyChanges": false,
        "x": 130,
        "y": 80,
        "wires": [
            [
                "2866f23c9718c311"
            ]
        ]
    },
    {
        "id": "f81e692fc62703d7",
        "type": "victron-input-battery",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "service": "com.victronenergy.battery/288",
        "path": "/Soc",
        "serviceObj": {
            "service": "com.victronenergy.battery/288",
            "name": "BMV Battery Monitor"
        },
        "pathObj": {
            "path": "/Soc",
            "type": "float",
            "name": "State of charge (%)"
        },
        "name": "soc",
        "onlyChanges": false,
        "x": 110,
        "y": 140,
        "wires": [
            [
                "2866f23c9718c311"
            ]
        ]
    },
    {
        "id": "2866f23c9718c311",
        "type": "function",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "name": "SoC% @ batteryAh",
        "func": "// Battery Capacity Estimator: Async SoC and ConsumedAh to Bound Intersection\n// Receives msg.topic 'soc' (payload float 0.0-100.0 in 0.1 increments) or 'consumedah' (payload float <=0.0 in 0.1 increments)\n// Computes capacity bounds for each pair, intersects to converge on actual integer capacity\n\nvar store = flow.get('storeahsoc') || {\n    ahsoc: undefined,\n    capacity: undefined,\n    latestSoc: undefined,\n    latestConsumed: undefined,\n    minCapacity: 100,  // Initial wide range min\n    maxCapacity: 3000, // Initial wide range max\n    capacityStats: {}\n};\n\n// Optional reset on topic='reset'\nif (msg.topic === 'reset') {\n    store.minCapacity = 100;\n    store.maxCapacity = 3000;\n    flow.set('storeahsoc', store);\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null;\n}\n\n// Update based on topic\nif (msg.topic === 'soc') {\n    var newSoc = Math.round(10 * msg.payload) / 10;\n    if ( newSoc === store.latestSoc) {\n        return null; // Ignore duplicate value\n    }\n    store.latestSoc = newSoc;\n    flow.set('storeahsoc', store); // Persist immediately\n} else if (msg.topic === 'consumedah') {\n    var newConsumed =  Math.round(10 * msg.payload) / 10;\n    if ( newConsumed === store.latestConsumed) {\n        return null; // Ignore duplicate value\n    }\n    store.latestConsumed = newConsumed;\n    flow.set('storeahsoc', store); // Persist immediately\n} else {\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null; // Ignore invalid topic\n}\n\n// Compute only if both present\nif (store.latestSoc === undefined || store.latestConsumed === undefined) {\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null;\n}\nif (store.latestSoc <= 0 || store.latestSoc >= 100) {\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null; // Skip div-zero or invalid\n}\n\n// Compute bounds for this pair\nvar denom = 1 - (store.latestSoc / 100);\nif (denom <= 0) {\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null; // Redundant div-zero guard\n}\n\n// Linear search for consistent capacities\nvar Match = false;\nvar pairMin = store.maxCapacity;\nvar pairMax = store.minCapacity;\nfor (var c = store.minCapacity; c <= store.maxCapacity; c++) {\n    var expectedSoC = Math.round(10 * 100 * (1 + store.latestConsumed / c)) / 10;\n    if (expectedSoC === store.latestSoc) {\n        Match = true;\n        pairMin = Math.min(pairMin, c);\n        pairMax = Math.max(pairMax, c);\n    }\n}\n\n// No match found on a single pair\nif ( !Match && (pairMin == pairMax)) {\n//    node.warn('No Match single pair, widen range');\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    pairMin = pairMin - 1;\n    pairMax = pairMax + 1;\n}\n\n// Reset on inversion\nif (pairMin > pairMax) {\n//    node.warn('pairMin > pairMax, Skip invalid pair');\n    node.status({fill: 'red', shape: 'dot', text: 'wait'});\n    return null; // Skip invalid pair\n}\n\n// Intersect with global range\nstore.minCapacity = Math.max(store.minCapacity, pairMin);\nstore.maxCapacity = Math.min(store.maxCapacity, pairMax);\n\n// Handle inversion as inconsistency (reset)\nif (store.minCapacity > store.maxCapacity) {\n    store.minCapacity = 100;\n    store.maxCapacity = 3000;\n    node.warn('Inversion detected, resetting bounds');\n}\n\n// Determine output: capacity if converged, else midpoint\nif (store.minCapacity === store.maxCapacity) {\n    msg.payload = store.minCapacity;\n} else {\n    msg.payload = Math.round((store.minCapacity + store.maxCapacity) / 2);\n}\n\n// High precision ahsoc integration with div-zero guard\nstore.capacity = msg.payload;\nif (store.capacity !== 0) {\n    store.ahsoc = Math.round( 1000 * 100 * ( 1 + ( store.latestConsumed / store.capacity ) ) ) / 1000;\n} else {\n    store.ahsoc = null;\n}\n\n// Compute and store stats KPI under store.capacityStats\nstore.capacityStats = {\n    uniques: store.minCapacity <= store.maxCapacity ? store.maxCapacity - store.minCapacity + 1 : 0,\n    usingFallback: store.minCapacity !== store.maxCapacity\n};\n\n// Persist all state including stats KPI\nflow.set('storeahsoc', store);\nmsg.ahsoc = store.ahsoc;\nmsg.capacity = store.capacity;\nmsg.accurate = !store.capacityStats.usingFallback;\nmsg.payload = msg.ahsoc;\n\n// node status\nlet text = ( (msg.ahsoc !== undefined) ? ( msg.ahsoc + '% SoC @ '+ msg.capacity + 'Ah' ) : 'wait')\nlet fill = ( ( msg.accurate === true) ? 'green' : 'red')\nlet shape = 'dot'\nnode.status({fill, shape, text});\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 330,
        "y": 80,
        "wires": [
            [
                "80a55421dc061bb4"
            ]
        ]
    },
    {
        "id": "80a55421dc061bb4",
        "type": "link out",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "name": "link out 86",
        "mode": "link",
        "links": [
            "6d2f544603bebb6e",
            "c1b98e927af57511"
        ],
        "x": 455,
        "y": 80,
        "wires": []
    },
    {
        "id": "6d2f544603bebb6e",
        "type": "link in",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "name": "link in 102",
        "links": [
            "80a55421dc061bb4"
        ],
        "x": 255,
        "y": 140,
        "wires": [
            [
                "064a69f1ecebd72d"
            ]
        ]
    },
    {
        "id": "064a69f1ecebd72d",
        "type": "debug",
        "z": "aca9a41baba8e498",
        "g": "c164cb7636efd2df",
        "name": "AhSoC%",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "true",
        "targetType": "full",
        "statusVal": "payload",
        "statusType": "msg",
        "x": 360,
        "y": 140,
        "wires": []
    },
    {
        "id": "580227e4bbf43e36",
        "type": "global-config",
        "env": [],
        "modules": {
            "@victronenergy/node-red-contrib-victron": "1.6.52"
        }
    }
]

version 1.0, for code see also: How to fetch the battery capacity as defined in smartshunt-settings? - #19 by UpCycleElectric