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;