Skip to content

Commit

Permalink
BatteryGuard: Open circuit voltage and internal resistance
Browse files Browse the repository at this point in the history
  • Loading branch information
SW-Niko committed Dec 16, 2024
1 parent a37fd0b commit 58a21c2
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 0 deletions.
54 changes: 54 additions & 0 deletions include/BatteryGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once
#include <Arduino.h>
#include <frozen/string.h>
#include <TaskSchedulerDeclarations.h>
#include "Statistic.h"


class BatteryGuardClass {
public:
BatteryGuardClass() = default;
~BatteryGuardClass() = default;

void init(Scheduler& scheduler);
void updateSettings(void);

std::optional<float> calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent);
std::optional<float> getOpenCircuitVoltage(void) const;
std::optional<float> calculateInternalResistance(float const nowVoltage, float const nowCurrent);
std::optional<float> getInternalResistance(void) const;

private:
enum class Text : uint8_t {
Q_NODATA = 0,
Q_EXCELLENT = 1,
Q_GOOD = 2,
Q_BAD = 3,
T_HEAD = 4
};
void loop(void);
void printOpenCircuitVoltageInformationBlock(void);
frozen::string const& getText(Text tNr);

// used for calculation of the "Open circuit voltage"
WeightedAVG<float> _openCircuitVoltageAVG {10}; // battery open circuit voltage (average factor 10%)
uint32_t _lastOCVMillis = 0; // last millis of calculation of the open circuit voltage
float _resistorConfig = 0.0f; // value from configuration or resistance calculation

// used for calculation of the "Battery internal resistance"
WeightedAVG<float> _internalResistanceAVG {10}; // resistor (average factor 10%)
bool _firstOfTwoAvailable = false; // false after to got the first of two values
bool _minMaxAvailable = false; // minimum and maximum values available
std::pair<float,float> _firstVolt = {0.0f,0.0f}; // first of two voltage and related current
std::pair<float,float> _maxVolt = {0.0f,0.0f}; // maximum voltage and related current
std::pair<float,float> _minVolt = {0.0f,0.0f}; // minimum voltage and related current
uint32_t _lastMinMaxMillis = 0; // last millis from the first min/max values
float const _minDiffVoltage = 0.05f; // 50mV minimum difference to calculate a resistance (Smart Shunt)
// unclear if this value will also fit to other battery provider

Task _loopTask; // Task
bool _verboseLogging = false; // Logging On/Off
bool _useBatteryGuard = false; // "Battery guard" On/Off
};

extern BatteryGuardClass BatteryGuard;
3 changes: 3 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,9 @@ class VictronSmartShuntStats : public BatteryStats {
bool _alarmLowSOC;
bool _alarmLowTemperature;
bool _alarmHighTemperature;

std::optional<float> _oBatteryResistor = std::nullopt;
std::optional<float> _oOpenCircuitVoltage = std::nullopt;
};

class MqttBatteryStats : public BatteryStats {
Expand Down
56 changes: 56 additions & 0 deletions include/Statistic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

/*
* Weighted average and statistics class (initialising value defines the weighted average 10 = 10%)
*/
template <typename T>
class WeightedAVG {
public:
explicit WeightedAVG(size_t factor)
: _countMax(factor)
, _count(0), _countNum(0), _avgV(0), _minV(0), _maxV(0), _lastV(0) {}

// Add a value to the statistics
void addNumber(const T& num) {
if (_count == 0){
_count++;
_avgV = num;
_minV = num;
_maxV = num;
_countNum = 1;
} else {
if (_count < _countMax)
_count++;
_avgV = (_avgV * (_count - 1) + num) / _count;
if (num < _minV) { _minV = num; }
if (num > _maxV) { _maxV = num; }
if (_countNum < 10000) { _countNum++; }
}
_lastV = num;
}

// Reset the statistic data
void reset(void) { _count = 0; _avgV = 0; _minV = 0; _maxV = 0; _lastV = 0; _countNum = 0; }
// Reset the statistic data and initialize with first value
void reset(const T& num) { _count = 0; addNumber(num); }
// Returns the weighted average
T getAverage() const { return _avgV; }
// Returns the minimum value
T getMin() const { return _minV; }
// Returns the maximum value
T getMax() const { return _maxV; }
// Returns the last added value
T getLast() const { return _lastV; }
// Returns the amount of added values. Limited to 10000
size_t getCounts() const { return _countNum; }

private:
size_t _countMax; // weighting factor (10 => 1/10 => 10%)
size_t _count; // counter (0 - _countMax)
size_t _countNum; // counts the amount of added values (0 - 10000)
T _avgV; // average value
T _minV; // minimum value
T _maxV; // maximum value
T _lastV; // last value
};

233 changes: 233 additions & 0 deletions src/BatteryGuard.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/* Battery-Guard
*
* The Battery-Guard has several functions.
* - Calculate the battery internal resistance
* - Calculate the battery open circuit voltage
* - Limit the power drawn from the battery, if the battery voltage is close to the stop threshold. (draft)
* - Periodically recharge the battery to 100% SoC (draft)
*
* Basic principe of the function: "Battery internal resistance"
* Collects minimum and maximum values (voltage and current) over a time frame. Calculates the resistance from these values
* and build a weighed average.
*
* Basic principe of the function: "Open circuit voltage"
* Use the battery internal resistance to calculate the open circuit voltage and build a weighed average.
*
* Basic principe of the function: "Low voltage limiter"
* If the battery voltage is close to the stop threshold, the battery limiter will calculate a maximum power limit
* to keep the battery voltage above the voltage threshold.
* The inverter is only switched-off when the threshold is exceeded and the inverter output cannot be reduced any further.
*
* Basic principe of the function: "Periodically recharge the battery"
* After some days we start to reduce barriers, to make it more easier to fully charge the battery.
* When we reach 100% SoC we remove all restrictions and start a new period.
* Especially usefull during winter to calibrate the SoC calculation of the BMS
*
* Notes:
* Some function are still under development.
*
* 01.08.2024 - 0.1 - first version. "Low voltage power limiter"
* 09.12.2024 - 0.2 - add of function "Periodically recharge the battery"
* 11.12.2024 - 0.3 - add of function "Battery internal resistance" and "Open circuit voltage"
*/

#include <frozen/map.h>
#include "Configuration.h"
#include "MessageOutput.h"
#include "BatteryGuard.h"


// support for debugging, 0 = without extended logging, 1 = with extended logging, 2 = with much more logging
constexpr int MODULE_DEBUG = 0;

BatteryGuardClass BatteryGuard;


/*
* Initialize the battery guard
*/
void BatteryGuardClass::init(Scheduler& scheduler) {

// init the task loop
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&BatteryGuardClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.setInterval(60*1000);
_loopTask.enable();

updateSettings();
}


/*
* Update some settings of the battery guard
*/
void BatteryGuardClass::updateSettings(void) {

// todo: get values from the configuration
_verboseLogging = true;
_useBatteryGuard = true;

// used for "Open circuit voltage"
_resistorConfig = 0.012f;
}


/*
* Periodical tasks, will be called once a minute
*/
void BatteryGuardClass::loop(void) {

if (_useBatteryGuard && _verboseLogging) {
MessageOutput.printf("%s\r\n", getText(Text::T_HEAD).data());
MessageOutput.printf("%s ---------------- Battery-Guard information block (every minute) ----------------\r\n",
getText(Text::T_HEAD).data());
MessageOutput.printf("%s\r\n", getText(Text::T_HEAD).data());
}

// "Open circuit voltage"
if (_useBatteryGuard && _verboseLogging) {
printOpenCircuitVoltageInformationBlock();
}

// "Low voltage power limiter"


// "Periodically recharge the battery"

}



/*
* Calculate the battery open circuit voltage.
* Returns the weighted average value or nullptr if calculation is not possible or if the value is out of date.
*/
std::optional<float> BatteryGuardClass::calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent) {

// calculate the open circuit battery voltage (current flow into the battery must be positive)
auto oResistor = getInternalResistance();
if ((nowVoltage > 0.0f) && (oResistor.has_value())) {
_openCircuitVoltageAVG.addNumber(nowVoltage - nowCurrent * oResistor.value());
_lastOCVMillis = millis();
}
return getOpenCircuitVoltage();
}


/*
* Returns the battery internal resistance, calculated / configured or nullopt if neither value is valid
*/
std::optional<float> BatteryGuardClass::getInternalResistance(void) const {
if (_internalResistanceAVG.getCounts() > 4) { return _internalResistanceAVG.getAverage(); }
if (_resistorConfig != 0.0f) { return _resistorConfig; }
return std::nullopt;
}


/*
* Returns the battery open circuit voltage or nullopt if value is not valid
*/
std::optional<float> BatteryGuardClass::getOpenCircuitVoltage(void) const {
if ((_openCircuitVoltageAVG.getCounts() > 0) && (millis() - _lastOCVMillis) < 30*1000) {
return _openCircuitVoltageAVG.getAverage();
} else {
return std::nullopt;
}
}


/*
* Calculate the battery internal resistance between the battery cells and the voltage measurement device. (BMS, MPPT, Inverter)
* Returns the resistance, calculated / configured or nullopt if neither value is valid
*/
std::optional<float> BatteryGuardClass::calculateInternalResistance(float const nowVoltage, float const nowCurrent) {

if (nowVoltage <= 0.0f) { return getInternalResistance(); }

// we must avoid to use measurement values during any power transition.
// To solve this problem, we check whether two consecutive measurements are almost identical (5mV, 200mA)
if (!_firstOfTwoAvailable || (std::abs(_firstVolt.first - nowVoltage) > 0.005f) ||
(std::abs(_firstVolt.second - nowCurrent) > 0.2f)) {
_firstVolt.first = nowVoltage;
_firstVolt.second = nowCurrent;
_firstOfTwoAvailable = true;
return getInternalResistance();
}
_firstOfTwoAvailable = false; // prepair for the next calculation

// store the average in min or max buffer
std::pair<float,float> avgVolt = std::make_pair((nowVoltage + _firstVolt.first) / 2.0f, (nowCurrent + _firstVolt.second) / 2.0f);
if (!_minMaxAvailable) {
_minVolt = _maxVolt = avgVolt;
_lastMinMaxMillis = millis();
_minMaxAvailable = true;
} else {
if (avgVolt.first < _minVolt.first) { _minVolt = avgVolt; }
if (avgVolt.first > _maxVolt.first) { _maxVolt = avgVolt; }
}

// we evaluate min and max values in a time duration of 30 sec
if ((!_minMaxAvailable || (millis() - _lastMinMaxMillis) < 30*1000)) { return getInternalResistance(); }
_minMaxAvailable = false; // prepair for the next calculation

// we need a minimum voltage difference to get a sufficiently good result (failure < 10%)
// SmartShunt: 50mV (about 100W on VDC: 24V, Ri: 12mOhm)
if ((_maxVolt.first - _minVolt.first) >= _minDiffVoltage) {
float resistor = std::abs((_maxVolt.first - _minVolt.first) / (_maxVolt.second - _minVolt.second));

// we try to keep out bad values from the average
if (_internalResistanceAVG.getCounts() < 10) {
_internalResistanceAVG.addNumber(resistor);
} else {
if ((resistor > _internalResistanceAVG.getAverage() / 2.0f) && (resistor < _internalResistanceAVG.getAverage() * 2.0f)) {
_internalResistanceAVG.addNumber(resistor);
}
}

// todo: delete after testing
if constexpr(MODULE_DEBUG >= 1) {
MessageOutput.printf("%s Resistor - Calculated: %0.3fOhm\r\n", getText(Text::T_HEAD).data(), resistor);
}
}
return getInternalResistance();
}


/*
* prints the "Battery open circuit voltage" information block
*/
void BatteryGuardClass::printOpenCircuitVoltageInformationBlock(void)
{
MessageOutput.printf("%s 1) Function: Battery open circuit voltage\r\n",
getText(Text::T_HEAD).data());

MessageOutput.printf("%s Open circuit voltage: %0.3fV\r\n",
getText(Text::T_HEAD).data(), _openCircuitVoltageAVG.getAverage());

MessageOutput.printf("%s Internal resistance: %0.4fOhm (Min: %0.4f, Max: %0.4f, Last: %0.4f, Amount: %i)\r\n",
getText(Text::T_HEAD).data(), _internalResistanceAVG.getAverage(), _internalResistanceAVG.getMin(),
_internalResistanceAVG.getMax(), _internalResistanceAVG.getLast(), _internalResistanceAVG.getCounts() - 1);
}


/*
* Returns a string according to current text nr
*/
frozen::string const& BatteryGuardClass::getText(BatteryGuardClass::Text tNr)
{
static const frozen::string missing = "programmer error: missing status text";

static const frozen::map<Text, frozen::string, 5> texts = {
{ Text::Q_NODATA, "Insufficient data" },
{ Text::Q_EXCELLENT, "Excellent" },
{ Text::Q_GOOD, "Good" },
{ Text::Q_BAD, "Bad" },
{ Text::T_HEAD, "[Battery-Guard]"}
};

auto iter = texts.find(tNr);
if (iter == texts.end()) { return missing; }

return iter->second;
}
12 changes: 12 additions & 0 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "JkBmsDataPoints.h"
#include "JbdBmsDataPoints.h"
#include "MqttSettings.h"
#include "BatteryGuard.h"

template<typename T>
static void addLiveViewInSection(JsonVariant& root,
Expand Down Expand Up @@ -863,6 +864,11 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& s
_alarmLowTemperature = shuntData.alarmReason_AR & 32;
_alarmHighTemperature = shuntData.alarmReason_AR & 64;

auto voltage = shuntData.batteryVoltage_V_mV / 1000.0f;
auto current = shuntData.batteryCurrent_I_mA / 1000.0f;
_oBatteryResistor = BatteryGuard.calculateInternalResistance(voltage, current);
_oOpenCircuitVoltage = BatteryGuard.calculateOpenCircuitVoltage(voltage, current);

_lastUpdate = VeDirectShunt.getLastUpdate();
}

Expand All @@ -881,6 +887,12 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const {
if (_tempPresent) {
addLiveViewValue(root, "temperature", _temperature, "°C", 0);
}
if (_oBatteryResistor.has_value()) {
addLiveViewValue(root, "resistor", _oBatteryResistor.value() * 1000.0f, "mOhm", 1);
}
if (_oOpenCircuitVoltage.has_value()) {
addLiveViewValue(root, "openCircuitVoltage", _oOpenCircuitVoltage.value(), "V", 3);
}

addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage);
addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage);
Expand Down
Loading

0 comments on commit 58a21c2

Please sign in to comment.