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

**URL:** <https://community.victronenergy.com/t/how-to-fetch-the-battery-capacity-as-defined-in-smartshunt-settings/48346>\
**Category:** Node-RED\
**Tags:** multiplus-ii, cerbo\_gx, victronconnect, diy\
**Created:** [7 November 2025 09:23 UTC](https://community.victronenergy.com/t/how-to-fetch-the-battery-capacity-as-defined-in-smartshunt-settings/48346 "2025-11-07T09:23:03Z")\
**Posts on this page:** 1\
**Showing post:** 29

<div class="post-metadata">

**Author:** ![UpCycleElectric](https://dub1.discourse-cdn.com/victron/user_avatar/community.victronenergy.com/upcycleelectric/32/36895_2.png) [@UpCycleElectric](https://community.victronenergy.com/u/UpCycleElectric)\
**Post date:** [25 November 2025 16:09 UTC](https://community.victronenergy.com/t/how-to-fetch-the-battery-capacity-as-defined-in-smartshunt-settings/48346/29 "2025-11-25T16:09:09Z")

</div>

> [@UpCycleElectric](#):
>
> Minor update, see if it helps, version 1.2:

Version 1.3, very small change on line 93, increased testwindow: “Match = absDelta \< 0.055555;”  
Surely there are opportunities to improve on the rounding and selection mechanism (that discards ‘unfitting’ capacity candidates) but this works perfectly for us. The correct capacity is found within a handful of value updates and when I change the capacity in the BMV manually, it automatically re-aligns with the new values as well.

```javascript
// 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
    misMatches: 0,
    capacityStats: {},
    debugStats: {}
};

// debug values
var rawSoc = null;
var rawConsumed = null;
var rawminCapacity = null;
var rawmaxCapacity = null;
var rawexpectedSoc = null;
var rawMatch = null;
var rawDelta = null;
var rawpairMin = null;
var rawpairMax = null;
var rawahSoc = null;

// Optional reset on topic='reset'
if (msg.topic === 'reset') {
    store.minCapacity = 100;
    store.maxCapacity = 3000;
    store.misMatches = 0;
    flow.set('storeahsoc', store);
    node.status({fill: 'red', shape: 'dot', text: 'topic reset'});
    return null;
}

rawSoc = store.latestSoc;
rawConsumed = store.latestConsumed;
// Update based on topic
if (msg.topic === 'soc') {
    var newSoc = Math.round(1000 * msg.payload) / 1000;
    rawSoc = newSoc;
    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(1000 * msg.payload) / 1000;
    rawConsumed = newConsumed;
    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: 'wait1'});
    return null; // Ignore invalid topic
}

// Compute only if both present
if (store.latestSoc === undefined || store.latestConsumed === undefined) {
    node.status({fill: 'red', shape: 'dot', text: 'wait2'});
    return null;
}
if (store.latestSoc <= 0 || store.latestSoc >= 100) {
    node.status({fill: 'red', shape: 'dot', text: 'wait3'});
    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: 'wait4'});
    return null; // Redundant div-zero guard
}

// Linear search for consistent capacities
rawminCapacity = store.minCapacity;
rawmaxCapacity = store.maxCapacity;
var Match = false;
var pairMin = store.maxCapacity;
var pairMax = store.minCapacity;
for ( var c = store.minCapacity; c <= store.maxCapacity; c++ ) {
    rawexpectedSoc = 100 * ( 1 + store.latestConsumed / c );
    rawexpectedSoc = Math.round(1000 * rawexpectedSoc ) / 1000;
    rawDelta = rawexpectedSoc - store.latestSoc;
    rawDelta = Math.round(1000 * rawDelta) / 1000;
    var expectedSoC = Math.round(10 * rawexpectedSoc ) / 10;
    var absDelta = Math.abs( rawDelta );
    Match = absDelta < 0.055555;
    if ( Match ) {
        pairMin = Math.min(pairMin, c);
        pairMax = Math.max(pairMax, c);
    }
}
rawMatch = Match;
rawpairMin = pairMin;
rawpairMax = pairMax;

// No match found on a single pair
if ( !Match && ( pairMin == pairMax ) ) {
    node.warn('No Match single pair, widen range');
    node.status({fill: 'red', shape: 'dot', text: 'nomatch single'});
    pairMin = pairMin - 1;
// pairMax = pairMax + 1;
}

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

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

// Handle inversion as inconsistency (reset)
if ( store.minCapacity > store.maxCapacity ) {
    store.minCapacity = 100;
    store.maxCapacity = 3000;
    store.misMatches = 0;
    node.warn('Inversion detected, resetting bounds');
    node.status({fill: 'red', shape: 'dot', text: 'reset bounds'});
}

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

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

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

// count mismatches
//var calculatedSoC = Math.round( 10 * store.ahsoc ) / 10;
//var misMatch = calculatedSoC !== store.latestSoc
//var misMatch = !Match;
//if ( misMatch && ( statsuniques <= 2 ) ) {
if ( !Match && statsuniques <= 2 ) {
    store.misMatches = store.misMatches + 1;
    if (store.misMatches >= 2) {
        store.minCapacity = Math.round( 0.9 * store.minCapacity );
        store.maxCapacity = Math.round( 1.1 * store.maxCapacity );
        store.misMatches = 0;
        node.warn('2 mismatches, reset');
        node.status({fill: 'red', shape: 'dot', text: 'mismatch reset'});
    }
}
if ( !Match ) {
//if ( true ) {
    store.debugStats = {
        rawSoc: rawSoc,
        rawConsumed: rawConsumed,
        rawminCapacity: rawminCapacity,
        rawmaxCapacity: rawmaxCapacity,
        rawexpectedSoc: rawexpectedSoc,
        rawMatch: rawMatch,
        rawDelta: rawDelta,
        rawpairMin: rawpairMin,
        rawpairMax: rawpairMax,
        rawahSoc: rawahSoc
    }
}

// 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 + '% ' + (-rawDelta) +' SoC '+ statsuniques + ' @ '+ msg.capacity + 'Ah' ) : 'wait')
let fill = ( ( (msg.accurate === true) && Match ) ? 'green' : 'red')
let shape = 'dot'
node.status({fill, shape, text});

return msg;

```

---

_[View the full topic](https://community.victronenergy.com/t/how-to-fetch-the-battery-capacity-as-defined-in-smartshunt-settings/48346)._
