How to fetch the battery capacity as defined in smartshunt-settings?

new approach, ultrafast

release 1.0 you can close this topic now

[
    {
        "id": "aca9a41baba8e498",
        "type": "tab",
        "label": "Flow 2",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "94127cfb4562fe53",
        "type": "group",
        "z": "aca9a41baba8e498",
        "name": "UpCycle Electric :  Battery Capacity and Precision SoC%",
        "style": {
            "label": true
        },
        "nodes": [
            "8c1672a223109fca",
            "b3269873eac10d0f",
            "e848a541d3e51b71",
            "590191036e5d0675",
            "d8a091ca57518a5b",
            "ed592eeaa570f1e5"
        ],
        "x": 34,
        "y": 39,
        "w": 462,
        "h": 142
    },
    {
        "id": "8c1672a223109fca",
        "type": "victron-input-battery",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "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": [
            [
                "e848a541d3e51b71"
            ]
        ]
    },
    {
        "id": "b3269873eac10d0f",
        "type": "victron-input-battery",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "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": [
            [
                "e848a541d3e51b71"
            ]
        ]
    },
    {
        "id": "e848a541d3e51b71",
        "type": "function",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "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    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\n//var pairMin = 3000;\n//var pairMax = 100;\n//for (var c = 100; c <= 3000; c++) {\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        pairMin = Math.min(pairMin, c);\n        pairMax = Math.max(pairMax, c);\n    }\n}\n\nif (pairMin > pairMax) {\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// Reset on inversion\nif (store.minCapacity > store.capacity) {\n    store.minCapacity = store.minCapacity - 1\n    node.warn('Inversion detected, resetting minCapacity');\n}\nif (store.maxCapacity < store.minCapacity) {\n    store.maxCapacity = store.maxCapacity + 1\n    node.warn('Inversion detected, resetting maxCapacity');\n}\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": [
            [
                "590191036e5d0675"
            ]
        ]
    },
    {
        "id": "590191036e5d0675",
        "type": "link out",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "name": "link out 86",
        "mode": "link",
        "links": [
            "d8a091ca57518a5b",
            "c1b98e927af57511"
        ],
        "x": 455,
        "y": 80,
        "wires": []
    },
    {
        "id": "d8a091ca57518a5b",
        "type": "link in",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "name": "link in 102",
        "links": [
            "590191036e5d0675"
        ],
        "x": 255,
        "y": 140,
        "wires": [
            [
                "ed592eeaa570f1e5"
            ]
        ]
    },
    {
        "id": "ed592eeaa570f1e5",
        "type": "debug",
        "z": "aca9a41baba8e498",
        "g": "94127cfb4562fe53",
        "name": "AhSoC%",
        "active": true,
        "tosidebar": false,
        "console": false,
        "tostatus": true,
        "complete": "true",
        "targetType": "full",
        "statusVal": "payload",
        "statusType": "msg",
        "x": 360,
        "y": 140,
        "wires": []
    },
    {
        "id": "d8bfb64a07523f9a",
        "type": "global-config",
        "env": [],
        "modules": {
            "@victronenergy/node-red-contrib-victron": "1.6.52"
        }
    }
]
// Battery Capacity Estimator: Async SoC and ConsumedAh to Bound Intersection
// Receives msg.topic 'soc' (payload float 0.0-100.0 in 0.1 increments) or 'consumedah' (payload float <=0.0 in 0.1 increments)
// Computes capacity bounds for each pair, intersects to converge on actual integer capacity

var store = flow.get('storeahsoc') || {
    ahsoc: undefined,
    capacity: undefined,
    latestSoc: undefined,
    latestConsumed: undefined,
    minCapacity: 100,  // Initial wide range min
    maxCapacity: 3000, // Initial wide range max
    capacityStats: {}
};

// Optional reset on topic='reset'
if (msg.topic === 'reset') {
    store.minCapacity = 100;
    store.maxCapacity = 3000;
    flow.set('storeahsoc', store);
    return null;
}

// Update based on topic
if (msg.topic === 'soc') {
    var newSoc = Math.round(10 * msg.payload) / 10;
    if ( newSoc === store.latestSoc) {
        return null; // Ignore duplicate value
    }
    store.latestSoc = newSoc;
    flow.set('storeahsoc', store); // Persist immediately
} else if (msg.topic === 'consumedah') {
    var newConsumed =  Math.round(10 * msg.payload) / 10;
    if ( newConsumed === store.latestConsumed) {
        return null; // Ignore duplicate value
    }
    store.latestConsumed = newConsumed;
    flow.set('storeahsoc', store); // Persist immediately
} else {
    node.status({fill: 'red', shape: 'dot', text: 'wait'});
    return null; // Ignore invalid topic
}

// Compute only if both present
if (store.latestSoc === undefined || store.latestConsumed === undefined) {
    node.status({fill: 'red', shape: 'dot', text: 'wait'});
    return null;
}
if (store.latestSoc <= 0 || store.latestSoc >= 100) {
    node.status({fill: 'red', shape: 'dot', text: 'wait'});
    return null; // Skip div-zero or invalid
}

// Compute bounds for this pair
var denom = 1 - (store.latestSoc / 100);
if (denom <= 0) {
    node.status({fill: 'red', shape: 'dot', text: 'wait'});
    return null; // Redundant div-zero guard
}

// Linear search for consistent capacities
//var pairMin = 3000;
//var pairMax = 100;
//for (var c = 100; c <= 3000; c++) {
var pairMin = store.maxCapacity;
var pairMax = store.minCapacity;
for (var c = store.minCapacity; c <= store.maxCapacity; c++) {
    var expectedSoC = Math.round(10 * 100 * (1 + store.latestConsumed / c)) / 10;
    if (expectedSoC === store.latestSoc) {
        pairMin = Math.min(pairMin, c);
        pairMax = Math.max(pairMax, c);
    }
}

if (pairMin > pairMax) {
    node.status({fill: 'red', shape: 'dot', text: 'wait'});
    return null; // Skip invalid pair
}

// Intersect with global range
store.minCapacity = Math.max(store.minCapacity, pairMin);
store.maxCapacity = Math.min(store.maxCapacity, pairMax);

// Reset on inversion
if (store.minCapacity > store.capacity) {
    store.minCapacity = store.minCapacity - 1
    node.warn('Inversion detected, resetting minCapacity');
}
if (store.maxCapacity < store.minCapacity) {
    store.maxCapacity = store.maxCapacity + 1
    node.warn('Inversion detected, resetting maxCapacity');
}


// Determine output: capacity if converged, else midpoint
if (store.minCapacity === store.maxCapacity) {
    msg.payload = store.minCapacity;
} else {
    msg.payload = Math.round((store.minCapacity + store.maxCapacity) / 2);
}

// High precision ahsoc integration with div-zero guard
store.capacity = msg.payload;
if (store.capacity !== 0) {
    store.ahsoc = Math.round( 1000 * 100 * ( 1 + ( store.latestConsumed / store.capacity ) ) ) / 1000;
} else {
    store.ahsoc = null;
}

// Compute and store stats KPI under store.capacityStats
store.capacityStats = {
    uniques: store.minCapacity <= store.maxCapacity ? store.maxCapacity - store.minCapacity + 1 : 0,
    usingFallback: store.minCapacity !== store.maxCapacity
};

// Persist all state including stats KPI
flow.set('storeahsoc', store);
msg.ahsoc = store.ahsoc;
msg.capacity = store.capacity;
msg.accurate = !store.capacityStats.usingFallback;
msg.payload = msg.ahsoc;

// node status
let text = ( (msg.ahsoc !== undefined) ? ( msg.ahsoc + '% SoC @ '+ msg.capacity + 'Ah' ) : 'wait')
let fill = ( ( msg.accurate === true) ? 'green' : 'red')
let shape = 'dot'
node.status({fill, shape, text});
return msg;