Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2d2bbd3
Statistics: Base
SW-Niko Feb 5, 2025
7dbdfa7
BatteryGuard: Open circuit voltage and internal resistance
SW-Niko Feb 15, 2025
77acf9a
Fix: Measurement period + Timebase minimum 1sec
SW-Niko Feb 16, 2025
0772d7d
BatteryGuard: show open circuit voltage in live view
SW-Niko Mar 6, 2025
5364f7f
BatteryGuard: handle different time stamps
SW-Niko Mar 6, 2025
07c795e
remove unused functions
SW-Niko Mar 22, 2025
926249a
Better handling for current resolution > 100mA, report calculation state
SW-Niko Mar 28, 2025
5f33111
add Configuration
AndreasBoehm Apr 8, 2025
9e55b23
add api and web ui for configuration (translations pending)
AndreasBoehm Apr 8, 2025
8228adb
webapp: battery guard: add translations
AndreasBoehm Apr 15, 2025
4e27b79
webapp: add battery guard info page
AndreasBoehm Apr 23, 2025
91b34cc
Ensure that const is used when possible, improve readability of check…
AndreasBoehm Apr 23, 2025
479963b
Statistics: Fix issue with negative integers
SW-Niko Mar 22, 2025
bc70728
Immediately remove “Open Circuit Voltage” and “Resistance” from the l…
SW-Niko Apr 23, 2025
7d342d9
Improved resistance calculation
SW-Niko Apr 23, 2025
3849934
Fix: allow calculation of resistor regardless of value of configured …
SW-Niko Apr 26, 2025
6d5ee95
switch BatteryGuard to ESP_LOG macros
AndreasBoehm May 23, 2025
3478fc2
webapp: batteryGuard: improve info view
AndreasBoehm May 23, 2025
080b24a
webapp: routes: adjust battery guard route name
AndreasBoehm May 23, 2025
c2e7f3c
webapp: update translations
SW-Niko May 29, 2025
d6db01a
default value for UI-delay, description improved, remove useless "%s"
SW-Niko May 30, 2025
96cfcf4
webapp: allow to translate 'Calculation not possible'
AndreasBoehm Jun 2, 2025
995dbd1
reduce logging during resistance calculation
SW-Niko Jun 7, 2025
3b4928a
fix: resistance calc in useable SoC range, update translation, use "b…
SW-Niko Jun 14, 2025
7cec82d
fix: check if SoC is valid
SW-Niko Jun 15, 2025
4a04741
fix: #undef TAG
SW-Niko Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions include/BatteryGuard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <Arduino.h>
#include <frozen/string.h>
#include <TaskSchedulerDeclarations.h>
#include "Statistic.h"


class BatteryGuardClass {
public:
BatteryGuardClass() = default;
~BatteryGuardClass() = default;
BatteryGuardClass(const BatteryGuardClass&) = delete;
BatteryGuardClass& operator=(const BatteryGuardClass&) = delete;
BatteryGuardClass(BatteryGuardClass&&) = delete;
BatteryGuardClass& operator=(BatteryGuardClass&&) = delete;

void init(Scheduler& scheduler);
void updateSettings(void);
void updateBatteryValues(float const nowVoltage, float const nowCurrent, uint32_t const millisCurrent);
bool isInternalResistanceCalculated(void) const;
std::optional<float> getOpenCircuitVoltage(void);
std::optional<float> getInternalResistance(void) const;

size_t getResistanceCalculationCount() const { return _resistanceFromCalcAVG.getCounts(); }
const char* getResistanceCalculationState() const { return getResistanceStateText(_rStateMax).data(); }
float getVoltageResolution() const { return _analyzedResolutionV; }
float getCurrentResolution() const { return _analyzedResolutionI; }
float getMeasurementPeriod() const { return _analyzedPeriod.getAverage(); }
float getVIStampDelay() const { return _analyzedUIDelay.getAverage(); }
bool isResolutionOK(void) const;

static constexpr float MAXIMUM_VOLTAGE_RESOLUTION = 0.020f; // 20mV
static constexpr float MAXIMUM_CURRENT_RESOLUTION = 0.200f; // 200mA
static constexpr float MAXIMUM_MEASUREMENT_TIME_PERIOD = 4000; // 4 seconds
static constexpr float MAXIMUM_V_I_TIME_STAMP_DELAY = 1000; // 1 seconds
static constexpr size_t MINIMUM_RESISTANCE_CALC = 5; // minimum number of calculations to use the calculated resistance

private:
void loop(void);
void slowLoop(void);

Task _slowLoopTask; // Task (print the report)
Task _fastLoopTask; // Task (get the battery values)
bool _useBatteryGuard = false; // "Battery guard" On/Off


struct Data { float value; uint32_t timeStamp; bool valid; };
Data _i1Data {0.0f, 0, false }; // buffer the last current data [current, millis(), true/false]
Data _u1Data {0.0f, 0, false }; // buffer the last voltage data [voltage, millis(), true/false]

// used to calculate the "Open circuit voltage"
enum class Text : uint8_t { Q_NODATA, Q_EXCELLENT, Q_GOOD, Q_BAD };

void calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent);
bool isDataValid() const { return (millis() - _battMillis) < 30*1000; }
void printOpenCircuitVoltageReport(void);

float _battVoltage = 0.0f; // actual battery voltage [V]
float _battCurrent = 0.0f; // actual battery current [A]
uint32_t _battMillis = 0; // measurement time stamp [millis()]
WeightedAVG<float> _battVoltageAVG {5}; // average battery voltage [V]
WeightedAVG<float> _openCircuitVoltageAVG {5}; // average battery open circuit voltage [V]
float _analyzedResolutionV = 0; // resolution from the battery voltage [V]
float _analyzedResolutionI = 0; // resolution from the battery current [V]
WeightedAVG<float> _analyzedPeriod {20}; // measurement period [ms]
WeightedAVG<float> _analyzedUIDelay {20}; // delay between voltage and current [ms]
size_t _notAvailableCounter = 0; // open circuit voltage not available counter


// used to calculate the "Battery internal resistance"
enum class RState : uint8_t { IDLE, RESOLUTION, SOC_NOT_VALID, SOC_RANGE, TIME, FIRST_PAIR, TRIGGER, SECOND_PAIR,
SECOND_BREAK, DELTA_POWER, TOO_BAD, CALCULATED };

void calculateInternalResistance(float const nowVoltage, float const nowCurrent);
frozen::string const& getResistanceStateText(RState state) const;

RState _rState = RState::IDLE; // holds the actual calculation state
RState _rStateMax = RState::IDLE; // holds the maximum calculation state
RState _rStateLast = RState::IDLE; // holds the last calculation state
float _resistanceFromConfig = 0.0f; // configured battery resistance [Ohm]
WeightedAVG<float> _resistanceFromCalcAVG {10}; // calculated battery resistance [Ohm]
bool _firstOfTwoAvailable = false; // true after to got the first of two values
bool _minMaxAvailable = false; // true if minimum and maximum values are available
bool _triggerEvent = false; // true if we have sufficient current change
bool _pairAfterTriggerAvailable = false; // true if after the trigger the first second pair is available
std::pair<float,float> _pFirstVolt = {0.0f,0.0f}; // first of two voltages and related current [V,A]
std::pair<float,float> _pMaxVolt = {0.0f,0.0f}; // maximum voltage and related current [V,A]
std::pair<float,float> _pMinVolt = {0.0f,0.0f}; // minimum voltage and related current [V,A]
float _checkCurrent = 0.0f; // used to check the current [A] after the trigger
uint32_t _lastTriggerMillis = 0; // last millis from the first min/max values [millis()]
uint32_t _lastDataInMillis = 0; // last millis for data in [millis()]

};

extern BatteryGuardClass BatteryGuard;
11 changes: 11 additions & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ struct SOLAR_CHARGER_CONFIG_T {
};
using SolarChargerConfig = struct SOLAR_CHARGER_CONFIG_T;

struct BATTERY_GUARD_CONFIG_T {
bool Enabled;
float InternalResistance;
};
using BatteryGuardConfig = struct BATTERY_GUARD_CONFIG_T;

struct CONFIG_T {
struct {
uint32_t Version;
Expand Down Expand Up @@ -436,6 +442,8 @@ struct CONFIG_T {

GridChargerConfig GridCharger;

BatteryGuardConfig BatteryGuard;

INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];

Expand Down Expand Up @@ -491,6 +499,7 @@ class ConfigurationClass {
static void serializeGridChargerConfig(GridChargerConfig const& source, JsonObject& target);
static void serializeGridChargerCanConfig(GridChargerCanConfig const& source, JsonObject& target);
static void serializeGridChargerHuaweiConfig(GridChargerHuaweiConfig const& source, JsonObject& target);
static void serializeBatteryGuardConfig(BatteryGuardConfig const& source, JsonObject& target);

static void deserializeHttpRequestConfig(JsonObject const& source_http_config, HttpRequestConfig& target);
static void deserializeSolarChargerConfig(JsonObject const& source, SolarChargerConfig& target);
Expand All @@ -508,6 +517,8 @@ class ConfigurationClass {
static void deserializeGridChargerConfig(JsonObject const& source, GridChargerConfig& target);
static void deserializeGridChargerCanConfig(JsonObject const& source, GridChargerCanConfig& target);
static void deserializeGridChargerHuaweiConfig(JsonObject const& source, GridChargerHuaweiConfig& target);
static void deserializeBatteryGuardConfig(JsonObject const& source, BatteryGuardConfig& target);

private:
void loop();
static double roundedFloat(float val);
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 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#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 * (static_cast<T>(_count) - 1) + num) / static_cast<T>(_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
};

2 changes: 2 additions & 0 deletions include/WebApi.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#pragma once

#include "WebApi_battery.h"
#include "WebApi_battery_guard.h"
#include "WebApi_device.h"
#include "WebApi_devinfo.h"
#include "WebApi_dtu.h"
Expand Down Expand Up @@ -57,6 +58,7 @@ class WebApiClass {
AsyncWebServer _server;

WebApiBatteryClass _webApiBattery;
WebApiBatteryGuardClass _webApiBatteryGuard;
WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu;
Expand Down
18 changes: 18 additions & 0 deletions include/WebApi_battery_guard.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>

class WebApiBatteryGuardClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);

private:
void onStatus(AsyncWebServerRequest* request);
void onMetaData(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request);

AsyncWebServer* _server;
};
2 changes: 2 additions & 0 deletions include/battery/Stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ class Stats {
uint8_t getSoCPrecision() const { return _socPrecision; }

float getVoltage() const { return _voltage; }
uint32_t getLastVoltageUpdate() const { return _lastUpdateVoltage; }
uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; }

float getChargeCurrent() const { return _current; };
uint32_t getLastCurrentUpdate() const { return _lastUpdateCurrent; }
uint8_t getChargeCurrentPrecision() const { return _currentPrecision; }

float getDischargeCurrentLimit() const { return _dischargeCurrentLimit; };
Expand Down
4 changes: 4 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,7 @@
#define GRIDCHARGER_HUAWEI_INPUT_CURRENT_LIMIT 0.0
#define GRIDCHARGER_HUAWEI_FAN_ONLINE_FULL_SPEED false
#define GRIDCHARGER_HUAWEI_FAN_OFFLINE_FULL_SPEED false

// BatteryGuard defaults
#define BATTERYGUARD_ENABLED false
#define BATTERYGUARD_INTERNAL_RESISTANCE 0.0f
Loading