Not sure whether it matters but I always set the BMV Smartshunt to:
Peukert exponent 1.00
Charge efficiency factor 100%
Minor update, see if it helps, version 1.2:
[
{
"id": "aca9a41baba8e498",
"type": "tab",
"label": "Flow 2",
"disabled": false,
"info": "",
"env": []
},
{
"id": "68b65dc97ef1cc5a",
"type": "group",
"z": "aca9a41baba8e498",
"name": "UpCycle Electric : Battery Capacity and Precision SoC%",
"style": {
"label": true
},
"nodes": [
"cbaa78a710257e7e",
"115d9f615c2bad76",
"6384ab7f49dddff4",
"3e20f1d85f0988bb",
"0be46f7b216b5462",
"edb2f2d5990f334b",
"600d744385e5ff14",
"7758b585988dfc10",
"d5afd63c79002b7e"
],
"x": 34,
"y": 19,
"w": 442,
"h": 262
},
{
"id": "cbaa78a710257e7e",
"type": "victron-input-battery",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"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": 120,
"wires": [
[
"0be46f7b216b5462"
]
]
},
{
"id": "115d9f615c2bad76",
"type": "victron-input-battery",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"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": 180,
"wires": [
[
"6384ab7f49dddff4"
]
]
},
{
"id": "6384ab7f49dddff4",
"type": "function",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"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 misMatches: 0,\n capacityStats: {},\n debugStats: {}\n};\n\n// debug values\nvar rawSoc = null;\nvar rawConsumed = null;\nvar rawminCapacity = null;\nvar rawmaxCapacity = null;\nvar rawexpectedSoc = null;\nvar rawMatch = null;\nvar rawDelta = null;\nvar rawpairMin = null;\nvar rawpairMax = null;\nvar rawahSoc = null;\n\n// Optional reset on topic='reset'\nif (msg.topic === 'reset') {\n store.minCapacity = 100;\n store.maxCapacity = 3000;\n store.misMatches = 0;\n flow.set('storeahsoc', store);\n node.status({fill: 'red', shape: 'dot', text: 'topic reset'});\n return null;\n}\n\nrawSoc = store.latestSoc;\nrawConsumed = store.latestConsumed;\n// Update based on topic\nif (msg.topic === 'soc') {\n var newSoc = Math.round(1000 * msg.payload) / 1000;\n rawSoc = newSoc;\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(1000 * msg.payload) / 1000;\n rawConsumed = newConsumed;\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: 'wait1'});\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: 'wait2'});\n return null;\n}\nif (store.latestSoc <= 0 || store.latestSoc >= 100) {\n node.status({fill: 'red', shape: 'dot', text: 'wait3'});\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: 'wait4'});\n return null; // Redundant div-zero guard\n}\n\n// Linear search for consistent capacities\nrawminCapacity = store.minCapacity;\nrawmaxCapacity = store.maxCapacity;\nvar Match = false;\nvar pairMin = store.maxCapacity;\nvar pairMax = store.minCapacity;\nfor ( var c = store.minCapacity; c <= store.maxCapacity; c++ ) {\n rawexpectedSoc = 100 * ( 1 + store.latestConsumed / c );\n rawexpectedSoc = Math.round(1000 * rawexpectedSoc ) / 1000;\n rawDelta = rawexpectedSoc - store.latestSoc;\n rawDelta = Math.round(1000 * rawDelta) / 1000;\n var expectedSoC = Math.round(10 * rawexpectedSoc ) / 10;\n var absDelta = Math.abs( rawDelta );\n Match = absDelta < 0.0505;\n if ( Match ) {\n pairMin = Math.min(pairMin, c);\n pairMax = Math.max(pairMax, c);\n }\n}\nrawMatch = Match;\nrawpairMin = pairMin;\nrawpairMax = pairMax;\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: 'nomatch single'});\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: 'skip invalid'});\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 store.misMatches = 0;\n node.warn('Inversion detected, resetting bounds');\n node.status({fill: 'red', shape: 'dot', text: 'reset bounds'});\n}\n\n// Determine output: capacity if converged, else midpoint\nif ( store.minCapacity === store.maxCapacity ) {\n msg.payload = store.minCapacity;\n} else if ( store.uniques > 2 ) {\n msg.payload = Math.round( ( store.minCapacity + store.maxCapacity ) / 2 );\n} else if ( store.maxCapacity - store.minCapacity > 1 ) {\n msg.payload = Math.round( ( store.minCapacity + store.maxCapacity ) / 2 );\n} else {\n msg.payload = store.minCapacity;\n}\n\n// High precision ahsoc integration with div-zero guard\nstore.capacity = msg.payload;\nif ( store.capacity !== 0 ) {\n rawahSoc = 100 * (1 + (store.latestConsumed / store.capacity) );\n rawahSoc = Math.round(1000 * rawahSoc) / 1000;\n store.ahsoc = rawahSoc;\n} else {\n store.ahsoc = null;\n}\n\n// Compute and store stats KPI under store.capacityStats\nvar statsuniques = store.minCapacity <= store.maxCapacity ? store.maxCapacity - store.minCapacity + 1 : 0\nvar statsusingFallback = ( statsuniques > 1 );//store.minCapacity !== store.maxCapacity\nstore.capacityStats = {\n uniques: statsuniques,\n usingFallback: statsusingFallback \n};\n\n// count mismatches\n//var calculatedSoC = Math.round( 10 * store.ahsoc ) / 10;\n//var misMatch = calculatedSoC !== store.latestSoc\n//var misMatch = !Match;\n//if ( misMatch && ( statsuniques <= 2 ) ) {\nif ( !Match && statsuniques <= 2 ) {\n store.misMatches = store.misMatches + 1;\n if (store.misMatches >= 2) {\n store.minCapacity = Math.round( 0.9 * store.minCapacity );\n store.maxCapacity = Math.round( 1.1 * store.maxCapacity );\n store.misMatches = 0;\n node.warn('2 mismatches, reset');\n node.status({fill: 'red', shape: 'dot', text: 'mismatch reset'});\n }\n}\nif ( !Match ) {\n//if ( true ) {\n store.debugStats = {\n rawSoc: rawSoc,\n rawConsumed: rawConsumed,\n rawminCapacity: rawminCapacity,\n rawmaxCapacity: rawmaxCapacity,\n rawexpectedSoc: rawexpectedSoc,\n rawMatch: rawMatch,\n rawDelta: rawDelta,\n rawpairMin: rawpairMin,\n rawpairMax: rawpairMax,\n rawahSoc: rawahSoc\n }\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 + '% ' + (-rawDelta) +' SoC '+ statsuniques + ' @ '+ msg.capacity + 'Ah' ) : 'wait')\nlet fill = ( ( (msg.accurate === true) && Match ) ? 'green' : 'red')\nlet shape = 'dot'\nnode.status({fill, shape, text});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 310,
"y": 180,
"wires": [
[
"3e20f1d85f0988bb"
]
]
},
{
"id": "3e20f1d85f0988bb",
"type": "link out",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "link out 86",
"mode": "link",
"links": [
"d5afd63c79002b7e",
"c1b98e927af57511"
],
"x": 435,
"y": 180,
"wires": []
},
{
"id": "0be46f7b216b5462",
"type": "delay",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "",
"pauseType": "delay",
"timeout": "10",
"timeoutUnits": "milliseconds",
"rate": "1",
"nbRateUnits": "15",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 290,
"y": 120,
"wires": [
[
"6384ab7f49dddff4"
]
]
},
{
"id": "edb2f2d5990f334b",
"type": "change",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "",
"rules": [
{
"t": "delete",
"p": "storeahsoc",
"pt": "flow"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 320,
"y": 60,
"wires": [
[]
]
},
{
"id": "600d744385e5ff14",
"type": "inject",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 140,
"y": 60,
"wires": [
[
"edb2f2d5990f334b"
]
]
},
{
"id": "7758b585988dfc10",
"type": "debug",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "AhSoC%",
"active": true,
"tosidebar": false,
"console": false,
"tostatus": true,
"complete": "true",
"targetType": "full",
"statusVal": "payload",
"statusType": "msg",
"x": 340,
"y": 240,
"wires": []
},
{
"id": "d5afd63c79002b7e",
"type": "link in",
"z": "aca9a41baba8e498",
"g": "68b65dc97ef1cc5a",
"name": "link in 102",
"links": [
"3e20f1d85f0988bb"
],
"x": 235,
"y": 240,
"wires": [
[
"7758b585988dfc10"
]
]
},
{
"id": "ceec3db10f622a01",
"type": "global-config",
"env": [],
"modules": {
"@victronenergy/node-red-contrib-victron": "1.6.55"
}
}
]