From 58a21c2aa09abc5ba914d21d27af1cfbfb492437 Mon Sep 17 00:00:00 2001 From: SW-Nico Date: Mon, 16 Dec 2024 19:47:21 +0100 Subject: [PATCH] BatteryGuard: Open circuit voltage and internal resistance --- include/BatteryGuard.h | 54 +++++++++ include/BatteryStats.h | 3 + include/Statistic.h | 56 +++++++++ src/BatteryGuard.cpp | 233 +++++++++++++++++++++++++++++++++++++ src/BatteryStats.cpp | 12 ++ src/main.cpp | 3 + webapp/src/locales/de.json | 2 + webapp/src/locales/en.json | 2 + webapp/src/locales/fr.json | 2 + 9 files changed, 367 insertions(+) create mode 100644 include/BatteryGuard.h create mode 100644 include/Statistic.h create mode 100644 src/BatteryGuard.cpp diff --git a/include/BatteryGuard.h b/include/BatteryGuard.h new file mode 100644 index 000000000..940d6e380 --- /dev/null +++ b/include/BatteryGuard.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include +#include "Statistic.h" + + +class BatteryGuardClass { + public: + BatteryGuardClass() = default; + ~BatteryGuardClass() = default; + + void init(Scheduler& scheduler); + void updateSettings(void); + + std::optional calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent); + std::optional getOpenCircuitVoltage(void) const; + std::optional calculateInternalResistance(float const nowVoltage, float const nowCurrent); + std::optional 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 _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 _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 _firstVolt = {0.0f,0.0f}; // first of two voltage and related current + std::pair _maxVolt = {0.0f,0.0f}; // maximum voltage and related current + std::pair _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; diff --git a/include/BatteryStats.h b/include/BatteryStats.h index cfce8ea88..13ba9664d 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -338,6 +338,9 @@ class VictronSmartShuntStats : public BatteryStats { bool _alarmLowSOC; bool _alarmLowTemperature; bool _alarmHighTemperature; + + std::optional _oBatteryResistor = std::nullopt; + std::optional _oOpenCircuitVoltage = std::nullopt; }; class MqttBatteryStats : public BatteryStats { diff --git a/include/Statistic.h b/include/Statistic.h new file mode 100644 index 000000000..bfc05acc4 --- /dev/null +++ b/include/Statistic.h @@ -0,0 +1,56 @@ +#pragma once + +/* + * Weighted average and statistics class (initialising value defines the weighted average 10 = 10%) +*/ +template +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 +}; + diff --git a/src/BatteryGuard.cpp b/src/BatteryGuard.cpp new file mode 100644 index 000000000..6ee7b5477 --- /dev/null +++ b/src/BatteryGuard.cpp @@ -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 +#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 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 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 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 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 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 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; +} diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index f4e383b29..919dc1878 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -7,6 +7,7 @@ #include "JkBmsDataPoints.h" #include "JbdBmsDataPoints.h" #include "MqttSettings.h" +#include "BatteryGuard.h" template static void addLiveViewInSection(JsonVariant& root, @@ -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(); } @@ -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); diff --git a/src/main.cpp b/src/main.cpp index 9a4ac2506..e608b1c15 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,6 +40,7 @@ #include #include #include +#include "BatteryGuard.h" void setup() { @@ -197,6 +198,8 @@ void setup() } Battery.init(scheduler); + + BatteryGuard.init(scheduler); } void loop() diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 8159e4cd0..57cc51773 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -995,6 +995,8 @@ "capacity": "Gesamtkapazität", "availableCapacity": "Verfügbare Kapazität", "temperature": "Temperatur", + "resistor": "Innenwiderstand", + "openCircuitVoltage": "Leerlaufspannung", "bmsTemp": "BMS-Temperatur", "chargeVoltage": "Gewünschte Ladespannung (BMS)", "chargeCurrentLimitation": "Ladestromlimit", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index cad075339..412678f68 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -999,6 +999,8 @@ "capacity": "Total capacity", "availableCapacity": "Available capacity", "temperature": "Temperature", + "resistor": "Internal resistance", + "openCircuitVoltage": "Open circuit voltage", "bmsTemp": "BMS temperature", "chargeVoltage": "Requested charge voltage", "chargeCurrentLimitation": "Charge current limit", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 5dc795908..c43d6fe0c 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -948,6 +948,8 @@ "current": "Current", "power": "Power", "temperature": "Temperature", + "resistor": "Internal resistance", + "openCircuitVoltage": "Open circuit voltage", "bmsTemp": "BMS temperature", "chargeVoltage": "Requested charge voltage", "chargeCurrentLimitation": "Charge current limit",